using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.Json; using System.Threading.Tasks; using CommunityToolkit.Mvvm.Messaging; using gehGassi.Dto; using gehGassi.Dto.Messages; using gehGassiApp.Core.Data; using gehGassiApp.Core.Helper; using gehGassiApp.Core.Interfaces; using gehGassiApp.Core.Interfaces.Synchronization; using gehGassiApp.Domain.Common; using gehGassiApp.Domain.Lookup; using gehGassiApp.Domain.Messages; using gehGassiApp.Domain.Users; namespace gehGassiApp.Core.Services { /// /// Schnittstellenbeschreibung für einen Service der die Verwaltung von Konversationen ermöglicht /// public class ConversationService : ServiceBase, IConversationService { private readonly ICommunicationService _communicationService; private readonly ISyncInfoPullService _pullService; private readonly ISyncInfoPushService _pushService; private readonly IRepository _appUserRepository; /// /// Erstellt eine Instanz /// /// Instanz eines IUnitOfWork /// Instanz eines ICommunicationService /// Instanz eines ISyncInfoPullService /// Instanz eines ISyncInfoPushService public ConversationService(IUnitOfWork unitOfWork, ICommunicationService communicationService, ISyncInfoPullService syncInfoPullService, ISyncInfoPushService syncInfoPushService) : base(unitOfWork) { _communicationService = communicationService; _pullService = syncInfoPullService; _pushService = syncInfoPushService; _appUserRepository = unitOfWork.GetRepository(); } /// /// Gibt eine Entität anhand der eindeutigen Id zurück /// /// Id der Entität /// Gibt an ob NoTRacking verwendet werden soll. Es werden keine Entitäten im EF-Speicher gehalten /// Entität oder null, wenn nicht gefunden public override Conversation Get(object id, bool noTracking = true) { return Repository.SingleOrDefault(c => c.Id == id.ToString(), noTracking); } /// /// Gibt eine Entität anhand der eindeutigen Id zurück /// /// Id der Entität /// Gibt an ob NoTRacking verwendet werden soll. Es werden keine Entitäten im EF-Speicher gehalten /// Entität oder null, wenn nicht gefunden public override async Task GetAsync(object id, bool noTracking = true) { return await Repository.SingleOrDefaultAsync(c => c.Id == id.ToString(), noTracking).ConfigureAwait(false); } /// /// Anlegen einer Konversation /// /// Sender, falls online gesucht werden muss /// Empfänger nach welcher gesucht wird /// Aktuelles Accesstoken /// CancellationToken /// Konversation public async Task> CreateAsync(string senderId, AppUserLookup receipient, string accessToken, CancellationToken token) { var result = new CommunicationResult() { Success = false, }; //Zuerst versuchen den Datensatz online anzulegen var conversationId = Guid.NewGuid().ToString("N"); var isConnected = await _communicationService.IsConnected(); if (isConnected) { var createResult = await _communicationService.CreateConversationAsync(conversationId, senderId, receipient.Id, accessToken, token); if (createResult.Success) { if (createResult.Value.Status == CreateStatus.Success || createResult.Value.Status == CreateStatus.Exists) { var createResponse = createResult.Value; Repository.Add(createResponse.Value); await CommitAsync(); result.Success = true; result.Value = createResponse.Value; return result; } } } //TODO: Achtung, man kann nur eine Konversation anlegen wenn man online ist //else //{ // //In der DB anlegen und Delta erstellen // var conversation = new Conversation // { // Id = conversationId, // Recipient = receipient.Id, // Created = DateTimeOffset.UtcNow, // ReceipientType = receipient.Type, // UpdatedAt = DateTimeOffset.UtcNow, // RecipientPhoto = receipient.Photo, // RecipientName = receipient.FullName, // RecipientShort = receipient.Initials // }; // Repository.Add(conversation); // await CommitAsync().ConfigureAwait(false); // await _pushService.AddAsync(nameof(Conversation), conversation.Id, SyncOperation.Create, conversation).ConfigureAwait(false); // return conversation; //} result.ErrorCode = CommunicationErrors.ServerNoConnection; return result; } /// /// Anlegen einer Konversation /// /// Sender, falls online gesucht werden muss /// Id des Emprängers /// Aktuelles Accesstoken /// CancellationToken /// CommunicationResult public async Task> CreateAsync(string senderId, string receipientId, string accessToken, CancellationToken token) { var result = new CommunicationResult() { Success = false, }; //Zuerst versuchen den Datensatz online anzulegen var conversationId = Guid.NewGuid().ToString("N"); var isConnected = await _communicationService.IsConnected(); if (isConnected) { var createResult = await _communicationService.CreateConversationAsync(conversationId, senderId, receipientId, accessToken, token); if (createResult.Success) { if (createResult.Value.Status == CreateStatus.Success || createResult.Value.Status == CreateStatus.Exists) { var createResponse = createResult.Value; Repository.Add(createResponse.Value); await CommitAsync(); result.Success = true; result.Value = createResponse.Value; return result; } } } result.ErrorCode = CommunicationErrors.ServerNoConnection; return result; } /// /// Gibt eine Liste der Konversationen zurück /// /// Aktuelles Accesstoken /// CancellationToken /// Id des Senders /// Liste Konversatinen public async Task> GetConversationsAsync(string senderId, string accessToken, CancellationToken token) { //Zuerst DB var localConversations = (await Repository.FindAsync(c => c.Deleted == false).ConfigureAwait(false)).ToList(); if (localConversations.Any()) return localConversations.OrderByDescending(c => c.UpdatedAt).ToList(); else { //Online... var isConnected = await _communicationService.IsConnected(); //isConnected = false; if (isConnected) { var conversationsResult = await _communicationService.GetConversationsAsync(senderId, null, accessToken, token); if (conversationsResult.Success) { var result = await HandleChangesAsync(conversationsResult.Value); if (result.LastUpdate != null) await _pullService.AddOrUpddateAsync(nameof(Conversation), result.LastUpdate.Value).ConfigureAwait(false); return conversationsResult.Value.Where(c => c.Deleted == false).OrderByDescending(c => c.UpdatedAt).ToList(); } } } //Keine Daten online, keine offline return new List(); } /// /// Gibt eine Liste der Konversationen zurück /// /// Filter /// Aktuelles Accesstoken /// CancellationToken /// Id des Senders /// Liste Konversatinen public async Task> GetConversationsAsync(string senderId, string filter, string accessToken, CancellationToken token) { //Online... var isConnected = await _communicationService.IsConnected(); //isConnected = false; if (isConnected) { var conversationsResult = await _communicationService.GetConversationsWithFilterAsync(senderId, filter, accessToken, token); if (conversationsResult.Success) { var result = await HandleChangesAsync(conversationsResult.Value); if (result.LastUpdate != null) await _pullService.AddOrUpddateAsync(nameof(Conversation), result.LastUpdate.Value).ConfigureAwait(false); //Wir müssen tricksen und nun die Conversationen von der DB holen, damit auch Daten wie letzte Nachricht usw. dabei ist... var conversationsList = new List(); foreach (var conversation in conversationsResult.Value) { var localConversation = await Repository.FirstOrDefaultAsync(c => c.Id == conversation.Id).ConfigureAwait(false); if (localConversation != null) conversationsList.Add(localConversation); } return conversationsList.Where(c => c.Deleted == false).OrderByDescending(c => c.UpdatedAt).ToList(); } } return new List(); } /// /// Gibt eine Konversation zurück welche ein bestimmtes Ziel hat /// /// Sender, falls online gesucht werden muss /// EmpfängerId nach welcher gesucht wird /// Aktuelles Accesstoken /// CancellationToken /// public async Task GetByReceipientAsync(string senderId, string receipientId, string accessToken, CancellationToken token) { //Zuerst DB var localConversation = await Repository.FirstOrDefaultAsync(c => c.Recipient == receipientId).ConfigureAwait(false); if (localConversation != null) { if (localConversation.Deleted) { localConversation.Deleted = false; Repository.Update(localConversation); await CommitAsync(); } return localConversation; } //Nun online versuchen var isConnected = await _communicationService.IsConnected(); //isConnected = false; if (isConnected) { var conversationsResult = await _communicationService.GetConversationByReceipientAsync(senderId, receipientId, accessToken, token); if (conversationsResult.Success) { var list = new List(); list.Add(conversationsResult.Value); var result = await HandleChangesAsync(list); if (result.LastUpdate != null) await _pullService.AddOrUpddateAsync(nameof(Conversation), result.LastUpdate.Value).ConfigureAwait(false); return conversationsResult.Value; } } return null; } /// /// Setzen der letzten Nachricht für eine Konversation /// /// Id der Konversation /// Id der letzten Nachricht /// Vorschautext der letzten Nachricht /// Datum letztes Update /// Task public async Task SetLastMessageAsync(string conversationId, string messageId, string messagePreview, DateTimeOffset lastUpdate) { var conversation = await Repository.FirstOrDefaultAsync(c => c.Id == conversationId, false).ConfigureAwait(false); if (conversation != null) { conversation.LastMessageId = messageId; conversation.LastMessagePreview = messagePreview.Ellipsis(100); conversation.UpdatedAt = lastUpdate; Repository.Update(conversation); await CommitAsync().ConfigureAwait(false); } } /// /// Gibt zurück ob es Konversationen mit ungelesenen nachrichten gibt /// /// public async Task HasUnreadAsync() { var unread = await Repository.CountAsync(c => c.HasUnreadMessages == true).ConfigureAwait(false); return unread > 0; } #region Pull-Push Implementation /// /// Holen der letzten Daten vom Server und Synchronisieren mit den lokalen Daten /// /// Sprache /// Aktuelles Accesstoken /// CancellationToken /// Task async Task> ISyncPushPullService.PullAsync(string language, string accessToken, CancellationToken token) { var result = new SyncResult(); var isConnected = await _communicationService.IsConnected(); if (isConnected) { //Zuerst lezte Aktivität holen... DateTimeOffset? lastUpdate = null; var lastSyncInfo = await _pullService.GetAsync(nameof(Conversation)); if (lastSyncInfo != null) lastUpdate = lastSyncInfo.LastUpdate; var user = await _appUserRepository.FirstOrDefaultAsync(c => c.Id != "", false).ConfigureAwait(false); var conversationsResult = await _communicationService.GetConversationsAsync(user.Id, lastUpdate, accessToken, token); if (conversationsResult.Success) { result = await HandleChangesAsync(conversationsResult.Value); if (result.LastUpdate != null) await _pullService.AddOrUpddateAsync(nameof(Conversation), result.LastUpdate.Value).ConfigureAwait(false); } result.LastUpdate ??= lastUpdate; } return result; } /// /// Holen der Daten vom lokalen Speicher die noch nicht synchronisiert wurden und senden an den Server /// /// Aktuelles Accesstoken /// CancellationToken /// Task async Task> ISyncPushPullService.PushAsync(string accessToken, CancellationToken token) { var result = new SyncResult(); if (await _pushService.HasOpenAsync(nameof(Conversation)) > 0) { var deltas = await _pushService.GetAllAsync(nameof(Conversation)); if (deltas.Any()) { var isConnected = await _communicationService.IsConnected(); if (!isConnected) return result; var user = await _appUserRepository.FirstOrDefaultAsync(c => c.Id != "", false).ConfigureAwait(false); deltas = deltas.OrderBy(c => c.DateTime).ToList(); foreach (var syncInfoPush in deltas) { //Löschen einer Konversation geben wir nicht weiter... Update gibt es eigentlich auch keines if (syncInfoPush.Operation == SyncOperation.Delete || syncInfoPush.Operation == SyncOperation.Edit) { await _pushService.RemoveAsync(syncInfoPush.Id).ConfigureAwait(false); continue; } try { if (!string.IsNullOrWhiteSpace(syncInfoPush.Value)) { var conversation = JsonSerializer.Deserialize(syncInfoPush.Value, new JsonSerializerOptions(JsonSerializerDefaults.Web)); var model = new CreateConversationDto() { Id = conversation.Id, SenderId = user.Id, ReceiverId = conversation.Recipient }; var createResult = await _communicationService.CreateConversationAsync(conversation.Id, user.Id, conversation.Recipient, accessToken, token); if (createResult.Success) { if (createResult.Value.Status == CreateStatus.Exists) { //Neue Konversation anlegen alte löschen. //Deltas der alten Nachrichten werden am Server ignoriert... var newConversation = createResult.Value.Value; Repository.Add(newConversation); await CommitAsync().ConfigureAwait(false); var existingConversation = await Repository.FirstOrDefaultAsync(c => c.Id == conversation.Id, false).ConfigureAwait(false); Remove(existingConversation); await CommitAsync().ConfigureAwait(false); } await _pushService.RemoveAsync(syncInfoPush.Id).ConfigureAwait(false); } else { //Fehler, weg damit await _pushService.RemoveAsync(syncInfoPush.Id).ConfigureAwait(false); } //Else ist nichts tun, konnte nicht übertragen werden. } else { await _pushService.RemoveAsync(syncInfoPush.Id).ConfigureAwait(false); } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex.Message); await _pushService.RemoveAsync(syncInfoPush.Id).ConfigureAwait(false); } } } } //Es gibt hier kein Push! await Task.Delay(1); return result; } #endregion #region Private /// /// Behandeln der Liste von Konversationen wenn welche vom Online-Store geholt werden. /// /// Liste der Konversationen /// Task private async Task> HandleChangesAsync(List conversations) { var result = new SyncResult(); foreach (var conversation in conversations) { var localConversation = await Repository.FirstOrDefaultAsync(c => c.Id == conversation.Id, false).ConfigureAwait(false); if (localConversation != null && localConversation.UpdatedAt < conversation.UpdatedAt) { if (localConversation.UpdatedAt >= conversation.UpdatedAt) { if (result.LastUpdate == null || result.LastUpdate < localConversation.UpdatedAt) result.LastUpdate = localConversation.UpdatedAt; continue; } var deleted = localConversation.Deleted == false && conversation.Deleted; localConversation.Version = conversation.Version; localConversation.UpdatedAt = conversation.UpdatedAt; localConversation.Deleted = conversation.Deleted; localConversation.Recipient = conversation.Recipient; localConversation.ReceipientType = conversation.ReceipientType; localConversation.RecipientName = conversation.RecipientName; localConversation.RecipientShort = conversation.RecipientShort; localConversation.RecipientPhoto = conversation.RecipientPhoto; if (result.LastUpdate == null || result.LastUpdate < localConversation.UpdatedAt) result.LastUpdate = localConversation.UpdatedAt; if (!deleted) result.Updated.Add(localConversation); else result.Deleted.Add(localConversation); Repository.Update(localConversation); } if (localConversation == null) { localConversation = new Conversation() { Id = conversation.Id, Version = conversation.Version, UpdatedAt = conversation.UpdatedAt, Deleted = conversation.Deleted, Recipient = conversation.Recipient, ReceipientType = conversation.ReceipientType, RecipientName = conversation.RecipientName, RecipientShort = conversation.RecipientShort, RecipientPhoto = conversation.RecipientPhoto, Created = conversation.Created }; Repository.Add(localConversation); if (result.LastUpdate == null || result.LastUpdate < localConversation.UpdatedAt) result.LastUpdate = localConversation.UpdatedAt; result.Added.Add(localConversation); } } if (result.HasChanges) { try { await CommitAsync().ConfigureAwait(false); } catch (Exception ex) { var err = ex.Message; } } return result; } #endregion } }