diff --git a/Notesnook.API/Controllers/MonographsController.cs b/Notesnook.API/Controllers/MonographsController.cs index bf78c0b..e177b23 100644 --- a/Notesnook.API/Controllers/MonographsController.cs +++ b/Notesnook.API/Controllers/MonographsController.cs @@ -18,20 +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.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; @@ -40,7 +39,6 @@ using Streetwriters.Common.Enums; using Streetwriters.Common.Helpers; using Streetwriters.Common.Interfaces; using Streetwriters.Common.Messages; -using Streetwriters.Data.Interfaces; using Streetwriters.Data.Repositories; namespace Notesnook.API.Controllers @@ -70,13 +68,15 @@ namespace Notesnook.API.Controllers ); } - private static FilterDefinition CreateMonographFilter(string itemId) + private static FilterDefinition CreateMonographFilter(string itemIdOrSlug) { - return ObjectId.TryParse(itemId, out ObjectId id) + return ObjectId.TryParse(itemIdOrSlug, out ObjectId id) ? Builders.Filter.Or( Builders.Filter.Eq("_id", id), - Builders.Filter.Eq("ItemId", itemId)) - : Builders.Filter.Eq("ItemId", itemId); + Builders.Filter.Eq("ItemId", itemIdOrSlug)) + : Builders.Filter.Or( + Builders.Filter.Eq("Slug", itemIdOrSlug), + Builders.Filter.Eq("ItemId", itemIdOrSlug)); } private async Task FindMonographAsync(string userId, Monograph monograph) @@ -88,15 +88,20 @@ namespace Notesnook.API.Controllers return await result.FirstOrDefaultAsync(); } - private async Task FindMonographAsync(string itemId) + private async Task FindMonographAsync(string itemIdOrSlug) { - var result = await monographs.Collection.FindAsync(CreateMonographFilter(itemId), new FindOptions + var result = await monographs.Collection.FindAsync(CreateMonographFilter(itemIdOrSlug), 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) { @@ -126,6 +131,7 @@ namespace Notesnook.API.Controllers } monograph.Deleted = false; monograph.ViewCount = 0; + monograph.Slug = GenerateSlug(); await monographs.Collection.ReplaceOneAsync( CreateMonographFilter(userId, monograph), monograph, @@ -137,7 +143,8 @@ namespace Notesnook.API.Controllers return Ok(new { id = monograph.ItemId, - datePublished = monograph.DatePublished + datePublished = monograph.DatePublished, + publishUrl = Helpers.UrlHelper.ConstructPublishUrl(monograph) }); } catch (Exception e) @@ -192,7 +199,8 @@ namespace Notesnook.API.Controllers return Ok(new { id = monograph.ItemId, - datePublished = monograph.DatePublished + datePublished = monograph.DatePublished, + publishUrl = Helpers.UrlHelper.ConstructPublishUrl(existingMonograph) }); } catch (Exception e) @@ -224,7 +232,7 @@ namespace Notesnook.API.Controllers public async Task GetMonographAsync([FromRoute] string id) { var monograph = await FindMonographAsync(id); - if (monograph == null || monograph.Deleted) + if (monograph == null || monograph.Deleted || (monograph.Slug != null && monograph.Slug != id)) { return NotFound(new { @@ -259,7 +267,8 @@ namespace Notesnook.API.Controllers public async Task TrackView([FromRoute] string id) { var monograph = await FindMonographAsync(id); - if (monograph == null || monograph.Deleted) return Content(SVG_PIXEL, "image/svg+xml"); + if (monograph == null || monograph.Deleted || (monograph.Slug != null && monograph.Slug != id)) + return Content(SVG_PIXEL, "image/svg+xml"); var cookieName = $"viewed_{id}"; var hasVisitedBefore = Request.Cookies.ContainsKey(cookieName); @@ -300,6 +309,7 @@ namespace Notesnook.API.Controllers } [HttpGet("{id}/analytics")] + [Obsolete("This endpoint is deprecated and will be removed in future versions. Use GET /monographs/{id}/metadata instead.")] public async Task GetMonographAnalyticsAsync([FromRoute] string id) { if (!FeatureAuthorizationHelper.IsFeatureAllowed(Features.MONOGRAPH_ANALYTICS, Clients.Notesnook.Id, User)) @@ -343,6 +353,29 @@ namespace Notesnook.API.Controllers return Ok(); } + [HttpGet("{id}/metadata")] + public async Task GetMetadataAsync([FromRoute] string id) + { + var userId = this.User.GetUserId(); + var monograph = await FindMonographAsync(id); + if (monograph == null || monograph.Deleted || monograph.UserId != userId) + { + return NotFound(); + } + + var isPro = FeatureAuthorizationHelper.IsFeatureAllowed(Features.MONOGRAPH_ANALYTICS, Clients.Notesnook.Id, User); + var totalViews = isPro ? monograph.ViewCount : 0; + + return Ok(new + { + publishUrl = Helpers.UrlHelper.ConstructPublishUrl(monograph), + analytics = new + { + totalViews + } + }); + } + private async Task MarkMonographForSyncAsync(string userId, string monographId, string? deviceId, string? jti) { if (deviceId == null) return; diff --git a/Notesnook.API/Helpers/UrlHelper.cs b/Notesnook.API/Helpers/UrlHelper.cs new file mode 100644 index 0000000..5f93afe --- /dev/null +++ b/Notesnook.API/Helpers/UrlHelper.cs @@ -0,0 +1,42 @@ +/* +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.Helpers +{ + public class UrlHelper + { + public static string ConstructPublishUrl(string slug) + { + var baseUrl = Constants.MONOGRAPH_PUBLIC_URL; + return $"{baseUrl}/{slug}"; + } + public static string ConstructPublishUrl(Monograph monograph) + { + return ConstructPublishUrl(monograph.Slug ?? monograph.ItemId ?? monograph.Id); + } + + public static string ConstructPublishUrl(MonographMetadata metadata) + { + return ConstructPublishUrl(metadata.PublishUrl ?? metadata.ItemId); + } + } +} diff --git a/Notesnook.API/Hubs/SyncV2Hub.cs b/Notesnook.API/Hubs/SyncV2Hub.cs index 85129c9..3215718 100644 --- a/Notesnook.API/Hubs/SyncV2Hub.cs +++ b/Notesnook.API/Hubs/SyncV2Hub.cs @@ -32,6 +32,8 @@ using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; using MongoDB.Driver; using Notesnook.API.Authorization; +using Notesnook.API.Extensions; +using Notesnook.API.Helpers; using Notesnook.API.Interfaces; using Notesnook.API.Models; using Notesnook.API.Services; @@ -275,15 +277,25 @@ namespace Notesnook.API.Hubs 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) + .Project((m) => new MonographMetadata + { + DatePublished = m.DatePublished, + Deleted = m.Deleted, + Password = m.Password, + SelfDestruct = m.SelfDestruct, + Title = m.Title, + ItemId = m.ItemId ?? m.Id.ToString(), + PublishUrl = m.Slug // this will be converted to full url in the end, but we only need slug for now + }) + .ToListAsync(); + + userMonographs = userMonographs.Select((p) => { - DatePublished = m.DatePublished, - Deleted = m.Deleted, - Password = m.Password, - SelfDestruct = m.SelfDestruct, - Title = m.Title, - ItemId = m.ItemId ?? m.Id.ToString() - }).ToListAsync(); + p.PublishUrl = UrlHelper.ConstructPublishUrl(p); + return p; + }).ToList(); if (userMonographs.Count > 0 && !await Clients.Caller.SendMonographs(userMonographs).WaitAsync(TimeSpan.FromMinutes(10))) throw new HubException("Client rejected monographs."); diff --git a/Notesnook.API/Models/Monograph.cs b/Notesnook.API/Models/Monograph.cs index 147e8e0..ae4b146 100644 --- a/Notesnook.API/Models/Monograph.cs +++ b/Notesnook.API/Models/Monograph.cs @@ -56,6 +56,9 @@ namespace Notesnook.API.Models [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 73391af..09867e4 100644 --- a/Notesnook.API/Models/MonographMetadata.cs +++ b/Notesnook.API/Models/MonographMetadata.cs @@ -19,8 +19,6 @@ along with this program. If not, see . using System.Runtime.Serialization; using System.Text.Json.Serialization; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; namespace Notesnook.API.Models { @@ -37,6 +35,9 @@ namespace Notesnook.API.Models [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 7898c27..58862cd 100644 --- a/Streetwriters.Common/Constants.cs +++ b/Streetwriters.Common/Constants.cs @@ -80,6 +80,7 @@ namespace Streetwriters.Common public static string? SUBSCRIPTIONS_CERT_KEY_PATH => ReadSecret("SUBSCRIPTIONS_CERT_KEY_PATH"); public static string[] NOTESNOOK_CORS_ORIGINS => ReadSecret("NOTESNOOK_CORS")?.Split(",") ?? []; public static string? SIGNALR_REDIS_CONNECTION_STRING => ReadSecret("SIGNALR_REDIS_CONNECTION_STRING"); + public static string MONOGRAPH_PUBLIC_URL => ReadSecret("MONOGRAPH_PUBLIC_URL") ?? "https://monogr.ph"; public static string? ReadSecret(string name) {