diff --git a/Govor.Console/FriendsClient.cs b/Govor.Console/FriendsClient.cs deleted file mode 100644 index 0af8398..0000000 --- a/Govor.Console/FriendsClient.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Net.Http.Json; -using Govor.Contracts.DTOs; -using System.Collections.Generic; -using System.Net.Http; -using System.Threading.Tasks; // Added for Task - -namespace Govor.ConsoleClient -{ - public class FriendsClient - { - private readonly HttpClient _client; - - public FriendsClient(HttpClient client) - { - _client = client; - } - - public async Task> SearchAsync(string query) - { - var response = await _client.GetAsync($"/api/friends/search?query={query}"); - response.EnsureSuccessStatusCode(); - - return await response.Content.ReadFromJsonAsync>() ?? new List(); - } - - // SendFriendRequestAsync was removed as the SendFriendRequestCommand now uses SignalR directly. - // public async Task SendFriendRequestAsync(Guid targetUserId) - // { - // var response = await _client.PostAsync($"api/friends/request?targetUserId={targetUserId}", null); - // response.EnsureSuccessStatusCode(); - // } - - public async Task> GetIncomingRequestsAsync() - { - var response = await _client.GetAsync("/api/friends/requests"); - response.EnsureSuccessStatusCode(); - - return await response.Content.ReadFromJsonAsync>() ?? new List(); - } - - // AcceptFriendRequestAsync was removed as the AcceptFriendRequestCommand now uses SignalR directly. - // public async Task AcceptFriendRequestAsync(Guid friendshipId) // Parameter was requesterId, should be friendshipId - // { - // var response = await _client.PostAsync($"/api/friends/accept?friendshipId={friendshipId}", null); - // response.EnsureSuccessStatusCode(); - // } - - public async Task> GetFriendsAsync() - { - var response = await _client.GetAsync("/api/friends"); - response.EnsureSuccessStatusCode(); - - return await response.Content.ReadFromJsonAsync>() ?? new List(); - } - } - -} \ No newline at end of file diff --git a/Govor.Console/FriendsHubClient.cs b/Govor.Console/FriendsHubClient.cs deleted file mode 100644 index fdc342b..0000000 --- a/Govor.Console/FriendsHubClient.cs +++ /dev/null @@ -1,70 +0,0 @@ -using Microsoft.AspNetCore.SignalR.Client; -using System; -using System.Threading.Tasks; -using Govor.Contracts.Responses.SignalR; - -namespace Govor.ConsoleClient; - - -public class FriendsHubClient -{ - private readonly string _hubUrl; - private readonly string _jwtToken; - private HubConnection _connection; - - public FriendsHubClient(string hubUrl, string jwtToken) - { - _hubUrl = hubUrl; - _jwtToken = jwtToken; - } - - public async Task ConnectAsync() - { - _connection = new HubConnectionBuilder() - .WithUrl($"{_hubUrl}/friends", options => - { - options.AccessTokenProvider = () => Task.FromResult(_jwtToken); - }) - .WithAutomaticReconnect() - .Build(); - - // Событие: получен входящий запрос в друзья - _connection.On("FriendRequestReceived", userId => - { - Console.WriteLine($"📨 Friend request received from: {userId}"); - }); - - // Событие: запрос принят - _connection.On("FriendRequestAccepted", friendshipId => - { - Console.WriteLine($"✅ Friend request accepted! Friendship ID: {friendshipId}"); - }); - - // Событие: запрос отклонён - _connection.On("FriendRequestRejected", friendshipId => - { - Console.WriteLine($"❌ Friend request rejected! Friendship ID: {friendshipId}"); - }); - - await _connection.StartAsync(); - Console.WriteLine("✅ Connected to FriendsHub"); - } - - public async Task SendFriendRequestAsync(Guid targetUserId) - { - var result = await _connection.InvokeAsync>("SendRequest", targetUserId); - Console.WriteLine($"📤 Friend request sent. Status: {result.Status}"); - } - - public async Task AcceptFriendRequestAsync(Guid friendshipId) - { - var result = await _connection.InvokeAsync>("AcceptRequest", friendshipId); - Console.WriteLine($"👍 Accept result: {result.Status}"); - } - - public async Task RejectFriendRequestAsync(Guid friendshipId) - { - var result = await _connection.InvokeAsync>("RejectRequest", friendshipId); - Console.WriteLine($"👎 Reject result: {result.Status}"); - } -} diff --git a/Govor.Console/HttpClientService.cs b/Govor.Console/HttpClientService.cs deleted file mode 100644 index 9482b92..0000000 --- a/Govor.Console/HttpClientService.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System.Net.Http; -using System.Net.Http.Json; -using System.Text.Json; -using System.Threading.Tasks; -using Govor.Contracts.Requests; - -namespace Govor.ConsoleClient -{ - public class HttpClientService - { - private readonly HttpClient _client; - - public HttpClientService(string baseUrl) - { - _client = new HttpClient - { - BaseAddress = new Uri(baseUrl) - }; - } - - public string GetBaseUrl() => _client.BaseAddress?.ToString() ?? string.Empty; - - public async Task GetAsync(string uri) - { - var response = await _client.GetAsync(uri); - response.EnsureSuccessStatusCode(); - return await response.Content.ReadAsStringAsync(); - } - - public async Task PostAsync(string uri, object? payload = null) - { - HttpResponseMessage response; - - if (payload is null) - response = await _client.PostAsync(uri, null); - else - response = await _client.PostAsJsonAsync(uri, payload); - - response.EnsureSuccessStatusCode(); - return await response.Content.ReadAsStringAsync(); - } - - public async Task RegisterAsync(string username, string password, string inviteCode) - { - var request = new RegistrationRequest - { - Name = username, - Password = password, - InviteLink = inviteCode - }; - - var response = await _client.PostAsJsonAsync("/api/auth/register", request); - if (response.IsSuccessStatusCode) - { - var result = await response.Content.ReadFromJsonAsync(); - return result?.Token; - } - - var error = await response.Content.ReadAsStringAsync(); - throw new Exception($"Registration failed: {error}"); - } - - public async Task LoginAsync(string username, string password) - { - var request = new LoginRequest - { - Name = username, - Password = password - }; - - var response = await _client.PostAsJsonAsync("/api/auth/login", request); - if (response.IsSuccessStatusCode) - { - var result = await response.Content.ReadFromJsonAsync(); - return result?.Token; - } - - var error = await response.Content.ReadAsStringAsync(); - throw new Exception($"Login failed: {error}"); - } - } - - public class TokenResponse - { - public string Token { get; set; } - } -} diff --git a/Govor.Console/Program.cs b/Govor.Console/Program.cs deleted file mode 100644 index 9ac0bbf..0000000 --- a/Govor.Console/Program.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Govor.ConsoleClient; - -internal class Program -{ - private const string HubBaseUrl = "https://govor-team-govor-88b3.twc1.net/hubs"; - private static string jwtToken = "eyJhbGciOiJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNobWFjLXNoYTI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI4NDRjNzkyMi1hNjdlLTRlMzctYWI0MC0wNThlZDE5NzM5NjMiLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJBZG1pbiIsImV4cCI6MTc1MjQ4MzE0MH0.ECXLPwIljdeWgWUJtBiXXQXKMFC8Iw0x7_zdjbsUe1I"; // сюда подставьте валидный JWT - - static async Task Main() - { - Console.Write("🔑 JWT Token: "); - jwtToken = Console.ReadLine(); - - var client = new FriendsHubClient(HubBaseUrl, jwtToken); - await client.ConnectAsync(); - - while (true) - { - Console.WriteLine("\n1. Send friend request"); - Console.WriteLine("2. Accept request"); - Console.WriteLine("3. Reject request"); - Console.Write("➡ Choose option: "); - var option = Console.ReadLine(); - - switch (option) - { - case "1": - Console.Write("Target UserId: "); - if (Guid.TryParse(Console.ReadLine(), out var targetId)) - { - await client.SendFriendRequestAsync(targetId); - } - break; - case "2": - Console.Write("FriendshipId to accept: "); - if (Guid.TryParse(Console.ReadLine(), out var acceptId)) - { - await client.AcceptFriendRequestAsync(acceptId); - } - break; - case "3": - Console.Write("FriendshipId to reject: "); - if (Guid.TryParse(Console.ReadLine(), out var rejectId)) - { - await client.RejectFriendRequestAsync(rejectId); - } - break; - default: - Console.WriteLine("❗ Unknown option"); - break; - } - } - } -} \ No newline at end of file diff --git a/Govor.ConsoleClient/App.cs b/Govor.ConsoleClient/App.cs new file mode 100644 index 0000000..bbdfa7a --- /dev/null +++ b/Govor.ConsoleClient/App.cs @@ -0,0 +1,27 @@ +using Govor.ConsoleClient.Services; + +namespace Govor.ConsoleClient; + +public class App +{ + private readonly InputPipeline _inputPipeline; + private readonly ILogger _logger; + + public App(InputPipeline inputPipeline, ILogger logger) + { + _logger = logger; + _inputPipeline = inputPipeline; + } + + public async Task RunAsync() + { + _logger.Title("Добро пожаловать в консольный клиент Говор!"); + while (true) + { + Console.Write(">> "); + var input = Console.ReadLine(); + if (input == null || input.Trim().ToLower() == "exit") break; + await _inputPipeline.ProcessInputAsync(input); + } + } +} diff --git a/Govor.ConsoleClient/Commands/HelpCommand.cs b/Govor.ConsoleClient/Commands/HelpCommand.cs new file mode 100644 index 0000000..b50c796 --- /dev/null +++ b/Govor.ConsoleClient/Commands/HelpCommand.cs @@ -0,0 +1,22 @@ +using Govor.ConsoleClient.Services; + +namespace Govor.ConsoleClient.Commands; + +public class HelpCommand : ICommand +{ + public Task ExecuteAsync(CommandContext context) + { + Console.WriteLine("Commands:"); + return Task.CompletedTask; + } + + public string LongHelp() + { + throw new NotImplementedException(); + } + + public string ShortHelp() + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Govor.ConsoleClient/Commands/ICommand.cs b/Govor.ConsoleClient/Commands/ICommand.cs new file mode 100644 index 0000000..aa1c844 --- /dev/null +++ b/Govor.ConsoleClient/Commands/ICommand.cs @@ -0,0 +1,17 @@ +using Govor.ConsoleClient.Services; + +namespace Govor.ConsoleClient.Commands; + +public interface ICommand +{ + Task ExecuteAsync(CommandContext context); + string LongHelp(); + string ShortHelp(); +} + +[AttributeUsage(AttributeTargets.Class)] +public class CommandRouteAttribute : Attribute +{ + public string Path { get; } + public CommandRouteAttribute(string path) => Path = path; +} \ No newline at end of file diff --git a/Govor.ConsoleClient/Commands/IInteractiveCommand.cs b/Govor.ConsoleClient/Commands/IInteractiveCommand.cs new file mode 100644 index 0000000..c13154e --- /dev/null +++ b/Govor.ConsoleClient/Commands/IInteractiveCommand.cs @@ -0,0 +1,7 @@ +namespace Govor.ConsoleClient.Commands; + +public interface IInteractiveCommand : ICommand +{ + Task HandleInputAsync(string input); + bool IsCompleted { get; } +} \ No newline at end of file diff --git a/Govor.ConsoleClient/Commands/SendMessageCommand.cs b/Govor.ConsoleClient/Commands/SendMessageCommand.cs new file mode 100644 index 0000000..544f264 --- /dev/null +++ b/Govor.ConsoleClient/Commands/SendMessageCommand.cs @@ -0,0 +1,44 @@ +using Govor.ConsoleClient.Services; + +namespace Govor.ConsoleClient.Commands; + +[CommandRoute("/send")] +public class SendMessageCommand : IInteractiveCommand +{ + private string? _recipient; + private bool _isCompleted; + public bool IsCompleted => _isCompleted; + + public Task ExecuteAsync(CommandContext context) + { + Console.WriteLine("Кому вы хотите отправить сообщение?"); + return Task.CompletedTask; + } + + public async Task HandleInputAsync(string input) + { + if (_recipient == null) + { + _recipient = input; + Console.WriteLine("Введите сообщение:"); + } + else + { + var message = input; + Console.WriteLine($"(Отправка '{message}' пользователю '{_recipient}')"); + _isCompleted = true; + } + + await Task.CompletedTask; + } + + public string LongHelp() + { + throw new NotImplementedException(); + } + + public string ShortHelp() + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Govor.ConsoleClient/DependencyInjection.cs b/Govor.ConsoleClient/DependencyInjection.cs new file mode 100644 index 0000000..ff33799 --- /dev/null +++ b/Govor.ConsoleClient/DependencyInjection.cs @@ -0,0 +1,47 @@ +using System.Reflection; +using Govor.ConsoleClient.Commands; +using Govor.ConsoleClient.Services; +using Govor.ConsoleClient.Services.Middleware; +using Microsoft.Extensions.DependencyInjection; + +namespace Govor.ConsoleClient; + +public static class DependencyInjection +{ + public static IServiceProvider Configure() + { + var services = new ServiceCollection(); + + // Регистрация команд + services.AddCommands(); + + // Сервисы + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(sp => sp.GetRequiredService()); + services.AddSingleton(); + services.AddSingleton(); + + // Middleware + services.AddSingleton(); + + + return services.BuildServiceProvider(); + } + + public static IServiceCollection AddCommands(this IServiceCollection services) + { + var commandTypes = Assembly.GetExecutingAssembly() + .GetTypes() + .Where(t => typeof(ICommand).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract); + + foreach (var type in commandTypes) + { + services.AddTransient(typeof(ICommand), type); + services.AddTransient(type); // Для прямого внедрения + } + + return services; + } +} \ No newline at end of file diff --git a/Govor.Console/Govor.Console.csproj b/Govor.ConsoleClient/Govor.ConsoleClient.csproj similarity index 81% rename from Govor.Console/Govor.Console.csproj rename to Govor.ConsoleClient/Govor.ConsoleClient.csproj index 0f9919f..e3de400 100644 --- a/Govor.Console/Govor.Console.csproj +++ b/Govor.ConsoleClient/Govor.ConsoleClient.csproj @@ -19,5 +19,12 @@ + + + + + + + diff --git a/Govor.ConsoleClient/Program.cs b/Govor.ConsoleClient/Program.cs new file mode 100644 index 0000000..491b499 --- /dev/null +++ b/Govor.ConsoleClient/Program.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Govor.ConsoleClient; + +internal class Program +{ + //private const string HubBaseUrl = "https://govor-team-govor-88b3.twc1.net/hubs"; + + static async Task Main() + { + Console.Title = "Govor Console Client"; + + var serviceProvider = DependencyInjection.Configure(); + + var app = ActivatorUtilities.CreateInstance(serviceProvider); + await app.RunAsync(); + } +} \ No newline at end of file diff --git a/Govor.ConsoleClient/Services/CommandContext.cs b/Govor.ConsoleClient/Services/CommandContext.cs new file mode 100644 index 0000000..d3af695 --- /dev/null +++ b/Govor.ConsoleClient/Services/CommandContext.cs @@ -0,0 +1,17 @@ +using Govor.ConsoleClient.Commands; + +namespace Govor.ConsoleClient.Services; + +public class CommandContext +{ + public string Route { get; } + public string? Arguments { get; } + public ICommand Command { get; } + + public CommandContext(string route, string? arguments, ICommand command) + { + Route = route; + Arguments = arguments; + Command = command; + } +} \ No newline at end of file diff --git a/Govor.ConsoleClient/Services/CommandDispatcher.cs b/Govor.ConsoleClient/Services/CommandDispatcher.cs new file mode 100644 index 0000000..ae0a299 --- /dev/null +++ b/Govor.ConsoleClient/Services/CommandDispatcher.cs @@ -0,0 +1,41 @@ +using System.Reflection; +using Govor.ConsoleClient.Commands; + +namespace Govor.ConsoleClient.Services; + +public class CommandDispatcher +{ + private readonly Dictionary _commands = new(); + private readonly ILogger _logger; + private readonly MiddlewarePipeline _pipeline; + + public CommandDispatcher(IEnumerable commands, ILogger logger, MiddlewarePipeline pipeline) + { + _logger = logger; + _pipeline = pipeline; + + foreach (var command in commands) + { + var route = command.GetType().GetCustomAttribute()?.Path.Replace("/","") + ?? command.GetType().Name.Replace("Command", "").ToLower(); + _commands[route.ToLower()] = command; + } + } + + public async Task DispatchAsync(string input) + { + var args = input.Split(' ', 2); + var cmd = args[0].ToLower(); + if (_commands.TryGetValue(cmd, out var command)) + { + var context = new CommandContext(cmd, args.Length > 1 ? args[1] : null, command); + await _pipeline.ExecuteAsync(context); + return command; + } + else + { + _logger.Warn("Неизвестная команда. Введите '/help'."); + return null; + } + } +} \ No newline at end of file diff --git a/Govor.ConsoleClient/Services/ConsoleLogger.cs b/Govor.ConsoleClient/Services/ConsoleLogger.cs new file mode 100644 index 0000000..7e2b0b8 --- /dev/null +++ b/Govor.ConsoleClient/Services/ConsoleLogger.cs @@ -0,0 +1,45 @@ +namespace Govor.ConsoleClient.Services; + +public class ConsoleLogger : ILogger +{ + public void Log(string message) + { + Console.ResetColor(); + Console.WriteLine(message); + } + + public void Info(string message) + { + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($"[INFO] {message}"); + Console.ResetColor(); + } + + public void Warn(string message) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"[WARN] {message}"); + Console.ResetColor(); + } + + public void Error(string message) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"[ERROR] {message}"); + Console.ResetColor(); + } + + public void Title(string message) + { + var upper = message.ToUpper(); + var length = upper.Length + 6; + var border = new string('=', length); + var padded = $"= {upper} ="; + + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine(border); + Console.WriteLine(padded); + Console.WriteLine(border); + Console.ResetColor(); + } +} \ No newline at end of file diff --git a/Govor.ConsoleClient/Services/ILogger.cs b/Govor.ConsoleClient/Services/ILogger.cs new file mode 100644 index 0000000..9e27a75 --- /dev/null +++ b/Govor.ConsoleClient/Services/ILogger.cs @@ -0,0 +1,10 @@ +namespace Govor.ConsoleClient.Services; + +public interface ILogger +{ + void Log(string message); + void Info(string message); + void Warn(string message); + void Error(string message); + void Title(string title); +} \ No newline at end of file diff --git a/Govor.ConsoleClient/Services/InputPipeline.cs b/Govor.ConsoleClient/Services/InputPipeline.cs new file mode 100644 index 0000000..ed93d15 --- /dev/null +++ b/Govor.ConsoleClient/Services/InputPipeline.cs @@ -0,0 +1,49 @@ +using Govor.ConsoleClient.Commands; + +namespace Govor.ConsoleClient.Services; + +public class InputPipeline +{ + private readonly CommandDispatcher _dispatcher; + private readonly ILogger _logger; + private IInteractiveCommand? _activeCommand; + + public InputPipeline(CommandDispatcher dispatcher, ILogger logger) + { + _dispatcher = dispatcher; + _logger = logger; + } + + public async Task ProcessInputAsync(string input) + { + if (string.IsNullOrWhiteSpace(input)) return; + + if (input.StartsWith("/")) + { + _activeCommand = null; // Сброс активной команды + + var commandInput = input[1..]; + var result = await _dispatcher.DispatchAsync(commandInput); + + // Если команда поддерживает интерактивность, сохраняем как активную + if (result is IInteractiveCommand interactiveCommand && !interactiveCommand.IsCompleted) + { + _activeCommand = interactiveCommand; + } + } + else + { + if (_activeCommand != null) + { + await _activeCommand.HandleInputAsync(input); + + if (_activeCommand.IsCompleted) + _activeCommand = null; + } + else + { + _logger.Info($"[Ввод пользователя]: {input}"); + } + } + } +} \ No newline at end of file diff --git a/Govor.ConsoleClient/Services/Middleware/ExceptionHandlingMiddleware.cs b/Govor.ConsoleClient/Services/Middleware/ExceptionHandlingMiddleware.cs new file mode 100644 index 0000000..7cd6fee --- /dev/null +++ b/Govor.ConsoleClient/Services/Middleware/ExceptionHandlingMiddleware.cs @@ -0,0 +1,23 @@ +namespace Govor.ConsoleClient.Services.Middleware; + +public class ExceptionHandlingMiddleware : ICommandMiddleware +{ + private readonly ILogger _logger; + + public ExceptionHandlingMiddleware(ILogger logger) + { + _logger = logger; + } + + public async Task InvokeAsync(CommandContext context, Func next) + { + try + { + await next(); + } + catch (Exception ex) + { + _logger.Error($"Произошла ошибка при выполнении команды '{context.Route}': {ex.Message}"); + } + } +} \ No newline at end of file diff --git a/Govor.ConsoleClient/Services/Middleware/ICommandMiddleware.cs b/Govor.ConsoleClient/Services/Middleware/ICommandMiddleware.cs new file mode 100644 index 0000000..e441906 --- /dev/null +++ b/Govor.ConsoleClient/Services/Middleware/ICommandMiddleware.cs @@ -0,0 +1,6 @@ +namespace Govor.ConsoleClient.Services.Middleware; + +public interface ICommandMiddleware +{ + Task InvokeAsync(CommandContext context, Func next); +} \ No newline at end of file diff --git a/Govor.ConsoleClient/Services/MiddlewarePipeline.cs b/Govor.ConsoleClient/Services/MiddlewarePipeline.cs new file mode 100644 index 0000000..e19dd23 --- /dev/null +++ b/Govor.ConsoleClient/Services/MiddlewarePipeline.cs @@ -0,0 +1,29 @@ +using Govor.ConsoleClient.Services.Middleware; + +namespace Govor.ConsoleClient.Services; + +public delegate Task CommandMiddleware(CommandContext context, Func next); + +public class MiddlewarePipeline +{ + private readonly IList _middlewares; + + public MiddlewarePipeline(IEnumerable middlewares) + { + _middlewares = middlewares.ToList(); + } + + public Task ExecuteAsync(CommandContext context) + { + return InvokeNext(0, context); + } + + private Task InvokeNext(int index, CommandContext context) + { + if (index < _middlewares.Count) + { + return _middlewares[index].InvokeAsync(context, () => InvokeNext(index + 1, context)); + } + return context.Command.ExecuteAsync(context); + } +} \ No newline at end of file diff --git a/Govor.sln b/Govor.sln index 0aec0ab..99567f9 100644 --- a/Govor.sln +++ b/Govor.sln @@ -8,7 +8,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Govor.Core", "Govor.Core\Go EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Govor.Data", "Govor.Data\Govor.Data.csproj", "{E4EDB179-7EB5-468D-9C1F-0CBE2E5E459E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Govor.Console", "Govor.Console\Govor.Console.csproj", "{F4535DC3-BDFB-4EB2-B259-F92B6BBB535B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Govor.ConsoleClient", "Govor.ConsoleClient\Govor.ConsoleClient.csproj", "{F4535DC3-BDFB-4EB2-B259-F92B6BBB535B}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{4ED5259A-6FB4-4D89-8E6B-4778DC68F7D4}" EndProject