Compare commits

...

5 Commits

Author SHA1 Message Date
ad5576958f Рефактор
All checks were successful
Create and publish a Docker image / Publish image (push) Successful in 4m6s
2025-10-15 00:03:31 +03:00
e0fffbd4b5 Добавил копию контроллера для мягкого переезда 2025-10-14 23:18:44 +03:00
ec389bb596 Добавил поддержку документации плагинов 2025-10-14 23:18:26 +03:00
498a183be5 Заглушил Quartz 2025-10-14 23:18:08 +03:00
496def0166 Рефакторинг 2025-10-14 23:17:55 +03:00
12 changed files with 125 additions and 50 deletions

View File

@@ -1,5 +1,7 @@
using Microsoft.Playwright;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Microsoft.Playwright;
namespace SfeduSchedule.BrowserScripts;
public static class MicrosoftLoginHelper public static class MicrosoftLoginHelper
{ {

View File

@@ -0,0 +1,69 @@
using System.Net;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using SfeduSchedule.Abstractions;
using SfeduSchedule.Services;
namespace SfeduSchedule.Controllers;
[ApiController]
[Route("api/proxy")]
[EnableRateLimiting("throttle")]
public class ProxyController(ModeusService modeusService, ILogger<ScheduleController> logger) : ControllerBase
{
/// <summary>
/// Получить расписание по пользовательскому запросу.
/// </summary>
/// <param name="request">Объект запроса, содержащий параметры фильтрации расписания.</param>
/// <returns>Список событий расписания.</returns>
/// <response code="200">Возвращает расписание</response>
/// <response code="429">Слишком много запросов</response>
[HttpPost]
[Route("events/search")]
public async Task<IActionResult> Post([FromBody] ModeusScheduleRequest request)
{
string? schedule;
try
{
schedule = await modeusService.GetScheduleAsync(request);
}
catch (HttpRequestException e)
{
logger.LogError("Ошибка при получении расписания\n\n" + e.Message + "\n\n" + e.StackTrace +
"\n\n JSON: " +
JsonSerializer.Serialize(request, GlobalVariables.JsonSerializerOptions));
return StatusCode((int)(e.StatusCode ?? HttpStatusCode.InternalServerError),
"Proxied Modeus: " + e.Message);
}
return Ok(schedule);
}
/// <summary>
/// Поиск аудиторий по пользовательскому запросу.
/// </summary>
/// <param name="request">Объект запроса, содержащий параметры фильтрации аудиторий.</param>
/// <returns>Список аудиторий.</returns>
/// <response code="200">Возвращает список аудиторий</response>
/// <response code="429">Слишком много запросов</response>
[HttpPost]
[Route("rooms/search")]
public async Task<IActionResult> SearchRooms([FromBody] RoomSearchRequest request)
{
string? rooms;
try
{
rooms = await modeusService.SearchRoomsAsync(request);
}
catch (HttpRequestException e)
{
logger.LogError("Ошибка при поиске аудиторий\n\n" + e.Message + "\n\n" + e.StackTrace + "\n\n JSON: " +
JsonSerializer.Serialize(request, GlobalVariables.JsonSerializerOptions));
return StatusCode((int)(e.StatusCode ?? HttpStatusCode.InternalServerError),
"Proxied Modeus: " + e.Message);
}
return Ok(rooms);
}
}

View File

@@ -9,12 +9,12 @@ using SfeduSchedule.Services;
namespace SfeduSchedule.Controllers; namespace SfeduSchedule.Controllers;
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/schedule")]
[EnableRateLimiting("throttle")] [EnableRateLimiting("throttle")]
public class ScheduleController(ModeusService modeusService, ILogger<ScheduleController> logger) : ControllerBase public class ScheduleController(ModeusService modeusService, ILogger<ScheduleController> logger) : ControllerBase
{ {
/// <summary> /// <summary>
/// Получить расписание по пользовательскому запросу. /// [УСТАРЕЛО] Получить расписание по пользовательскому запросу.
/// </summary> /// </summary>
/// <param name="request">Объект запроса, содержащий параметры фильтрации расписания.</param> /// <param name="request">Объект запроса, содержащий параметры фильтрации расписания.</param>
/// <returns>Список событий расписания.</returns> /// <returns>Список событий расписания.</returns>
@@ -32,7 +32,7 @@ public class ScheduleController(ModeusService modeusService, ILogger<ScheduleCon
{ {
logger.LogError("Ошибка при получении расписания\n\n" + e.Message + "\n\n" + e.StackTrace + logger.LogError("Ошибка при получении расписания\n\n" + e.Message + "\n\n" + e.StackTrace +
"\n\n JSON: " + "\n\n JSON: " +
JsonSerializer.Serialize(request, GlobalVariables.jsonSerializerOptions)); JsonSerializer.Serialize(request, GlobalVariables.JsonSerializerOptions));
return StatusCode((int)(e.StatusCode ?? HttpStatusCode.InternalServerError), return StatusCode((int)(e.StatusCode ?? HttpStatusCode.InternalServerError),
"Proxied Modeus: " + e.Message); "Proxied Modeus: " + e.Message);
} }
@@ -41,7 +41,7 @@ public class ScheduleController(ModeusService modeusService, ILogger<ScheduleCon
} }
/// <summary> /// <summary>
/// Поиск аудиторий по пользовательскому запросу. /// [УСТАРЕЛО] Поиск аудиторий по пользовательскому запросу.
/// </summary> /// </summary>
/// <param name="request">Объект запроса, содержащий параметры фильтрации аудиторий.</param> /// <param name="request">Объект запроса, содержащий параметры фильтрации аудиторий.</param>
/// <returns>Список аудиторий.</returns> /// <returns>Список аудиторий.</returns>
@@ -59,7 +59,7 @@ public class ScheduleController(ModeusService modeusService, ILogger<ScheduleCon
catch (HttpRequestException e) catch (HttpRequestException e)
{ {
logger.LogError("Ошибка при поиске аудиторий\n\n" + e.Message + "\n\n" + e.StackTrace + "\n\n JSON: " + logger.LogError("Ошибка при поиске аудиторий\n\n" + e.Message + "\n\n" + e.StackTrace + "\n\n JSON: " +
JsonSerializer.Serialize(request, GlobalVariables.jsonSerializerOptions)); JsonSerializer.Serialize(request, GlobalVariables.JsonSerializerOptions));
return StatusCode((int)(e.StatusCode ?? HttpStatusCode.InternalServerError), return StatusCode((int)(e.StatusCode ?? HttpStatusCode.InternalServerError),
"Proxied Modeus: " + e.Message); "Proxied Modeus: " + e.Message);
} }

View File

@@ -5,14 +5,10 @@ using SfeduSchedule.Services;
namespace SfeduSchedule.Controllers namespace SfeduSchedule.Controllers
{ {
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/sfedu")]
[Authorize(AuthenticationSchemes = "OpenIdConnect")] [Authorize(AuthenticationSchemes = "OpenIdConnect")]
public class SfeduController : ControllerBase public class SfeduController(ModeusService modeusService) : ControllerBase
{ {
private readonly ModeusService _modeusService;
public SfeduController(ModeusService modeusService) =>
_modeusService = modeusService;
/// <summary> /// <summary>
/// Получить GUID пользователя через авторизацию Microsoft. /// Получить GUID пользователя через авторизацию Microsoft.
/// </summary> /// </summary>
@@ -30,7 +26,7 @@ namespace SfeduSchedule.Controllers
if (string.IsNullOrEmpty(name)) if (string.IsNullOrEmpty(name))
return StatusCode(StatusCodes.Status500InternalServerError); return StatusCode(StatusCodes.Status500InternalServerError);
var guid = await _modeusService.GetGuidAsync(name); var guid = await modeusService.GetGuidAsync(name);
if (string.IsNullOrEmpty(guid)) if (string.IsNullOrEmpty(guid))
return NotFound(); return NotFound();

View File

@@ -5,6 +5,6 @@ namespace SfeduSchedule
public static class GlobalVariables public static class GlobalVariables
{ {
public static string JwtFilePath { get; set; } = "data/jwt.txt"; public static string JwtFilePath { get; set; } = "data/jwt.txt";
public static readonly JsonSerializerOptions jsonSerializerOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; public static readonly JsonSerializerOptions JsonSerializerOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
} }
} }

View File

@@ -1,5 +1,6 @@
using Microsoft.Playwright; using Microsoft.Playwright;
using Quartz; using Quartz;
using SfeduSchedule.BrowserScripts;
namespace SfeduSchedule.Jobs; namespace SfeduSchedule.Jobs;

View File

@@ -18,20 +18,26 @@ public static class PluginLoader
foreach (var file in Directory.EnumerateFiles(pluginsDir, "*.plugin.dll", SearchOption.AllDirectories)) foreach (var file in Directory.EnumerateFiles(pluginsDir, "*.plugin.dll", SearchOption.AllDirectories))
{ {
var path = Path.GetFullPath(file); try
var alc = new PluginLoadContext(path); {
var asm = alc.LoadFromAssemblyPath(path); var path = Path.GetFullPath(file);
var alc = new PluginLoadContext(path);
var asm = alc.LoadFromAssemblyPath(path);
// Ищем реализацию IPlugin var pluginType = asm
var pluginType = asm .GetTypes()
.GetTypes() .FirstOrDefault(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface);
.FirstOrDefault(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface);
if (pluginType is null) if (pluginType is null)
continue; continue;
var instance = (IPlugin)Activator.CreateInstance(pluginType)!; var instance = (IPlugin)Activator.CreateInstance(pluginType)!;
result.Add(new LoadedPlugin(instance, asm, alc)); result.Add(new LoadedPlugin(instance, asm, alc));
}
catch (Exception ex)
{
Console.WriteLine($"Ошибка загрузки плагина {file}: {ex.Message}");
}
} }
return result; return result;
@@ -39,15 +45,9 @@ public static class PluginLoader
} }
// Отдельный контекст загрузки для изоляции зависимостей плагина // Отдельный контекст загрузки для изоляции зависимостей плагина
public sealed class PluginLoadContext : AssemblyLoadContext public sealed class PluginLoadContext(string pluginMainAssemblyPath) : AssemblyLoadContext(isCollectible: true)
{ {
private readonly AssemblyDependencyResolver _resolver; private readonly AssemblyDependencyResolver _resolver = new(pluginMainAssemblyPath);
public PluginLoadContext(string pluginMainAssemblyPath)
: base(isCollectible: true)
{
_resolver = new AssemblyDependencyResolver(pluginMainAssemblyPath);
}
// Разрешаем управляемые зависимости плагина из его папки. // Разрешаем управляемые зависимости плагина из его папки.
// Возвращаем null, чтобы отдать решение в Default ALC для общих сборок (например, SfeduSchedule.Abstractions). // Возвращаем null, чтобы отдать решение в Default ALC для общих сборок (например, SfeduSchedule.Abstractions).

View File

@@ -20,9 +20,7 @@ string updateJwtCron = configuration["UPDATE_JWT_CRON"] ?? "0 0 4 ? * *";
// Если не указана TZ, ставим Europe/Moscow // Если не указана TZ, ставим Europe/Moscow
if (string.IsNullOrEmpty(configuration["TZ"])) if (string.IsNullOrEmpty(configuration["TZ"]))
{
configuration["TZ"] = "Europe/Moscow"; configuration["TZ"] = "Europe/Moscow";
}
int permitLimit = int.TryParse(configuration["PERMIT_LIMIT"], out var parsedPermitLimit) ? parsedPermitLimit : 40; int permitLimit = int.TryParse(configuration["PERMIT_LIMIT"], out var parsedPermitLimit) ? parsedPermitLimit : 40;
int timeLimit = int.TryParse(configuration["TIME_LIMIT"], out var parsedTimeLimit) ? parsedTimeLimit : 10; int timeLimit = int.TryParse(configuration["TIME_LIMIT"], out var parsedTimeLimit) ? parsedTimeLimit : 10;
@@ -39,6 +37,7 @@ var pluginsPath = Path.Combine(dataDirectory, "Plugins");
builder.Logging.ClearProviders(); builder.Logging.ClearProviders();
builder.Logging.AddConsole(); builder.Logging.AddConsole();
builder.Logging.AddFilter("Quartz", LogLevel.Warning);
if (!string.IsNullOrEmpty(tgChatId) && !string.IsNullOrEmpty(tgToken)) if (!string.IsNullOrEmpty(tgChatId) && !string.IsNullOrEmpty(tgToken))
builder.Logging.AddTelegram(options => builder.Logging.AddTelegram(options =>
{ {
@@ -50,7 +49,8 @@ if (!string.IsNullOrEmpty(tgChatId) && !string.IsNullOrEmpty(tgToken))
{ {
{ "Default", LogLevel.Error }, { "Default", LogLevel.Error },
{ "SfeduSchedule.Jobs.UpdateJwtJob", LogLevel.Information }, { "SfeduSchedule.Jobs.UpdateJwtJob", LogLevel.Information },
{ "Program", LogLevel.Information } { "Program", LogLevel.Information },
{ "Quartz", LogLevel.Warning }
}; };
}); });
@@ -67,9 +67,9 @@ builder.Services.AddAuthorization();
builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration); builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration);
// Загружаем плагины (изолированно), даём им зарегистрировать DI и подключаем контроллеры // Загружаем плагины (изолированно), даём им зарегистрировать DI и подключаем контроллеры
var loaded = PluginLoader.LoadPlugins(pluginsPath); var loadedPlugins = PluginLoader.LoadPlugins(pluginsPath);
Console.WriteLine("Plugins count: " + loaded.Count); Console.WriteLine("Plugins count: " + loadedPlugins.Count);
foreach (var p in loaded) foreach (var p in loadedPlugins)
{ {
Console.WriteLine("Loading plugin: " + p.Instance.Name); Console.WriteLine("Loading plugin: " + p.Instance.Name);
@@ -108,6 +108,14 @@ builder.Services.AddSwaggerGen(options =>
var pluginXmlFile = "SfeduSchedule.Abstractions.xml"; var pluginXmlFile = "SfeduSchedule.Abstractions.xml";
var pluginXmlPath = Path.Combine(AppContext.BaseDirectory, pluginXmlFile); var pluginXmlPath = Path.Combine(AppContext.BaseDirectory, pluginXmlFile);
options.IncludeXmlComments(pluginXmlPath); options.IncludeXmlComments(pluginXmlPath);
// Добавление документации плагинов
foreach (var p in loadedPlugins)
{
var pluginXmlFullPath = p.Assembly.Location.Replace("dll", "xml");
if (File.Exists(pluginXmlFullPath))
options.IncludeXmlComments(pluginXmlFullPath);
}
// Добавляем только схему авторизации по ApiKey // Добавляем только схему авторизации по ApiKey
options.AddSecurityDefinition(ApiKeyAuthenticationDefaults.Scheme, new Microsoft.OpenApi.Models.OpenApiSecurityScheme options.AddSecurityDefinition(ApiKeyAuthenticationDefaults.Scheme, new Microsoft.OpenApi.Models.OpenApiSecurityScheme
@@ -225,9 +233,8 @@ app.MapGet("/", async context =>
app.MapControllers(); app.MapControllers();
// Маршруты Minimal API из плагинов // Маршруты Minimal API из плагинов
foreach (var p in loaded) foreach (var p in loadedPlugins)
{ {
logger.LogInformation("Mapping endpoints for plugin: {PluginName}", p.Instance.Name);
p.Instance.MapEndpoints(app); p.Instance.MapEndpoints(app);
} }

View File

@@ -27,7 +27,7 @@ namespace SfeduSchedule.Services
{ {
var request = new HttpRequestMessage(HttpMethod.Post, var request = new HttpRequestMessage(HttpMethod.Post,
$"schedule-calendar-v2/api/calendar/events/search?tz={_configuration["TZ"]!}"); $"schedule-calendar-v2/api/calendar/events/search?tz={_configuration["TZ"]!}");
request.Content = new StringContent(JsonSerializer.Serialize(msr, GlobalVariables.jsonSerializerOptions), request.Content = new StringContent(JsonSerializer.Serialize(msr, GlobalVariables.JsonSerializerOptions),
System.Text.Encoding.UTF8, "application/json"); System.Text.Encoding.UTF8, "application/json");
var response = await _httpClient.SendAsync(request); var response = await _httpClient.SendAsync(request);
_logger.LogInformation("GetScheduleAsync: Ответ получен: {StatusCode}", response.StatusCode); _logger.LogInformation("GetScheduleAsync: Ответ получен: {StatusCode}", response.StatusCode);
@@ -59,7 +59,7 @@ namespace SfeduSchedule.Services
{ {
var request = new HttpRequestMessage(HttpMethod.Post, $"schedule-calendar-v2/api/campus/rooms/search"); var request = new HttpRequestMessage(HttpMethod.Post, $"schedule-calendar-v2/api/campus/rooms/search");
request.Content = request.Content =
new StringContent(JsonSerializer.Serialize(requestDto, GlobalVariables.jsonSerializerOptions), new StringContent(JsonSerializer.Serialize(requestDto, GlobalVariables.JsonSerializerOptions),
System.Text.Encoding.UTF8, "application/json"); System.Text.Encoding.UTF8, "application/json");
var response = await _httpClient.SendAsync(request); var response = await _httpClient.SendAsync(request);
_logger.LogInformation("SearchRoomsAsync: Ответ получен: {StatusCode}", response.StatusCode); _logger.LogInformation("SearchRoomsAsync: Ответ получен: {StatusCode}", response.StatusCode);
@@ -122,22 +122,22 @@ namespace SfeduSchedule.Services
case null: case null:
_logger.LogError( _logger.LogError(
"GetScheduleJsonAsync: scheduleJson is null. Schedule: {Schedule}\n Request: {msr}", "GetScheduleJsonAsync: scheduleJson is null. Schedule: {Schedule}\n Request: {msr}",
schedule, JsonSerializer.Serialize(msr, GlobalVariables.jsonSerializerOptions)); schedule, JsonSerializer.Serialize(msr, GlobalVariables.JsonSerializerOptions));
break; break;
case { Embedded: null }: case { Embedded: null }:
_logger.LogError( _logger.LogError(
"GetScheduleJsonAsync: scheduleJson.Embedded is null. scheduleJson: {@scheduleJson}\n Request: {msr}", "GetScheduleJsonAsync: scheduleJson.Embedded is null. scheduleJson: {@scheduleJson}\n Request: {msr}",
scheduleJson, JsonSerializer.Serialize(msr, GlobalVariables.jsonSerializerOptions)); scheduleJson, JsonSerializer.Serialize(msr, GlobalVariables.JsonSerializerOptions));
break; break;
case { Embedded.Events: null }: case { Embedded.Events: null }:
_logger.LogError( _logger.LogError(
"GetScheduleJsonAsync: scheduleJson.Embedded.Events is null. Embedded: {@Embedded}\n Request: {msr}", "GetScheduleJsonAsync: scheduleJson.Embedded.Events is null. Embedded: {@Embedded}\n Request: {msr}",
scheduleJson.Embedded, JsonSerializer.Serialize(msr, GlobalVariables.jsonSerializerOptions)); scheduleJson.Embedded, JsonSerializer.Serialize(msr, GlobalVariables.JsonSerializerOptions));
break; break;
case { Embedded.Events.Length: 0 }: case { Embedded.Events.Length: 0 }:
_logger.LogWarning( _logger.LogWarning(
"GetScheduleJsonAsync: scheduleJson.Embedded.Events is empty. Embedded: {@Embedded}\n Request: {msr}", "GetScheduleJsonAsync: scheduleJson.Embedded.Events is empty. Embedded: {@Embedded}\n Request: {msr}",
scheduleJson.Embedded, JsonSerializer.Serialize(msr, GlobalVariables.jsonSerializerOptions)); scheduleJson.Embedded, JsonSerializer.Serialize(msr, GlobalVariables.JsonSerializerOptions));
break; break;
default: default:
return scheduleJson; return scheduleJson;
@@ -147,7 +147,7 @@ namespace SfeduSchedule.Services
{ {
_logger.LogError(ex, _logger.LogError(ex,
"GetScheduleJsonAsync: Deserialization failed. Schedule: {Schedule}\n Request: {msr}", schedule, "GetScheduleJsonAsync: Deserialization failed. Schedule: {Schedule}\n Request: {msr}", schedule,
JsonSerializer.Serialize(msr, GlobalVariables.jsonSerializerOptions)); JsonSerializer.Serialize(msr, GlobalVariables.JsonSerializerOptions));
} }
return null; return null;
@@ -158,7 +158,7 @@ namespace SfeduSchedule.Services
Schedule? scheduleJson = await GetScheduleJsonAsync(msr); Schedule? scheduleJson = await GetScheduleJsonAsync(msr);
if (scheduleJson == null) if (scheduleJson == null)
{ {
_logger.LogError("GetIcsAsync: scheduleJson is null after deserialization. Request: " + JsonSerializer.Serialize(msr, GlobalVariables.jsonSerializerOptions)); _logger.LogError("GetIcsAsync: scheduleJson is null after deserialization. Request: " + JsonSerializer.Serialize(msr, GlobalVariables.JsonSerializerOptions));
return null; return null;
} }
@@ -252,7 +252,7 @@ namespace SfeduSchedule.Services
shortNameCourse = courseUnitRealization.NameShort ?? ""; shortNameCourse = courseUnitRealization.NameShort ?? "";
} }
} }
catch (Exception ex) catch (Exception)
{ {
// Ignored // Ignored
} }