Skip to content

Models RU

Алексей . edited this page Jul 26, 2026 · 1 revision

Модели

English Русский

Справочник по всем публичным моделям библиотеки: страницы логов, данные аккаунта, каталог фильтров, отчёты по активности администраторов и топу операций, а также транспортные и конфигурационные типы.

Соглашения

Эти правила действуют для всех моделей на этой странице.

  • Все публичные модели — запечатанные неизменяемые записи (sealed record). Единственное исключение — LogsParserRegistrationOptions: это sealed class с изменяемыми свойствами, потому что он заполняется конфигурационным делегатом.
  • Коллекции никогда не бывают null. Если модель объявляет IReadOnlyList<T>, он всегда есть; когда на исходной странице ничего нет, парсер записывает Array.Empty<T>(). Проверяйте Count == 0, а не null.
  • Необязательные скалярные значения объявлены как nullable. long?, string? и nullable-ссылки на модели означают «страница этого не показала», а не «ноль» или «пусто».
  • Записи дают равенство по значению, ToString(), Deconstruct и with. Позиционные параметры становятся публичными init-свойствами с теми же именами.
  • Модели ничего не валидируют. Их конструирование не выбрасывает исключений; перечисленные ниже исключения приходят от парсеров и транспорта, которые эти модели создают.

Пространства имён

Типы Пространство имён
ParserRequest, ParserCookie LogsParser
Все остальные модели этой страницы LogsParser.Models

Имена папок в репозитории не совпадают с пространствами имён: в Models/Shared/CommonModels.cs объявлено namespace LogsParser;, во всех остальных файлах под Models/ — namespace LogsParser.Models;.

Откуда берутся модели

Модель Кем создаётся Исключения
LogsPage LogsParserClient.GetLogsAsync, LogsHtmlParser.ParseLogs ArgumentException на пустом HTML; при отсутствии разметки таблицы деградирует до пустого списка Entries
LogsAccount LogsParserClient.GetCurrentAccountAsync; также вложена в LogsPage.Account и LogsFilterCatalog.Account ArgumentException на пустом HTML; null, если в разметке нет данных аккаунта
LogsFilterCatalog LogsParserClient.GetLogsFilterCatalogAsync, LogsFilterCatalogParser.Parse ArgumentException на пустом HTML
AdminActivityReport LogsParserClient.GetAdminActivityAsync, LogsHtmlParser.ParseAdminActivity ArgumentException на пустом HTML; HtmlParsingException, если не найдены поля периода или таблица
TopOperationsReport LogsParserClient.GetTopOperationsAsync, LogsHtmlParser.ParseTopOperations ArgumentException на пустом HTML; HtmlParsingException, если не найдены дата или таблица

Модели запросов LogsQuery, AdminActivityQuery и TopOperationsQuery описаны на странице Запросы и построитель URI.

Логи

LogsPage

public sealed record LogsPage(
    IReadOnlyList<LogEntry> Entries,
    LogPageMetaInfo? MetaInfo = null,
    LogsAccount? Account = null);
Член Тип Описание
Entries IReadOnlyList<LogEntry> Строки таблицы логов в порядке страницы. Пустой список, если на странице нет <tbody>.
MetaInfo LogPageMetaInfo? Счётчики пагинации; null, если на странице нет строки пагинации.
Account LogsAccount? Аккаунт, прочитанный из разметки страницы; null, если данных аккаунта в ней нет.
var page = await client.GetLogsAsync(new LogsQuery(ServerId: 18, Limit: 100));

Console.WriteLine($"{page.Entries.Count} entries of {page.MetaInfo?.Total} total");
Console.WriteLine($"signed in as {page.Account?.Nickname}");

LogEntry

public sealed record LogEntry(
    DateTime Timestamp,
    string Text,
    string Html,
    LogParticipant? Sender = null,
    LogParticipant? Target = null)
{
    public IReadOnlyList<LogRevealedValue> RevealedValues { get; init; } = Array.Empty<LogRevealedValue>();
}
Член Тип Описание
Timestamp DateTime Дата и время строки, разобранные с инвариантной культурой.
Text string Текст действия в том виде, в котором страница его показывает.
Html string Полная внутренняя разметка ячейки действия, включая скрытые блоки.
Sender LogParticipant? Участник «I»; null, если у строки нет данных по нему.
Target LogParticipant? Участник «II»; null, если у строки нет данных по нему.
RevealedValues IReadOnlyList<LogRevealedValue> Блоки, скрытые сайтом за кнопкой-глазом. Пусто, если таких блоков нет.

RevealedValues — это init-свойство, а не позиционный параметр записи. Так сделано намеренно: добавление позиционного параметра изменило бы арность первичного конструктора и сгенерированный Deconstruct уже опубликованной записи, поэтому свойство добавлено в тело записи. Первичный конструктор по-прежнему принимает пять аргументов, а Deconstruct по-прежнему возвращает (Timestamp, Text, Html, Sender, Target). Коллекция задаётся через инициализатор объекта:

var entry = new LogEntry(DateTime.UtcNow, "Готовый сет:", "<td>…</td>")
{
    RevealedValues = [new LogRevealedValue("Значение строки 1", "1.БоДжек Редкость 6(9553)", "…")]
};

var (timestamp, text, html, sender, target) = entry;

Text и Html — не два представления одного и того же. Text содержит только то, что страница показывает видимо: содержимое, скрытое за кнопкой-глазом, из него удалено, — тогда как Html сохраняет полную разметку ячейки вместе со скрытыми блоками. Для строки со скрытым сетом:

// Text ограничен видимой частью…
Console.WriteLine(entry.Text);   // Игрок Ivan_Kuznetsov арендует объявление №4851. Готовый сет:
// …а Html по-прежнему содержит блок app__hidden с самим сетом.
Console.WriteLine(entry.Html.Contains("app__hidden"));  // True

Всё, что было вырезано из Text, доступно в структурированном виде через RevealedValues.

LogRevealedValue

public sealed record LogRevealedValue(
    string Label,
    string Text,
    string Html);
Член Тип Описание
Label string Атрибут data-title блока, например Значение строки 1. Пустая строка, если атрибута нет.
Text string Раскрытое значение в виде текста с сохранением построчной структуры исходного блока.
Html string Исходная внутренняя разметка блока.

Раскрываемые значения чаще всего встречаются под фильтром item_market, где строка прячет за кнопкой-глазом целый набор предметов. Поскольку Text сохраняет переводы строк, набор можно разложить обратно на строки:

foreach (var value in entry.RevealedValues)
{
    Console.WriteLine(value.Label);   // Значение строки 1
    foreach (var line in value.Text.Split('\n', StringSplitOptions.RemoveEmptyEntries))
    {
        Console.WriteLine($"  {line.Trim()}");   // 1.БоДжек Редкость 6(9553), Описание: …, 2.ТВ Робот(8583), …
    }
}

LogParticipant

public sealed record LogParticipant(
    long? Money = null,
    long? Bank = null,
    long? Donate = null,
    LogAdditionalInfo? AdditionalInfo = null,
    string? LastIp = null,
    string? RegistrationIp = null);
Член Тип Описание
Money long? Наличные на момент записи.
Bank long? Баланс банковского счёта.
Donate long? Баланс донат-валюты.
AdditionalInfo LogAdditionalInfo? Блок сведений об аккаунте, скрытый за кнопкой-глазом в ячейке данных.
LastIp string? Последний IP-адрес из ячейки IP.
RegistrationIp string? Регистрационный IP-адрес из ячейки IP.

LogEntry.Sender — это участник «I», а LogEntry.Target — участник «II», ровно так, как сайт помечает их в ячейке данных. Каждый участник привязан к блоку дополнительной информации, следующему за его собственным маркером, а не сопоставляется с блоками по индексу, поэтому строка, где есть только получатель, никогда не припишет его данные отправителю:

// Строка, в ячейке данных которой есть только маркер «II:».
Console.WriteLine(entry.Sender is null);                       // True
Console.WriteLine(entry.Target?.AdditionalInfo?.AccountId);    // 999111

Участник создаётся только тогда, когда строка содержит для его маркера суммы, блок дополнительной информации или IP-адрес; иначе свойство остаётся null.

LogAdditionalInfo

public sealed record LogAdditionalInfo(
    long AccountId,
    long VC,
    long SubAccount1,
    long SubAccount2,
    long SubAccount3,
    long SubAccount4,
    long SubAccount5,
    long SubAccount6,
    long Deposit,
    int AdminLevel);
Член Тип Описание
AccountId long Идентификатор аккаунта (ID аккаунта).
VC long Баланс VC$.
SubAccount1 … SubAccount6 long Шесть дополнительных счетов (Доп. счёт №1 … №6), берутся со страницы как есть. Сайт часто выводит здесь -1; библиотека не приписывает этому значению никакого смысла.
Deposit long Баланс депозита.
AdminLevel int Уровень администрирования.

Блок разбирается только при наличии всех десяти значений; неполный блок даёт null в LogParticipant.AdditionalInfo.

LogPageMetaInfo

public sealed record LogPageMetaInfo(int Start, int End, int Total);
Член Тип Описание
Start int Номер первой строки на странице (нумерация с единицы).
End int Номер последней строки на странице.
Total int Общее число строк, подходящих под запрос, по всем страницам.

Читается из строки Показано с … по … из …, которая присутствует даже тогда, когда самой таблицы нет: страница без <tbody> всё равно возвращает мета-информацию.

if (page.MetaInfo is { } meta)
{
    var pages = (int)Math.Ceiling(meta.Total / (double)(meta.End - meta.Start + 1));
    Console.WriteLine($"{meta.Start}-{meta.End} of {meta.Total} ({pages} pages)");
}

Аккаунт

LogsAccount

public sealed record LogsAccount(
    string Nickname,
    IReadOnlyList<LogsAccountBadge> Badges,
    IReadOnlyList<LogsAccountServer> AvailableServers);
Член Тип Описание
Nickname string Никнейм из панели навигации. Пустая строка, если разметки нет.
Badges IReadOnlyList<LogsAccountBadge> Значки рядом с никнеймом, без повторов по имени.
AvailableServers IReadOnlyList<LogsAccountServer> Серверы, предлагаемые селектором сервера.

Вся модель равна null (а не пуста), если в разметке нет ни никнейма, ни значков, ни селектора серверов, — это признак того, что HTML не является страницей авторизованного пользователя.

LogsAccountBadge

public sealed record LogsAccountBadge(string Name);
Член Тип Описание
Name string Текст значка, например Admin или VIP.

LogsAccountServer

public sealed record LogsAccountServer(
    int Id,
    string Name,
    string DisplayName,
    bool IsSelected);
Член Тип Описание
Id int Номер сервера, используемый как LogsQuery.ServerId.
Name string Только имя сервера, без числового префикса (Phoenix).
DisplayName string Подпись в том виде, в котором она отрисована, включая числовой префикс ([18] Phoenix).
IsSelected bool true для сервера, выбранного в текущей сессии.

Если подпись не соответствует форме [n] Имя, Name и DisplayName совпадают.

var account = await client.GetCurrentAccountAsync();

var current = account?.AvailableServers.FirstOrDefault(server => server.IsSelected);
Console.WriteLine($"{current?.DisplayName} → ServerId {current?.Id}");   // [18] Phoenix → ServerId 18

var page = await client.GetLogsAsync(new LogsQuery(ServerId: current!.Id));

Каталог фильтров

LogsFilterCatalog

public sealed record LogsFilterCatalog(
    IReadOnlyList<LogsFilterDefinition> Filters,
    LogsAccount? Account = null);
Член Тип Описание
Filters IReadOnlyList<LogsFilterDefinition> Все фильтры, предлагаемые селектором типа логов.
Account LogsAccount? Аккаунт, прочитанный из разметки той же страницы.

LogsFilterDefinition

public sealed record LogsFilterDefinition(
    string Code,
    string Name,
    IReadOnlyList<LogsFilterAdditionalParameter> AdditionalParameters);
Член Тип Описание
Code string Код фильтра, отправляемый сервису, например kill или item_market. Именно он попадает в LogsQuery.Filters.
Name string Человекочитаемая русская подпись, например Убийство.
AdditionalParameters IReadOnlyList<LogsFilterAdditionalParameter> Дополнительные поля, которые сайт показывает для этого фильтра. Пусто, если их нет.

LogsFilterAdditionalParameter

public sealed record LogsFilterAdditionalParameter(
    string QueryKey,
    string Label);
Член Тип Описание
QueryKey string Ключ поля в строке запроса, всегда вида dynamic[n], например dynamic[0].
Label string Подпись поля, например Оружие.

QueryKey — это ровно тот ключ, который принимает LogsQuery.AdditionalParameters: построитель URI отвергает ключи, не соответствующие ^dynamic\[\d+\]$:

var catalog = await client.GetLogsFilterCatalogAsync();
var kill = catalog.Filters.Single(filter => filter.Code == "kill");
var weapon = kill.AdditionalParameters.First();   // QueryKey "dynamic[0]", Label "Оружие"

var page = await client.GetLogsAsync(new LogsQuery(
    ServerId: 18,
    Filters: [kill.Code],
    AdditionalParameters: new Dictionary<string, string> { [weapon.QueryKey] = "Deagle" }));

Активность администраторов

AdminActivityReport

public sealed record AdminActivityReport(
    (DateTime From, DateTime To) Period,
    IReadOnlyList<AdminActivityEntry> Entries,
    AdminActivityMetaInfo MetaInfo);
Член Тип Описание
Period (DateTime From, DateTime To) Период, который сервис фактически отдал; читается обратно из полей формы.
Entries IReadOnlyList<AdminActivityEntry> По одной записи на администратора.
MetaInfo AdminActivityMetaInfo Сводные счётчики, никогда не null.

Period — это именованный кортеж, а не запись: обращайтесь к нему как report.Period.From / report.Period.To либо деконструируйте:

var report = await client.GetAdminActivityAsync(
    new AdminActivityQuery(DateTime.Today.AddDays(-7), DateTime.Today));

var (from, to) = report.Period;
Console.WriteLine($"{from:yyyy-MM-dd} — {to:yyyy-MM-dd}");

AdminActivityEntry

public sealed record AdminActivityEntry(
    string Nickname,
    int Id,
    int AdminLevel,
    int TotalReports,
    int TotalMutes,
    int TotalJails,
    int TotalWarns,
    int TotalBans,
    int TotalThanks,
    TimeSpan TotalOnline,
    IReadOnlyList<AdminActivityDay> Details);
Член Тип Описание
Nickname string Никнейм администратора.
Id int Идентификатор администратора.
AdminLevel int Уровень администрирования.
TotalReports int Обработано репортов за период.
TotalMutes int Выдано мутов за период.
TotalJails int Выдано джейлов за период.
TotalWarns int Выдано варнов за период.
TotalBans int Выдано банов за период.
TotalThanks int Получено благодарностей за период.
TotalOnline TimeSpan Суммарное время онлайн.
Details IReadOnlyList<AdminActivityDay> Разбивка по дням. Пусто, если у строки нет раскрываемого блока.

TotalOnline не читается из отдельной ячейки: это сумма значений Online по дням из Details. Когда Details пуст, TotalOnline равен TimeSpan.Zero, даже если администратор был онлайн.

foreach (var admin in report.Entries.OrderByDescending(entry => entry.TotalOnline))
{
    Console.WriteLine($"{admin.Nickname} [{admin.AdminLevel}] {admin.TotalOnline:hh\\:mm\\:ss} " +
                      $"over {admin.Details.Count} days, {admin.TotalBans} bans");
}

AdminActivityDay

public sealed record AdminActivityDay(
    DateTime Date,
    TimeSpan Online,
    int Reports,
    int Mutes,
    int Jails,
    int Warns,
    int Bans);
Член Тип Описание
Date DateTime День, разобранный из формата yyyy-MM-dd.
Online TimeSpan Время онлайн за этот день (hh:mm:ss).
Reports int Обработано репортов за этот день.
Mutes int Выдано мутов за этот день.
Jails int Выдано джейлов за этот день.
Warns int Выдано варнов за этот день.
Bans int Выдано банов за этот день.

AdminActivityMetaInfo

public sealed record AdminActivityMetaInfo(int AdminCount, int PeriodDays, int TotalReports, int TotalBans);
Член Тип Описание
AdminCount int Количество разобранных записей.
PeriodDays int Полных дней между Period.From и Period.To, никогда не отрицательное.
TotalReports int Сумма TotalReports по всем записям.
TotalBans int Сумма TotalBans по всем записям.

Все счётчики вычисляются по разобранным записям, а не читаются со страницы.

Топ операций

TopOperationsReport

public sealed record TopOperationsReport(
    DateOnly Date,
    IReadOnlyList<TopOperationsEntry> Entries,
    TopOperationsMetaInfo MetaInfo);
Член Тип Описание
Date DateOnly День, за который построен отчёт; читается из строки Данные за:.
Entries IReadOnlyList<TopOperationsEntry> По одной записи на игрока.
MetaInfo TopOperationsMetaInfo Сводные счётчики, никогда не null.

Date имеет тип DateOnly: отчёт строится по дням и не содержит времени — в отличие от TopOperationsQuery.Date, который объявлен как DateTime?.

TopOperationsEntry

public sealed record TopOperationsEntry(
    string Nickname,
    int Id,
    IPAddress Ip,
    IPAddress RegistrationIp,
    int TotalTransactions,
    ulong Sum);
Член Тип Описание
Nickname string Никнейм игрока.
Id int Идентификатор игрока.
Ip IPAddress Последний IP-адрес.
RegistrationIp IPAddress Регистрационный IP-адрес.
TotalTransactions int Количество транзакций за день.
Sum ulong Суммарный оборот игрока.

Оба IP-члена имеют тип System.Net.IPAddress (пространство имён System.Net), а не string, — обратите внимание на асимметрию с LogParticipant.LastIp / RegistrationIp, которые объявлены как string?. Неразбираемый адрес заменяется на IPAddress.Loopback с записью предупреждения в лог.

var top = await client.GetTopOperationsAsync(new TopOperationsQuery(Date: DateTime.Today));

foreach (var entry in top.Entries.Where(e => e.Ip.Equals(e.RegistrationIp)))
{
    Console.WriteLine($"{entry.Nickname} ({entry.Id}): {entry.Sum:N0} in {entry.TotalTransactions} transactions");
}

TopOperationsMetaInfo

public sealed record TopOperationsMetaInfo(int PlayerCount, int TotalTransactions, ulong TotalSum);
Член Тип Описание
PlayerCount int Количество разобранных записей.
TotalTransactions int Сумма TotalTransactions по всем записям.
TotalSum ulong Сумма Sum по всем записям, тоже ulong.

Транспорт и конфигурация

ParserRequest

public sealed record ParserRequest(string RelativeUri, ICookieStorage? CookieStorage = null);

Пространство имён LogsParser.

Член Тип Описание
RelativeUri string Путь и строка запроса относительно LogsParserHttpOptions.BaseUri.
CookieStorage ICookieStorage? Хранилище, переопределяющее хранилище источника данных для этого одного запроса. При null используется хранилище по умолчанию.
using LogsParser;
using LogsParser.Abstractions;

ILogsParserDataSource dataSource = /* … */;
var html = await dataSource.GetContentAsync(new ParserRequest("/?server_number=18&limit=100"));

ParserCookie

public sealed record ParserCookie(string Name, string Value);

Пространство имён LogsParser.

Член Тип Описание
Name string Имя cookie; сравнивается через StringComparer.Ordinal.
Value string Значение cookie.

Cookie — это пара «имя/значение» и ничего больше: все атрибуты Set-Cookie (путь, срок жизни, флаги) намеренно отбрасываются. Сессионные cookie (arizonarp_session, XSRF-TOKEN) и токен анти-DDoS R3ACTLB хранятся в одном плоском хранилище.

using LogsParser;
using LogsParser.Abstractions;

var storage = new MemoryCookieStorage();
storage.SetCookies([new ParserCookie("arizonarp_session", "restored-value")]);

LogsParserCredentials

public sealed record LogsParserCredentials(string Login, string Password, string TotpSecret);
Член Тип Описание
Login string Логин на сайте.
Password string Пароль от сайта.
TotpSecret string Base32-секрет TOTP, по которому генерируются коды RFC 6238 для подтверждения двухфакторной аутентификации.

Для источника данных учётные данные необязательны: без них он запрашивает страницы с теми cookie, что у него есть, и завершается исключением аутентификации, когда сервис требует вход. Пароли и секреты никогда не попадают в логи.

LogsParserHttpOptions

public sealed record LogsParserHttpOptions
Свойство Тип По умолчанию Описание
BaseUri Uri new("https://arizonarp.logsparser.info/") Базовый адрес всех запросов.
UserAgent string "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36" Заголовок User-Agent.
Accept string "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7" Заголовок Accept.
MaxRetryAttempts int 5 Верхняя граница числа повторов при временных сбоях. Решение анти-DDoS-задачи и ожидание известного момента сброса лимита попытку не расходуют.
WaitForRateLimitReset bool true При 429 дождаться момента сброса и повторить запрос; при false вместо этого выбрасывается RateLimitExceededException.

Все свойства объявлены как init-свойства, поэтому изменённая копия создаётся через with:

using LogsParser.Models;
using LogsParser.Net;

var options = new LogsParserHttpOptions() with
{
    MaxRetryAttempts = 2,
    WaitForRateLimitReset = false
};

using var dataSource = new LogsParserHttpDataSource(options: options);

BaseUri, UserAgent и Accept применяются только к тому HttpClient, который их ещё не задал: внедрённый клиент сохраняет собственные BaseAddress и заголовки.

LogsParserRegistrationOptions

public sealed class LogsParserRegistrationOptions

Этот тип — запечатанный класс с изменяемыми свойствами, а не запись: он передаётся в делегат Action<LogsParserRegistrationOptions> метода AddLogsParser и заполняется в нём.

Свойство Тип По умолчанию Описание
Credentials LogsParserCredentials? null Учётные данные, регистрируемые как singleton, если заданы.
HttpOptions LogsParserHttpOptions new() Настройки транспорта, регистрируемые как singleton.
CookieStorageFactory Func<IServiceProvider, ICookieStorage>? null Заменяет регистрацию MemoryCookieStorage как singleton по умолчанию.
DataSourceFactory Func<IServiceProvider, ILogsParserDataSource>? null Заменяет регистрацию LogsParserHttpDataSource как transient по умолчанию.
using LogsParser.DependencyInjection;
using LogsParser.Models;

services.AddLogsParser(options =>
{
    options.Credentials = new LogsParserCredentials("my_login", "my_password", "BASE32SECRET");
    options.HttpOptions = new LogsParserHttpOptions { MaxRetryAttempts = 3 };
});

Разбор страницы LogsPage: полный пример

using LogsParser;
using LogsParser.Models;
using LogsParser.Net;

using var dataSource = new LogsParserHttpDataSource(
    credentials: new LogsParserCredentials("my_login", "my_password", "BASE32SECRET"));

var client = new LogsParserClient(dataSource);

var page = await client.GetLogsAsync(new LogsQuery(
    ServerId: 18,
    Filters: ["item_market"],
    PeriodFrom: DateTime.Today.AddDays(-1),
    PeriodTo: DateTime.Today,
    Limit: 100));

if (page.MetaInfo is { } meta)
{
    Console.WriteLine($"rows {meta.Start}–{meta.End} of {meta.Total}");
}

foreach (var entry in page.Entries)
{
    // Text — это то, что показывает страница; скрытых блоков в нём нет.
    Console.WriteLine($"{entry.Timestamp:yyyy-MM-dd HH:mm:ss}  {entry.Text}");

    // «I» — отправитель.
    if (entry.Sender is { } sender)
    {
        Console.WriteLine($"  I : money={sender.Money} bank={sender.Bank} donate={sender.Donate}");
        Console.WriteLine($"      ip={sender.LastIp} reg={sender.RegistrationIp}");

        if (sender.AdditionalInfo is { } info)
        {
            Console.WriteLine($"      account={info.AccountId} deposit={info.Deposit} adminLevel={info.AdminLevel}");
        }
    }

    // «II» — получатель. Присутствует сам по себе, даже если отправителя нет.
    if (entry.Target is { } target)
    {
        Console.WriteLine($"  II: money={target.Money} account={target.AdditionalInfo?.AccountId}");
    }

    // Всё, что прячет кнопка-глаз, — с подписью и построчной структурой.
    foreach (var revealed in entry.RevealedValues)
    {
        Console.WriteLine($"  [{revealed.Label}]");
        foreach (var line in revealed.Text.Split('\n'))
        {
            if (!string.IsNullOrWhiteSpace(line))
            {
                Console.WriteLine($"    {line.Trim()}");
            }
        }
    }
}

См. также

Clone this wiki locally