From 5717b6708ded19ec38e80271612dc55dddca6c39 Mon Sep 17 00:00:00 2001 From: 01zulfi <85733202+01zulfi@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:05:20 +0500 Subject: [PATCH] monographs: add slug field which regenerates on update --- .../Controllers/MonographsController.cs | 67 ++++++++++++++++--- .../Extensions/MonographExtensions.cs | 33 +++++++++ Notesnook.API/Hubs/SyncV2Hub.cs | 9 ++- Notesnook.API/Models/Monograph.cs | 3 + Notesnook.API/Models/MonographMetadata.cs | 5 +- Streetwriters.Common/Constants.cs | 1 + 6 files changed, 104 insertions(+), 14 deletions(-) create mode 100644 Notesnook.API/Extensions/MonographExtensions.cs diff --git a/Notesnook.API/Controllers/MonographsController.cs b/Notesnook.API/Controllers/MonographsController.cs index 25ec93d..d41a8fe 100644 --- a/Notesnook.API/Controllers/MonographsController.cs +++ b/Notesnook.API/Controllers/MonographsController.cs @@ -18,27 +18,25 @@ You should have received a copy of the Affero GNU General Public License */ using System; -using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Text.Json; using System.Threading.Tasks; using AngleSharp; -using AngleSharp.Dom; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using MongoDB.Bson; using MongoDB.Driver; -using Notesnook.API.Authorization; +using NanoidDotNet; +using Notesnook.API.Extensions; using Notesnook.API.Models; using Notesnook.API.Services; using Streetwriters.Common; using Streetwriters.Common.Helpers; using Streetwriters.Common.Interfaces; using Streetwriters.Common.Messages; -using Streetwriters.Data.Interfaces; using Streetwriters.Data.Repositories; namespace Notesnook.API.Controllers @@ -95,6 +93,22 @@ private async Task FindMonographAsync(string itemId) return await result.FirstOrDefaultAsync(); } + private async Task FindMonographBySlugAsync(string slug) + { + var result = await monographs.Collection.FindAsync( + Builders.Filter.Eq("Slug", slug), + new FindOptions + { + Limit = 1 + }); + return await result.FirstOrDefaultAsync(); + } + + private static string GenerateSlug() + { + return Nanoid.Generate(size: 24); + } + [HttpPost] public async Task PublishAsync([FromQuery] string? deviceId, [FromBody] Monograph monograph) { @@ -120,6 +134,7 @@ public async Task PublishAsync([FromQuery] string? deviceId, [Fro } monograph.Deleted = false; monograph.ViewCount = 0; + monograph.Slug = GenerateSlug(); await monographs.Collection.ReplaceOneAsync( CreateMonographFilter(userId, monograph), monograph, @@ -131,7 +146,8 @@ await monographs.Collection.ReplaceOneAsync( return Ok(new { id = monograph.ItemId, - datePublished = monograph.DatePublished + datePublished = monograph.DatePublished, + publishUrl = monograph.ConstructPublishUrl() }); } catch (Exception e) @@ -164,6 +180,7 @@ public async Task UpdateAsync([FromQuery] string? deviceId, [From monograph.Content = null; monograph.DatePublished = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + monograph.Slug = GenerateSlug(); var result = await monographs.Collection.UpdateOneAsync( CreateMonographFilter(userId, monograph), Builders.Update @@ -172,6 +189,7 @@ public async Task UpdateAsync([FromQuery] string? deviceId, [From .Set(m => m.EncryptedContent, monograph.EncryptedContent) .Set(m => m.SelfDestruct, monograph.SelfDestruct) .Set(m => m.Title, monograph.Title) + .Set(m => m.Slug, monograph.Slug) .Set(m => m.Password, monograph.Password) ); if (!result.IsAcknowledged) return BadRequest(); @@ -181,7 +199,8 @@ public async Task UpdateAsync([FromQuery] string? deviceId, [From return Ok(new { id = monograph.ItemId, - datePublished = monograph.DatePublished + datePublished = monograph.DatePublished, + publishUrl = monograph.ConstructPublishUrl() }); } catch (Exception e) @@ -208,11 +227,25 @@ public async Task GetUserMonographsAsync() return Ok(userMonographs.Select((m) => m.ItemId ?? m.Id)); } - [HttpGet("{id}")] + [HttpGet("{slugOrId}")] [AllowAnonymous] - public async Task GetMonographAsync([FromRoute] string id) + public async Task GetMonographAsync([FromRoute] string slugOrId) { - var monograph = await FindMonographAsync(id); + var monograph = await FindMonographBySlugAsync(slugOrId); + + if (monograph == null) + { + monograph = await FindMonographAsync(slugOrId); + if (!string.IsNullOrEmpty(monograph?.Slug)) + { + return NotFound(new + { + error = "invalid_id", + error_description = $"No such monograph found." + }); + } + } + if (monograph == null || monograph.Deleted) { return NotFound(new @@ -317,6 +350,22 @@ await monographs.Collection.ReplaceOneAsync( return Ok(); } + [HttpGet("{id}/publish-url")] + public async Task GetPublishUrlAsync([FromRoute] string id) + { + var userId = this.User.GetUserId(); + var monograph = await FindMonographAsync(id); + if (monograph == null || monograph.Deleted || monograph.UserId != userId) + { + return NotFound(); + } + + return Ok(new + { + publishUrl = monograph.ConstructPublishUrl() + }); + } + private static async Task MarkMonographForSyncAsync(string userId, string monographId, string? deviceId, string? jti) { if (deviceId == null) return; diff --git a/Notesnook.API/Extensions/MonographExtensions.cs b/Notesnook.API/Extensions/MonographExtensions.cs new file mode 100644 index 0000000..041fded --- /dev/null +++ b/Notesnook.API/Extensions/MonographExtensions.cs @@ -0,0 +1,33 @@ +/* +This file is part of the Notesnook Sync Server project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the Affero GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +Affero GNU General Public License for more details. + +You should have received a copy of the Affero GNU General Public License +along with this program. If not, see . +*/ + +using Notesnook.API.Models; +using Streetwriters.Common; + +namespace Notesnook.API.Extensions +{ + public static class MonographExtensions + { + public static string ConstructPublishUrl(this Monograph monograph) + { + var baseUrl = Constants.MONOGRAPH_PUBLIC_URL; + return $"{baseUrl}/{monograph.Slug ?? monograph.ItemId}"; + } + } +} \ No newline at end of file diff --git a/Notesnook.API/Hubs/SyncV2Hub.cs b/Notesnook.API/Hubs/SyncV2Hub.cs index e5846c2..e807f94 100644 --- a/Notesnook.API/Hubs/SyncV2Hub.cs +++ b/Notesnook.API/Hubs/SyncV2Hub.cs @@ -31,6 +31,7 @@ You should have received a copy of the Affero GNU General Public License using Microsoft.AspNetCore.SignalR; using MongoDB.Driver; using Notesnook.API.Authorization; +using Notesnook.API.Extensions; using Notesnook.API.Interfaces; using Notesnook.API.Models; using Notesnook.API.Services; @@ -275,18 +276,20 @@ private async Task HandleRequestFetch(string deviceId, bool incl Builders.Filter.In("_id", unsyncedMonographIds) ) ); - var userMonographs = await Repositories.Monographs.Collection.Find(filter).Project((m) => new MonographMetadata + var userMonographs = await Repositories.Monographs.Collection.Find(filter).ToListAsync(); + var userMonographMetadatas = userMonographs.Select((m) => new MonographMetadata { DatePublished = m.DatePublished, Deleted = m.Deleted, Password = m.Password, SelfDestruct = m.SelfDestruct, Title = m.Title, + PublishUrl = m.ConstructPublishUrl(), ItemId = m.ItemId ?? m.Id.ToString(), ViewCount = m.ViewCount - }).ToListAsync(); + }).ToList(); - if (userMonographs.Count > 0 && !await Clients.Caller.SendMonographs(userMonographs).WaitAsync(TimeSpan.FromMinutes(10))) + if (userMonographMetadatas.Count > 0 && !await Clients.Caller.SendMonographs(userMonographMetadatas).WaitAsync(TimeSpan.FromMinutes(10))) throw new HubException("Client rejected monographs."); device.HasInitialMonographsSync = true; diff --git a/Notesnook.API/Models/Monograph.cs b/Notesnook.API/Models/Monograph.cs index 3d0aa59..247b260 100644 --- a/Notesnook.API/Models/Monograph.cs +++ b/Notesnook.API/Models/Monograph.cs @@ -56,6 +56,9 @@ public Monograph() [JsonPropertyName("title")] public string? Title { get; set; } + [JsonPropertyName("slug")] + public string? Slug { get; set; } + [JsonPropertyName("userId")] public string? UserId { get; set; } diff --git a/Notesnook.API/Models/MonographMetadata.cs b/Notesnook.API/Models/MonographMetadata.cs index f45477f..d7868c1 100644 --- a/Notesnook.API/Models/MonographMetadata.cs +++ b/Notesnook.API/Models/MonographMetadata.cs @@ -18,8 +18,6 @@ You should have received a copy of the Affero GNU General Public License */ using System.Text.Json.Serialization; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; using System.Runtime.Serialization; namespace Notesnook.API.Models @@ -37,6 +35,9 @@ public required string ItemId [JsonPropertyName("title")] public string? Title { get; set; } + [JsonPropertyName("publishUrl")] + public string? PublishUrl { get; set; } + [JsonPropertyName("selfDestruct")] public bool SelfDestruct { get; set; } diff --git a/Streetwriters.Common/Constants.cs b/Streetwriters.Common/Constants.cs index f05529e..590440e 100644 --- a/Streetwriters.Common/Constants.cs +++ b/Streetwriters.Common/Constants.cs @@ -79,6 +79,7 @@ public class Constants public static string? SUBSCRIPTIONS_CERT_PATH => Environment.GetEnvironmentVariable("SUBSCRIPTIONS_CERT_PATH"); public static string? SUBSCRIPTIONS_CERT_KEY_PATH => Environment.GetEnvironmentVariable("SUBSCRIPTIONS_CERT_KEY_PATH"); public static string[] NOTESNOOK_CORS_ORIGINS => Environment.GetEnvironmentVariable("NOTESNOOK_CORS")?.Split(",") ?? new string[] { }; + public static string MONOGRAPH_PUBLIC_URL => Environment.GetEnvironmentVariable("MONOGRAPH_PUBLIC_URL") ?? "https://monogr.ph"; } }