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)
{