547 lines
20 KiB
C#
547 lines
20 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Asp.Versioning;
|
|
using AutoMapper;
|
|
using gehGassi.Core.Interfaces;
|
|
using gehGassi.Core.Services;
|
|
using gehGassi.Domain.Common;
|
|
using gehGassi.Domain.Dogs;
|
|
using gehGassi.Domain.Messages;
|
|
using gehGassi.Domain.Pushnotifications;
|
|
using gehGassi.Dto;
|
|
using gehGassi.Dto.Common;
|
|
using gehGassi.Dto.Favourites;
|
|
using gehGassi.Dto.Messages;
|
|
using gehGassi.Web.Auth;
|
|
using gehGassi.Web.Hubs;
|
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.Extensions.Localization;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using LocalizationOptions = gehGassi.Web.Helper.LocalizationOptions;
|
|
|
|
namespace gehGassi.Web.Controllers.Api
|
|
{
|
|
/// <summary>
|
|
/// Controller der Zugriff auf Nachrichten via API ermöglicht
|
|
/// </summary>
|
|
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
|
|
[ApiController]
|
|
[ApiVersion(1)]
|
|
[Route("api/messages")]
|
|
[Route("api/v{v:apiVersion}/messages")]
|
|
public class ApiMessagesController : ApiBaseController
|
|
{
|
|
private readonly ILogger<ApiMessagesController> _logger;
|
|
private readonly IMessageService _messageService;
|
|
private readonly IAppHubSender _appHubSender;
|
|
private readonly ISystemMessageService _systemMessageService;
|
|
private readonly IPushNotificationService _pushNotificationService;
|
|
private readonly IDeviceService _deviceService;
|
|
private readonly IStringLocalizer<ApiMessagesController> _localizer;
|
|
private readonly IUserOnlineService _userOnlineService;
|
|
|
|
/// <summary>
|
|
/// Erstellt eine Instanz
|
|
/// </summary>
|
|
/// <param name="mapper">Instanz eines IMapper</param>
|
|
/// <param name="logger">Instanz eines ILogger</param>
|
|
/// <param name="localizationOptions">Instanz von LocalizationOptions</param>
|
|
/// <param name="appUserService">Instanz eines IDogOwnerService</param>
|
|
/// <param name="messageService">Instanz eines IMessageService</param>
|
|
/// <param name="appHubSender">Instanz eines IAppHubSender</param>
|
|
/// <param name="systemMessageService"></param>
|
|
/// <param name="pushNotificationService">Instanz eines IPushNotificationService</param>
|
|
/// <param name="deviceService">Instanz eines IDeviceService</param>
|
|
/// <param name="localizer">Instanz eines IStringLocalizer</param>
|
|
/// <param name="userOnlineService">Instanz eines IUserOnlineService</param>
|
|
public ApiMessagesController(IMapper mapper, ILogger<ApiMessagesController> logger, IOptions<LocalizationOptions> localizationOptions, IAppUserService appUserService,
|
|
IMessageService messageService, IAppHubSender appHubSender, ISystemMessageService systemMessageService, IPushNotificationService pushNotificationService,
|
|
IDeviceService deviceService, IStringLocalizer<ApiMessagesController> localizer, IUserOnlineService userOnlineService) : base(mapper, localizationOptions, appUserService)
|
|
{
|
|
_logger = logger;
|
|
_messageService = messageService;
|
|
_appHubSender = appHubSender;
|
|
_systemMessageService = systemMessageService;
|
|
_pushNotificationService = pushNotificationService;
|
|
_deviceService = deviceService;
|
|
_localizer = localizer;
|
|
_userOnlineService = userOnlineService;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Anlegen einer Konversation
|
|
/// </summary>
|
|
/// <param name="model">CreateConversationDto</param>
|
|
/// <returns>HTTP 200 OK, Felher sonst</returns>
|
|
[HttpPost]
|
|
[Route("CreateConversation")]
|
|
public async Task<IActionResult> CreateConversation(CreateConversationDto model)
|
|
{
|
|
var clientOffset = GetClientDateOffset();
|
|
|
|
var result = new CreateResponseDto<ConversationDto>
|
|
{
|
|
Status = CreateStatusDto.Error,
|
|
Value = null
|
|
};
|
|
if (User?.Identity != null && User.Identity.IsAuthenticated)
|
|
{
|
|
if (ModelState.IsValid)
|
|
{
|
|
if (model.SenderId != model.ReceiverId)
|
|
{
|
|
//Wenn Sender und Emfpägner nicht gleich sind!
|
|
|
|
var otherId = string.Empty;
|
|
|
|
//Prüfen ob es die Konversation schon gibt...
|
|
var conversation = await _messageService.GetConversationAsync(model.SenderId, model.ReceiverId);
|
|
if (conversation == null)
|
|
{
|
|
conversation = await _messageService.AddConversationAsync(model.Id, model.SenderId, model.ReceiverId, User.Identity.Name);
|
|
result.Status = CreateStatusDto.Success;
|
|
result.Value = Mapper.Map<ConversationDto>(conversation);
|
|
|
|
otherId = model.ReceiverId;
|
|
}
|
|
else
|
|
{
|
|
result.Status = CreateStatusDto.Exists;
|
|
result.Value = Mapper.Map<ConversationDto>(conversation);
|
|
|
|
if (model.SenderId == conversation.SenderId)
|
|
{
|
|
otherId = model.ReceiverId;
|
|
}
|
|
else
|
|
{
|
|
otherId = model.SenderId;
|
|
}
|
|
}
|
|
|
|
var baseAddress = GetBaseAddress();
|
|
var appUser = await AppUserService.GetAsync(otherId);
|
|
result.Value.ReceipientType = (AppUserTypeDto)appUser.Type;
|
|
result.Value.Recipient = appUser.Id;
|
|
result.Value.RecipientName = $"{appUser.FirstName} {appUser.LastName}";
|
|
result.Value.RecipientShort = GetInitials(appUser.FirstName, appUser.LastName);
|
|
result.Value.RecipientPhoto = !string.IsNullOrWhiteSpace(appUser.Photo) ? $"{baseAddress}/file/documents/thumbnails/{100}/{appUser.Photo}" : "";
|
|
|
|
return Ok(result);
|
|
}
|
|
}
|
|
}
|
|
|
|
return BadRequest(CommunicationErrors.Common_Model_Invalid);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Abfragen der Konversationen eines Users
|
|
/// </summary>
|
|
/// <param name="senderId">Id des Senders</param>
|
|
/// <param name="lastUpdate">Letztes Update</param>
|
|
/// <returns>Liste der Konversationen</returns>
|
|
[HttpGet]
|
|
[Route("GetConversations")]
|
|
public async Task<IActionResult> GetConversations(string senderId, DateTimeOffset? lastUpdate)
|
|
{
|
|
var clientOffset = GetClientDateOffset();
|
|
|
|
if (User?.Identity != null && User.Identity.IsAuthenticated)
|
|
{
|
|
var conversations = await _messageService.GetConversationsAsync(senderId, lastUpdate);
|
|
var conversationDtoList = Mapper.Map<List<ConversationDto>>(conversations);
|
|
var baseAddress = GetBaseAddress();
|
|
foreach (var item in conversationDtoList)
|
|
{
|
|
var otherId = string.Empty;
|
|
|
|
var conversation = conversations.First(c => c.Id == item.Id);
|
|
if (conversation.SenderId == senderId)
|
|
{
|
|
otherId = conversation.ReceiverId;
|
|
}
|
|
else
|
|
{
|
|
otherId = conversation.SenderId;
|
|
}
|
|
|
|
var appUser = await AppUserService.GetAsync(otherId);
|
|
item.ReceipientType = (AppUserTypeDto)appUser.Type;
|
|
item.Recipient = appUser.Id;
|
|
item.RecipientName = $"{appUser.FirstName} {appUser.LastName}";
|
|
item.RecipientShort = GetInitials(appUser.FirstName, appUser.LastName);
|
|
item.RecipientPhoto = !string.IsNullOrWhiteSpace(appUser.Photo) ? $"{baseAddress}/file/documents/thumbnails/{100}/{appUser.Photo}" : "";
|
|
}
|
|
|
|
return Ok(conversationDtoList);
|
|
}
|
|
|
|
return NotFound(CommunicationErrors.Common_NotFound);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Abfragen der Konversationen eines Users
|
|
/// </summary>
|
|
/// <param name="senderId">Id des Senders</param>
|
|
/// <param name="filter">Filterwert der in Namen gesucht wird</param>
|
|
/// <returns>Liste der Konversationen</returns>
|
|
[HttpGet]
|
|
[Route("GetConversationsEx")]
|
|
public async Task<IActionResult> GetConversationsEx(string senderId, string filter)
|
|
{
|
|
var clientOffset = GetClientDateOffset();
|
|
filter ??= string.Empty;
|
|
|
|
if (User?.Identity != null && User.Identity.IsAuthenticated)
|
|
{
|
|
var conversations = await _messageService.GetConversationsWithNamesAsync(senderId);
|
|
var conversationList = new List<ConversationWithNames>();
|
|
|
|
foreach (var conversation in conversations)
|
|
{
|
|
if (conversation.SenderId == senderId)
|
|
{
|
|
if (!conversation.ReceiverBlocked && !conversation.ReceiverLocked && conversation.ReceiverName.Contains(filter))
|
|
conversationList.Add(conversation);
|
|
}
|
|
else
|
|
{
|
|
if (!conversation.SenderBlocked && !conversation.SenderLocked && conversation.SenderName.Contains(filter))
|
|
conversationList.Add(conversation);
|
|
}
|
|
}
|
|
|
|
|
|
var conversationDtoList = Mapper.Map<List<ConversationDto>>(conversationList);
|
|
var baseAddress = GetBaseAddress();
|
|
foreach (var item in conversationDtoList)
|
|
{
|
|
var conversation = conversationList.First(c => c.Id == item.Id);
|
|
if (conversation.SenderId == senderId)
|
|
{
|
|
item.ReceipientType = (AppUserTypeDto)conversation.ReceiverType;
|
|
item.Recipient = conversation.ReceiverId;
|
|
item.RecipientName = conversation.ReceiverName;
|
|
item.RecipientShort = GetInitials(conversation.ReceiverFirstName, conversation.ReceiverLastName);
|
|
item.RecipientPhoto = !string.IsNullOrWhiteSpace(conversation.ReceiverPhoto) ? $"{baseAddress}/file/documents/thumbnails/{100}/{conversation.ReceiverPhoto}" : "";
|
|
}
|
|
else
|
|
{
|
|
item.ReceipientType = (AppUserTypeDto)conversation.SenderType;
|
|
item.Recipient = conversation.SenderId;
|
|
item.RecipientName = conversation.SenderName;
|
|
item.RecipientShort = GetInitials(conversation.SenderFirstName, conversation.SenderLastName);
|
|
item.RecipientPhoto = !string.IsNullOrWhiteSpace(conversation.SenderPhoto) ? $"{baseAddress}/file/documents/thumbnails/{100}/{conversation.SenderPhoto}" : "";
|
|
}
|
|
}
|
|
|
|
return Ok(conversationDtoList);
|
|
}
|
|
|
|
return NotFound(CommunicationErrors.Common_NotFound);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Abfragen der Konversationen eines Benutzers mit einem Emüfänger
|
|
/// </summary>
|
|
/// <param name="senderId">Id des Senders</param>
|
|
/// <param name="receipientId">Id des Empfängers nach dem gesucht wird</param>
|
|
/// <returns>Konversationen oder NotFound wenn nicht gefunden</returns>
|
|
[HttpGet]
|
|
[Route("GetConversationByReceipient")]
|
|
public async Task<IActionResult> GetConversationByReceipient(string senderId, string receipientId)
|
|
{
|
|
var clientOffset = GetClientDateOffset();
|
|
|
|
if (User?.Identity != null && User.Identity.IsAuthenticated)
|
|
{
|
|
var conversation = await _messageService.GetConversationAsync(senderId, receipientId);
|
|
if (conversation != null)
|
|
{
|
|
var baseAddress = GetBaseAddress();
|
|
var otherId = string.Empty;
|
|
|
|
if (conversation.SenderId == senderId)
|
|
{
|
|
otherId = conversation.ReceiverId;
|
|
}
|
|
else
|
|
{
|
|
otherId = conversation.SenderId;
|
|
}
|
|
|
|
var item = Mapper.Map<ConversationDto>(conversation);
|
|
|
|
var appUser = await AppUserService.GetAsync(otherId);
|
|
item.ReceipientType = (AppUserTypeDto)appUser.Type;
|
|
item.Recipient = appUser.Id;
|
|
item.RecipientName = $"{appUser.FirstName} {appUser.LastName}";
|
|
item.RecipientShort = GetInitials(appUser.FirstName, appUser.LastName);
|
|
item.RecipientPhoto = !string.IsNullOrWhiteSpace(appUser.Photo) ? $"{baseAddress}/file/documents/thumbnails/{100}/{appUser.Photo}" : "";
|
|
|
|
return Ok(item);
|
|
}
|
|
}
|
|
|
|
return NotFound(CommunicationErrors.Common_NotFound);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Abfragen der bisher noch nicht abgerufenen Nachrichten für einen Benutzer
|
|
/// </summary>
|
|
/// <param name="senderId">Id des Senders - gilt dann als Empfänger</param>
|
|
/// <param name="lastUpdate">Letztes Update</param>
|
|
/// <returns>Liste Nachrichten</returns>
|
|
[HttpGet]
|
|
[Route("GetMessages")]
|
|
public async Task<IActionResult> GetMessages(string senderId, DateTimeOffset? lastUpdate)
|
|
{
|
|
var clientOffset = GetClientDateOffset();
|
|
|
|
if (User?.Identity != null && User.Identity.IsAuthenticated)
|
|
{
|
|
var messages = await _messageService.GetMessagesAsync(senderId, lastUpdate);
|
|
var messagesDtoList = Mapper.Map<List<MessageDto>>(messages);
|
|
return Ok(messagesDtoList);
|
|
}
|
|
|
|
return NotFound(CommunicationErrors.Common_NotFound);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Abfragen der bisher noch nicht abgerufenen Nachrichten für einen Benutzer in einer Konversation
|
|
/// </summary>
|
|
/// <param name="senderId">Id des Senders - gilt dann als Empfänger</param>
|
|
/// <param name="conversationId">Id der Konversation</param>
|
|
/// <param name="lastUpdate">Letztes Update</param>
|
|
/// <returns>Liste Nachrichten</returns>
|
|
[HttpGet]
|
|
[Route("GetMessagesByConversation")]
|
|
public async Task<IActionResult> GetMessagesByConversation(string senderId, string conversationId, DateTimeOffset? lastUpdate)
|
|
{
|
|
var clientOffset = GetClientDateOffset();
|
|
|
|
if (User?.Identity != null && User.Identity.IsAuthenticated)
|
|
{
|
|
var messages = await _messageService.GetMessagesAsync(senderId, conversationId, lastUpdate);
|
|
var messagesDtoList = Mapper.Map<List<MessageDto>>(messages);
|
|
return Ok(messagesDtoList);
|
|
}
|
|
|
|
return NotFound(CommunicationErrors.Common_NotFound);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bestätigen des erfolgreichen Erhalts von Nachrichten
|
|
/// </summary>
|
|
/// <param name="model">MessageConfirmationDto</param>
|
|
/// <returns>HTTP 200 OK, Felher sonst</returns>
|
|
[HttpPost]
|
|
[Route("ConfirmMessages")]
|
|
public async Task<IActionResult> ConfirmMessages(MessageConfirmationDto model)
|
|
{
|
|
var clientOffset = GetClientDateOffset();
|
|
|
|
if (User?.Identity != null && User.Identity.IsAuthenticated)
|
|
{
|
|
if (ModelState.IsValid)
|
|
{
|
|
await _messageService.ConfirmMessagesAsync(model.SenderId, model.LastUpdate.Value, User.Identity.Name);
|
|
return Ok();
|
|
}
|
|
}
|
|
|
|
return BadRequest(CommunicationErrors.Common_Model_Invalid);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bestätigen des erfolgreichen Erhalts von Nachrichten für eine Konversation
|
|
/// </summary>
|
|
/// <param name="model">MessageConfirmationDto</param>
|
|
/// <returns>HTTP 200 OK, Felher sonst</returns>
|
|
[HttpPost]
|
|
[Route("ConfirmMessagesByConversation")]
|
|
public async Task<IActionResult> ConfirmMessagesByConversation(MessageConfirmationDto model)
|
|
{
|
|
var clientOffset = GetClientDateOffset();
|
|
|
|
if (User?.Identity != null && User.Identity.IsAuthenticated)
|
|
{
|
|
if (ModelState.IsValid)
|
|
{
|
|
await _messageService.ConfirmMessagesAsync(model.SenderId, model.ConversationId, model.LastUpdate.Value, User.Identity.Name);
|
|
return Ok();
|
|
}
|
|
}
|
|
|
|
return BadRequest(CommunicationErrors.Common_Model_Invalid);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Hinzufügen einer Nachricht zu einer Konversation
|
|
/// </summary>
|
|
/// <param name="model">MessageDto</param>
|
|
/// <returns>HTTP 200 OK, Felher sonst</returns>
|
|
[HttpPost]
|
|
[Route("AddMessage")]
|
|
public async Task<IActionResult> AddMessage(MessageDto model)
|
|
{
|
|
var clientOffset = GetClientDateOffset();
|
|
|
|
var result = new CreateResponseDto<MessageDto>
|
|
{
|
|
Status = CreateStatusDto.Error,
|
|
Value = null
|
|
};
|
|
|
|
if (User?.Identity != null && User.Identity.IsAuthenticated)
|
|
{
|
|
if (ModelState.IsValid)
|
|
{
|
|
var appUserId = User.AppUserId();
|
|
var isLocked = await AppUserService.IsLockedAsync(appUserId);
|
|
var isBlocked = await AppUserService.IsBlockedAsync(model.ReceiverId, model.SenderId);
|
|
|
|
var message = Mapper.Map<Message>(model);
|
|
|
|
var existingMessage = await _messageService.GetAsync(message.Id);
|
|
if (existingMessage == null && !isLocked && !isBlocked)
|
|
{
|
|
message = await _messageService.AddMessageAsync(message, User.Identity.Name);
|
|
var conversation = await _messageService.GetConversationAsync(message.SenderId, message.ReceiverId);
|
|
if (conversation != null)
|
|
{
|
|
conversation.MessageCount += 1;
|
|
conversation.LastMessage = DateTimeOffset.UtcNow;
|
|
await _messageService.CommitAsync(User.Identity.Name);
|
|
}
|
|
|
|
result.Status = CreateStatusDto.Success;
|
|
result.Value = model;
|
|
|
|
await _appHubSender.MessageAddedAsync(model.ReceiverId);
|
|
|
|
var isOnline = await _userOnlineService.IsAppUserOnlineAsync(model.ReceiverId);
|
|
|
|
if (!isOnline)
|
|
{
|
|
//Pushnotification nur senden, wenn es sich um eine offene Nachricht handelt
|
|
var openMessageCount = await _messageService.CountOpenMessagesAsync(model.ReceiverId, model.ConversationId);
|
|
if (openMessageCount == 1)
|
|
{
|
|
var success = await _pushNotificationService.SendNewMessageAsync(model.ReceiverId, model.ConversationId);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
return Ok(result);
|
|
}
|
|
}
|
|
|
|
return BadRequest(CommunicationErrors.Common_Model_Invalid);
|
|
}
|
|
|
|
#region Systemmessages
|
|
|
|
/// <summary>
|
|
/// Abfrage der Systemnachrichten eines AppUsers
|
|
/// </summary>
|
|
/// <param name="appUserId">Id des AppUsers</param>
|
|
/// <param name="lastUpdate">Letztes Update oder null, wenn noch keines</param>
|
|
/// <returns>Liste der Favoriten</returns>
|
|
[HttpGet]
|
|
[Route("GetSystemMessagesForSync")]
|
|
public async Task<IActionResult> GetSystemMessagesForSync(string appUserId, DateTimeOffset? lastUpdate)
|
|
{
|
|
var clientOffset = GetClientDateOffset();
|
|
|
|
if (User?.Identity != null && User.Identity.IsAuthenticated)
|
|
{
|
|
var systemMessages = await _systemMessageService.GetForSyncAppAsync(appUserId, lastUpdate);
|
|
var dtoList = Mapper.Map<List<SystemMessageDto>>(systemMessages);
|
|
return Ok(dtoList);
|
|
}
|
|
|
|
return NotFound(CommunicationErrors.Common_NotFound);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bestätigen des erfolgreichen Erhalts von Systemnachrichten
|
|
/// </summary>
|
|
/// <param name="model">SystemMessageConfirmationDto</param>
|
|
/// <returns>HTTP 200 OK, Felher sonst</returns>
|
|
[HttpPost]
|
|
[Route("ConfirmSystemMessages")]
|
|
public async Task<IActionResult> ConfirmSystemMessages(SystemMessageConfirmationDto model)
|
|
{
|
|
var clientOffset = GetClientDateOffset();
|
|
|
|
if (User?.Identity != null && User.Identity.IsAuthenticated)
|
|
{
|
|
if (ModelState.IsValid)
|
|
{
|
|
await _systemMessageService.ConfirmAsync(model.SenderId, model.LastUpdate.Value, User.Identity.Name);
|
|
return Ok();
|
|
}
|
|
}
|
|
|
|
return BadRequest(CommunicationErrors.Common_Model_Invalid);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Löschen von Systemnachrichten für einen AppUser wenn die Details in der App schon verfügbar sind
|
|
/// </summary>
|
|
/// <param name="model">SystemMessageRemoveDto</param>
|
|
/// <returns>HTTP 200 OK, Felher sonst</returns>
|
|
[HttpPost]
|
|
[Route("RemoveSystemMessages")]
|
|
public async Task<IActionResult> RemoveSystemMessages(SystemMessageRemoveDto model)
|
|
{
|
|
var clientOffset = GetClientDateOffset();
|
|
|
|
if (User?.Identity != null && User.Identity.IsAuthenticated)
|
|
{
|
|
if (ModelState.IsValid)
|
|
{
|
|
await _systemMessageService.RemoveAsync(model.AppUserId, (AppUserType)model.AppUserType, model.Key, model.Table, model.Created, User.Identity.Name);
|
|
return Ok();
|
|
}
|
|
}
|
|
|
|
return BadRequest(CommunicationErrors.Common_Model_Invalid);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helper
|
|
|
|
/// <summary>
|
|
/// Hilfsmethode zum erstellen der Initialen aus Vor- und Nachname
|
|
/// </summary>
|
|
/// <param name="firstName">Vorname</param>
|
|
/// <param name="lastName">Nachname</param>
|
|
/// <returns>Initialen</returns>
|
|
private string GetInitials(string firstName, string lastName)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(firstName) && !string.IsNullOrWhiteSpace(lastName))
|
|
return $"{firstName.Substring(0, 1)}{lastName.Substring(0, 1)}";
|
|
else if (!string.IsNullOrWhiteSpace(firstName) && firstName.Length >= 2 && string.IsNullOrWhiteSpace(lastName))
|
|
return $"{firstName.Substring(0, 2)}";
|
|
else if (string.IsNullOrWhiteSpace(firstName) && string.IsNullOrWhiteSpace(lastName) && lastName.Length >= 2)
|
|
return $"{lastName.Substring(0, 2)}";
|
|
else
|
|
return "??";
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|