mirror of
https://github.com/streetwriters/notesnook-sync-server.git
synced 2026-02-12 19:22:45 +00:00
monograph: add sync support (#39)
* monograph: add sync support * monograph: fix password field && improve syncing logic && fix delete endpoint * sync: get rid of unnecessary .ToList & ToListAsync * sync: AddIdsToAllDevices is no longer asynchronous * monograph: simplify and fix several bugs - we were sending the triggerSync event to all users instead of all devices - asynchronous methods did not have the `Async` suffix - we weren't properly replacing the deleted monograph * monograph: fix minor issues * fix publishing * don't return deleted monograph in monographs/:id endpoint * persist UserId when soft deleting monograph * monograph: check soft delete status in several endpoints --------- Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
This commit is contained in:
@@ -18,16 +18,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<IActionResult> PublishAsync([FromBody] Monograph monograph)
|
||||
public async Task<IActionResult> 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<IActionResult> UpdateAsync([FromBody] Monograph monograph)
|
||||
public async Task<IActionResult> 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<Monograph>.Filter.Eq("UserId", userId), new FindOptions<Monograph, ObjectWithId>
|
||||
{
|
||||
Projection = Builders<Monograph>.Projection.Include("_id").Include("ItemId"),
|
||||
})).ToEnumerable();
|
||||
var monographs = (await Monographs.Collection.FindAsync(
|
||||
Builders<Monograph>.Filter.And(
|
||||
Builders<Monograph>.Filter.Eq("UserId", userId),
|
||||
Builders<Monograph>.Filter.Eq("Deleted", false)
|
||||
)
|
||||
, new FindOptions<Monograph, ObjectWithId>
|
||||
{
|
||||
Projection = Builders<Monograph>.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<IActionResult> 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<IActionResult> 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<IActionResult> DeleteAsync([FromRoute] string id)
|
||||
public async Task<IActionResult> 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." })
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
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<bool> SendItems(SyncTransferItemV2 transferItem);
|
||||
Task<bool> SendVaultKey(EncryptedData vaultKey);
|
||||
Task<bool> SendMonographs(IEnumerable<Monograph> 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
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -194,6 +194,21 @@ namespace Notesnook.API.Services
|
||||
}
|
||||
}
|
||||
|
||||
public void AddIdsToAllDevices(List<string> 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)
|
||||
|
||||
Reference in New Issue
Block a user