diff --git a/Notesnook.API/Controllers/MonographsController.cs b/Notesnook.API/Controllers/MonographsController.cs index 4a08439..77a27a1 100644 --- a/Notesnook.API/Controllers/MonographsController.cs +++ b/Notesnook.API/Controllers/MonographsController.cs @@ -18,16 +18,19 @@ along with this program. If not, see . */ using System; +using System.Collections.Generic; using System.Linq; using System.Security.Claims; -using System.Text.RegularExpressions; +using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using MongoDB.Bson; using MongoDB.Driver; using Notesnook.API.Models; +using Notesnook.API.Services; using Streetwriters.Common; +using Streetwriters.Common.Messages; using Streetwriters.Data.Interfaces; using Streetwriters.Data.Repositories; @@ -92,14 +95,18 @@ namespace Notesnook.API.Controllers } [HttpPost] - public async Task PublishAsync([FromBody] Monograph monograph) + public async Task PublishAsync([FromQuery] string deviceId, [FromBody] Monograph monograph) { try { var userId = this.User.FindFirstValue("sub"); if (userId == null) return Unauthorized(); - if (await FindMonographAsync(userId, monograph) != null) return base.Conflict("This monograph is already published."); + var existingMonograph = await FindMonographAsync(userId, monograph); + if (existingMonograph != null && !existingMonograph.Deleted) + { + return base.Conflict("This monograph is already published."); + } if (monograph.EncryptedContent == null) monograph.CompressedContent = monograph.Content.CompressBrotli(); @@ -109,11 +116,23 @@ namespace Notesnook.API.Controllers if (monograph.EncryptedContent?.Cipher.Length > MAX_DOC_SIZE || monograph.CompressedContent?.Length > MAX_DOC_SIZE) return base.BadRequest("Monograph is too big. Max allowed size is 15mb."); - await Monographs.InsertAsync(monograph); + if (existingMonograph != null) + { + monograph.Id = existingMonograph?.Id; + } + monograph.Deleted = false; + await Monographs.Collection.ReplaceOneAsync( + CreateMonographFilter(userId, monograph), + monograph, + new ReplaceOptions { IsUpsert = true } + ); + + await MarkMonographForSyncAsync(monograph.ItemId ?? monograph.Id, deviceId); return Ok(new { - id = monograph.ItemId + id = monograph.ItemId, + datePublished = monograph.DatePublished, }); } catch (Exception e) @@ -124,14 +143,18 @@ namespace Notesnook.API.Controllers } [HttpPatch] - public async Task UpdateAsync([FromBody] Monograph monograph) + public async Task UpdateAsync([FromQuery] string deviceId, [FromBody] Monograph monograph) { try { var userId = this.User.FindFirstValue("sub"); if (userId == null) return Unauthorized(); - if (await FindMonographAsync(userId, monograph) == null) return NotFound(); + var existingMonograph = await FindMonographAsync(userId, monograph); + if (existingMonograph != null || existingMonograph.Deleted) + { + return NotFound(); + } if (monograph.EncryptedContent?.Cipher.Length > MAX_DOC_SIZE || monograph.CompressedContent?.Length > MAX_DOC_SIZE) return base.BadRequest("Monograph is too big. Max allowed size is 15mb."); @@ -150,12 +173,16 @@ namespace Notesnook.API.Controllers .Set(m => m.EncryptedContent, monograph.EncryptedContent) .Set(m => m.SelfDestruct, monograph.SelfDestruct) .Set(m => m.Title, monograph.Title) + .Set(m => m.Password, monograph.Password) ); if (!result.IsAcknowledged) return BadRequest(); + await MarkMonographForSyncAsync(monograph.ItemId ?? monograph.Id, deviceId); + return Ok(new { - id = monograph.ItemId + id = monograph.ItemId, + datePublished = monograph.DatePublished, }); } catch (Exception e) @@ -171,10 +198,15 @@ namespace Notesnook.API.Controllers var userId = this.User.FindFirstValue("sub"); if (userId == null) return Unauthorized(); - var monographs = (await Monographs.Collection.FindAsync(Builders.Filter.Eq("UserId", userId), new FindOptions - { - Projection = Builders.Projection.Include("_id").Include("ItemId"), - })).ToEnumerable(); + var monographs = (await Monographs.Collection.FindAsync( + Builders.Filter.And( + Builders.Filter.Eq("UserId", userId), + Builders.Filter.Eq("Deleted", false) + ) + , new FindOptions + { + Projection = Builders.Projection.Include("_id").Include("ItemId"), + })).ToEnumerable(); return Ok(monographs.Select((m) => m.ItemId ?? m.Id)); } @@ -183,7 +215,7 @@ namespace Notesnook.API.Controllers public async Task GetMonographAsync([FromRoute] string id) { var monograph = await FindMonographAsync(id); - if (monograph == null) + if (monograph == null || monograph.Deleted) { return NotFound(new { @@ -203,19 +235,89 @@ namespace Notesnook.API.Controllers public async Task TrackView([FromRoute] string id) { var monograph = await FindMonographAsync(id); - if (monograph == null) return Content(SVG_PIXEL, "image/svg+xml"); + if (monograph == null || monograph.Deleted) return Content(SVG_PIXEL, "image/svg+xml"); if (monograph.SelfDestruct) - await Monographs.DeleteByIdAsync(monograph.Id); + { + var userId = this.User.FindFirstValue("sub"); + await Monographs.Collection.ReplaceOneAsync( + CreateMonographFilter(userId, monograph), + new Monograph + { + ItemId = id, + Id = monograph.Id, + Deleted = true + } + ); + + await MarkMonographForSyncAsync(id); + } return Content(SVG_PIXEL, "image/svg+xml"); } [HttpDelete("{id}")] - public async Task DeleteAsync([FromRoute] string id) + public async Task DeleteAsync([FromQuery] string deviceId, [FromRoute] string id) { - await Monographs.Collection.DeleteOneAsync(CreateMonographFilter(id)); + var monograph = await FindMonographAsync(id); + if (monograph == null || monograph.Deleted) + { + return NotFound(new + { + error = "invalid_id", + error_description = $"No such monograph found." + }); + } + + var userId = this.User.FindFirstValue("sub"); + await Monographs.Collection.ReplaceOneAsync( + CreateMonographFilter(userId, monograph), + new Monograph + { + ItemId = id, + Id = monograph.Id, + Deleted = true, + UserId = monograph.UserId + } + ); + + await MarkMonographForSyncAsync(id, deviceId); + return Ok(); } + + private async Task MarkMonographForSyncAsync(string monographId, string deviceId) + { + if (deviceId == null) return; + var userId = this.User.FindFirstValue("sub"); + + new SyncDeviceService(new SyncDevice(userId, deviceId)).AddIdsToOtherDevices([$"{monographId}:monograph"]); + await SendTriggerSyncEventAsync(); + } + + private async Task MarkMonographForSyncAsync(string monographId) + { + var userId = this.User.FindFirstValue("sub"); + + new SyncDeviceService(new SyncDevice(userId, string.Empty)).AddIdsToAllDevices([$"{monographId}:monograph"]); + await SendTriggerSyncEventAsync(sendToAllDevices: true); + } + + private async Task SendTriggerSyncEventAsync(bool sendToAllDevices = false) + { + var userId = this.User.FindFirstValue("sub"); + var jti = this.User.FindFirstValue("jti"); + + await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage + { + OriginTokenId = sendToAllDevices ? null : jti, + UserId = userId, + Message = new Message + { + Type = "triggerSync", + Data = JsonSerializer.Serialize(new { reason = "Monographs updated." }) + } + }); + } } } \ No newline at end of file diff --git a/Notesnook.API/Hubs/SyncV2Hub.cs b/Notesnook.API/Hubs/SyncV2Hub.cs index 8b62d92..819c23f 100644 --- a/Notesnook.API/Hubs/SyncV2Hub.cs +++ b/Notesnook.API/Hubs/SyncV2Hub.cs @@ -20,7 +20,6 @@ along with this program. If not, see . using System; using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.Metrics; using System.Linq; using System.Security.Claims; using System.Text.Json.Serialization; @@ -28,8 +27,6 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.SignalR; -using MongoDB.Bson; -using MongoDB.Bson.Serialization; using MongoDB.Driver; using Notesnook.API.Authorization; using Notesnook.API.Interfaces; @@ -43,6 +40,7 @@ namespace Notesnook.API.Hubs { Task SendItems(SyncTransferItemV2 transferItem); Task SendVaultKey(EncryptedData vaultKey); + Task SendMonographs(IEnumerable monographs); Task PushCompleted(); } @@ -259,6 +257,7 @@ namespace Notesnook.API.Hubs if (!await Clients.Caller.SendVaultKey(userSettings.VaultKey).WaitAsync(TimeSpan.FromMinutes(10))) throw new HubException("Client rejected vault key."); } + await foreach (var chunk in chunks) { if (!await Clients.Caller.SendItems(chunk).WaitAsync(TimeSpan.FromMinutes(10))) throw new HubException("Client rejected sent items."); @@ -271,6 +270,15 @@ namespace Notesnook.API.Hubs } } + var unsyncedMonographs = ids.Where((id) => id.EndsWith(":monograph")).ToHashSet(); + var unsyncedMonographIds = unsyncedMonographs.Select((id) => id.Split(":")[0]).ToArray(); + var userMonographs = isResetSync + ? await Repositories.Monographs.FindAsync(m => m.UserId == userId) + : await Repositories.Monographs.FindAsync(m => m.UserId == userId && unsyncedMonographIds.Contains(m.ItemId)); + + if (userMonographs.Any() && !await Clients.Caller.SendMonographs(userMonographs).WaitAsync(TimeSpan.FromMinutes(10))) + throw new HubException("Client rejected monographs."); + deviceService.Reset(); return new SyncV2Metadata diff --git a/Notesnook.API/Models/Monograph.cs b/Notesnook.API/Models/Monograph.cs index bae6067..1830e4c 100644 --- a/Notesnook.API/Models/Monograph.cs +++ b/Notesnook.API/Models/Monograph.cs @@ -86,5 +86,11 @@ namespace Notesnook.API.Models [JsonIgnore] public byte[] CompressedContent { get; set; } + + [JsonPropertyName("password")] + public EncryptedData Password { get; set; } + + [JsonPropertyName("deleted")] + public bool Deleted { get; set; } } } \ No newline at end of file diff --git a/Notesnook.API/Services/SyncDeviceService.cs b/Notesnook.API/Services/SyncDeviceService.cs index d627e64..2c1bf3d 100644 --- a/Notesnook.API/Services/SyncDeviceService.cs +++ b/Notesnook.API/Services/SyncDeviceService.cs @@ -194,6 +194,21 @@ namespace Notesnook.API.Services } } + public void AddIdsToAllDevices(List ids) + { + foreach (var id in ListDevices()) + { + if (IsSyncReset(id)) return; + lock (id) + { + if (!IsDeviceRegistered(id)) Directory.CreateDirectory(Path.Join(device.UserSyncDirectoryPath, id)); + + var oldIds = GetUnsyncedIds(id); + File.WriteAllLinesAsync(Path.Join(device.UserSyncDirectoryPath, id, "unsynced"), ids.Union(oldIds)); + } + } + } + public void RegisterDevice() { lock (device.UserId)