mirror of
https://github.com/streetwriters/notesnook-sync-server.git
synced 2026-10-01 02:39:39 +02:00
monographs: add slug field which regenerates on republish (#72)
* monographs: add slug field which regenerates on update * monographs: don't regenerate slug on update * common: fix monograph public url constant * monographs: improve APIs && use .Project when fetching monographs * create separate endpoint for fetching monographs by slug * combine analytics and publish-url endpoint into a publish-info endpoint * monographs: reinstate analytics endpoint * common: add missing monograph constant * monograph: refactoring --------- Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
This commit is contained in:
1 parent
da58262afb
commit
4bc1469dfe
6 files changed
+116
-24
No files matched your search
@@ -18,20 +18,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AngleSharp;
|
using AngleSharp;
|
||||||
using AngleSharp.Dom;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using MongoDB.Bson;
|
using MongoDB.Bson;
|
||||||
using MongoDB.Driver;
|
using MongoDB.Driver;
|
||||||
using Notesnook.API.Authorization;
|
using NanoidDotNet;
|
||||||
|
using Notesnook.API.Extensions;
|
||||||
using Notesnook.API.Models;
|
using Notesnook.API.Models;
|
||||||
using Notesnook.API.Services;
|
using Notesnook.API.Services;
|
||||||
using Streetwriters.Common;
|
using Streetwriters.Common;
|
||||||
@@ -40,7 +39,6 @@ using Streetwriters.Common.Enums;
|
|||||||
using Streetwriters.Common.Helpers;
|
using Streetwriters.Common.Helpers;
|
||||||
using Streetwriters.Common.Interfaces;
|
using Streetwriters.Common.Interfaces;
|
||||||
using Streetwriters.Common.Messages;
|
using Streetwriters.Common.Messages;
|
||||||
using Streetwriters.Data.Interfaces;
|
|
||||||
using Streetwriters.Data.Repositories;
|
using Streetwriters.Data.Repositories;
|
||||||
|
|
||||||
namespace Notesnook.API.Controllers
|
namespace Notesnook.API.Controllers
|
||||||
@@ -70,13 +68,15 @@ namespace Notesnook.API.Controllers
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static FilterDefinition<Monograph> CreateMonographFilter(string itemId)
|
private static FilterDefinition<Monograph> CreateMonographFilter(string itemIdOrSlug)
|
||||||
{
|
{
|
||||||
return ObjectId.TryParse(itemId, out ObjectId id)
|
return ObjectId.TryParse(itemIdOrSlug, out ObjectId id)
|
||||||
? Builders<Monograph>.Filter.Or(
|
? Builders<Monograph>.Filter.Or(
|
||||||
Builders<Monograph>.Filter.Eq("_id", id),
|
Builders<Monograph>.Filter.Eq("_id", id),
|
||||||
Builders<Monograph>.Filter.Eq("ItemId", itemId))
|
Builders<Monograph>.Filter.Eq("ItemId", itemIdOrSlug))
|
||||||
: Builders<Monograph>.Filter.Eq("ItemId", itemId);
|
: Builders<Monograph>.Filter.Or(
|
||||||
|
Builders<Monograph>.Filter.Eq("Slug", itemIdOrSlug),
|
||||||
|
Builders<Monograph>.Filter.Eq("ItemId", itemIdOrSlug));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Monograph> FindMonographAsync(string userId, Monograph monograph)
|
private async Task<Monograph> FindMonographAsync(string userId, Monograph monograph)
|
||||||
@@ -88,15 +88,20 @@ namespace Notesnook.API.Controllers
|
|||||||
return await result.FirstOrDefaultAsync();
|
return await result.FirstOrDefaultAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Monograph> FindMonographAsync(string itemId)
|
private async Task<Monograph> FindMonographAsync(string itemIdOrSlug)
|
||||||
{
|
{
|
||||||
var result = await monographs.Collection.FindAsync(CreateMonographFilter(itemId), new FindOptions<Monograph>
|
var result = await monographs.Collection.FindAsync(CreateMonographFilter(itemIdOrSlug), new FindOptions<Monograph>
|
||||||
{
|
{
|
||||||
Limit = 1
|
Limit = 1
|
||||||
});
|
});
|
||||||
return await result.FirstOrDefaultAsync();
|
return await result.FirstOrDefaultAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string GenerateSlug()
|
||||||
|
{
|
||||||
|
return Nanoid.Generate(size: 24);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IActionResult> PublishAsync([FromQuery] string? deviceId, [FromBody] Monograph monograph)
|
public async Task<IActionResult> PublishAsync([FromQuery] string? deviceId, [FromBody] Monograph monograph)
|
||||||
{
|
{
|
||||||
@@ -126,6 +131,7 @@ namespace Notesnook.API.Controllers
|
|||||||
}
|
}
|
||||||
monograph.Deleted = false;
|
monograph.Deleted = false;
|
||||||
monograph.ViewCount = 0;
|
monograph.ViewCount = 0;
|
||||||
|
monograph.Slug = GenerateSlug();
|
||||||
await monographs.Collection.ReplaceOneAsync(
|
await monographs.Collection.ReplaceOneAsync(
|
||||||
CreateMonographFilter(userId, monograph),
|
CreateMonographFilter(userId, monograph),
|
||||||
monograph,
|
monograph,
|
||||||
@@ -137,7 +143,8 @@ namespace Notesnook.API.Controllers
|
|||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
id = monograph.ItemId,
|
id = monograph.ItemId,
|
||||||
datePublished = monograph.DatePublished
|
datePublished = monograph.DatePublished,
|
||||||
|
publishUrl = Helpers.UrlHelper.ConstructPublishUrl(monograph)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
@@ -192,7 +199,8 @@ namespace Notesnook.API.Controllers
|
|||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
id = monograph.ItemId,
|
id = monograph.ItemId,
|
||||||
datePublished = monograph.DatePublished
|
datePublished = monograph.DatePublished,
|
||||||
|
publishUrl = Helpers.UrlHelper.ConstructPublishUrl(existingMonograph)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
@@ -224,7 +232,7 @@ namespace Notesnook.API.Controllers
|
|||||||
public async Task<IActionResult> GetMonographAsync([FromRoute] string id)
|
public async Task<IActionResult> GetMonographAsync([FromRoute] string id)
|
||||||
{
|
{
|
||||||
var monograph = await FindMonographAsync(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
|
return NotFound(new
|
||||||
{
|
{
|
||||||
@@ -259,7 +267,8 @@ namespace Notesnook.API.Controllers
|
|||||||
public async Task<IActionResult> TrackView([FromRoute] string id)
|
public async Task<IActionResult> TrackView([FromRoute] string id)
|
||||||
{
|
{
|
||||||
var monograph = await FindMonographAsync(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 cookieName = $"viewed_{id}";
|
||||||
var hasVisitedBefore = Request.Cookies.ContainsKey(cookieName);
|
var hasVisitedBefore = Request.Cookies.ContainsKey(cookieName);
|
||||||
@@ -300,6 +309,7 @@ namespace Notesnook.API.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}/analytics")]
|
[HttpGet("{id}/analytics")]
|
||||||
|
[Obsolete("This endpoint is deprecated and will be removed in future versions. Use GET /monographs/{id}/metadata instead.")]
|
||||||
public async Task<IActionResult> GetMonographAnalyticsAsync([FromRoute] string id)
|
public async Task<IActionResult> GetMonographAnalyticsAsync([FromRoute] string id)
|
||||||
{
|
{
|
||||||
if (!FeatureAuthorizationHelper.IsFeatureAllowed(Features.MONOGRAPH_ANALYTICS, Clients.Notesnook.Id, User))
|
if (!FeatureAuthorizationHelper.IsFeatureAllowed(Features.MONOGRAPH_ANALYTICS, Clients.Notesnook.Id, User))
|
||||||
@@ -343,6 +353,29 @@ namespace Notesnook.API.Controllers
|
|||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id}/metadata")]
|
||||||
|
public async Task<IActionResult> 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)
|
private async Task MarkMonographForSyncAsync(string userId, string monographId, string? deviceId, string? jti)
|
||||||
{
|
{
|
||||||
if (deviceId == null) return;
|
if (deviceId == null) return;
|
||||||
|
|||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,8 @@ using Microsoft.AspNetCore.SignalR;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using MongoDB.Driver;
|
using MongoDB.Driver;
|
||||||
using Notesnook.API.Authorization;
|
using Notesnook.API.Authorization;
|
||||||
|
using Notesnook.API.Extensions;
|
||||||
|
using Notesnook.API.Helpers;
|
||||||
using Notesnook.API.Interfaces;
|
using Notesnook.API.Interfaces;
|
||||||
using Notesnook.API.Models;
|
using Notesnook.API.Models;
|
||||||
using Notesnook.API.Services;
|
using Notesnook.API.Services;
|
||||||
@@ -275,15 +277,25 @@ namespace Notesnook.API.Hubs
|
|||||||
Builders<Monograph>.Filter.In("_id", unsyncedMonographIds)
|
Builders<Monograph>.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,
|
p.PublishUrl = UrlHelper.ConstructPublishUrl(p);
|
||||||
Deleted = m.Deleted,
|
return p;
|
||||||
Password = m.Password,
|
}).ToList();
|
||||||
SelfDestruct = m.SelfDestruct,
|
|
||||||
Title = m.Title,
|
|
||||||
ItemId = m.ItemId ?? m.Id.ToString()
|
|
||||||
}).ToListAsync();
|
|
||||||
|
|
||||||
if (userMonographs.Count > 0 && !await Clients.Caller.SendMonographs(userMonographs).WaitAsync(TimeSpan.FromMinutes(10)))
|
if (userMonographs.Count > 0 && !await Clients.Caller.SendMonographs(userMonographs).WaitAsync(TimeSpan.FromMinutes(10)))
|
||||||
throw new HubException("Client rejected monographs.");
|
throw new HubException("Client rejected monographs.");
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ namespace Notesnook.API.Models
|
|||||||
[JsonPropertyName("title")]
|
[JsonPropertyName("title")]
|
||||||
public string? Title { get; set; }
|
public string? Title { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("slug")]
|
||||||
|
public string? Slug { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("userId")]
|
[JsonPropertyName("userId")]
|
||||||
public string? UserId { get; set; }
|
public string? UserId { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using MongoDB.Bson;
|
|
||||||
using MongoDB.Bson.Serialization.Attributes;
|
|
||||||
|
|
||||||
namespace Notesnook.API.Models
|
namespace Notesnook.API.Models
|
||||||
{
|
{
|
||||||
@@ -37,6 +35,9 @@ namespace Notesnook.API.Models
|
|||||||
[JsonPropertyName("title")]
|
[JsonPropertyName("title")]
|
||||||
public string? Title { get; set; }
|
public string? Title { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("publishUrl")]
|
||||||
|
public string? PublishUrl { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("selfDestruct")]
|
[JsonPropertyName("selfDestruct")]
|
||||||
public bool SelfDestruct { get; set; }
|
public bool SelfDestruct { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ namespace Streetwriters.Common
|
|||||||
public static string? SUBSCRIPTIONS_CERT_KEY_PATH => ReadSecret("SUBSCRIPTIONS_CERT_KEY_PATH");
|
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[] NOTESNOOK_CORS_ORIGINS => ReadSecret("NOTESNOOK_CORS")?.Split(",") ?? [];
|
||||||
public static string? SIGNALR_REDIS_CONNECTION_STRING => ReadSecret("SIGNALR_REDIS_CONNECTION_STRING");
|
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)
|
public static string? ReadSecret(string name)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in new issue
Block a user