Kavita+ Overhaul & New Changelog (#3507)

This commit is contained in:
Joe Milazzo 2025-01-20 08:14:57 -06:00 committed by GitHub
parent d880c1690c
commit a5707617f2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
249 changed files with 14775 additions and 2300 deletions

View file

@ -1,10 +1,13 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using API.Constants;
using API.Data;
using API.DTOs.Account;
using API.DTOs.License;
using API.DTOs.KavitaPlus.License;
using API.Entities.Enums;
using API.Extensions;
using API.Services.Tasks;
using EasyCaching.Core;
using Flurl.Http;
using Kavita.Common;
@ -29,17 +32,20 @@ public interface ILicenseService
Task<bool> HasActiveLicense(bool forceCheck = false);
Task<bool> HasActiveSubscription(string? license);
Task<bool> ResetLicense(string license, string email);
Task<LicenseInfoDto?> GetLicenseInfo(bool forceCheck = false);
}
public class LicenseService(
IEasyCachingProviderFactory cachingProviderFactory,
IUnitOfWork unitOfWork,
ILogger<LicenseService> logger)
ILogger<LicenseService> logger,
IVersionUpdaterService versionUpdaterService)
: ILicenseService
{
private readonly TimeSpan _licenseCacheTimeout = TimeSpan.FromHours(8);
public const string Cron = "0 */4 * * *";
public const string Cron = "0 */9 * * *";
private const string CacheKey = "license";
private const string LicenseInfoCacheKey = "license-info";
/// <summary>
@ -53,13 +59,7 @@ public class LicenseService(
try
{
var response = await (Configuration.KavitaPlusApiUrl + "/api/license/check")
.WithHeader("Accept", "application/json")
.WithHeader("User-Agent", "Kavita")
.WithHeader("x-license-key", license)
.WithHeader("x-installId", HashUtil.ServerToken())
.WithHeader("x-kavita-version", BuildInfo.Version)
.WithHeader("Content-Type", "application/json")
.WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs))
.WithKavitaPlusHeaders(license)
.PostJsonAsync(new LicenseValidDto()
{
License = license,
@ -87,13 +87,7 @@ public class LicenseService(
try
{
var response = await (Configuration.KavitaPlusApiUrl + "/api/license/register")
.WithHeader("Accept", "application/json")
.WithHeader("User-Agent", "Kavita")
.WithHeader("x-license-key", license)
.WithHeader("x-installId", HashUtil.ServerToken())
.WithHeader("x-kavita-version", BuildInfo.Version)
.WithHeader("Content-Type", "application/json")
.WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs))
.WithKavitaPlusHeaders(license)
.PostJsonAsync(new EncryptLicenseDto()
{
License = license.Trim(),
@ -118,36 +112,6 @@ public class LicenseService(
}
}
/// <summary>
/// Checks licenses and updates cache
/// </summary>
/// <remarks>Expected to be called at startup and on reoccurring basis</remarks>
// public async Task ValidateLicenseStatus()
// {
// var provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License);
// try
// {
// var license = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey);
// if (string.IsNullOrEmpty(license.Value)) {
// await provider.SetAsync(CacheKey, false, _licenseCacheTimeout);
// return;
// }
//
// _logger.LogInformation("Validating Kavita+ License");
//
// await provider.FlushAsync();
// var isValid = await IsLicenseValid(license.Value);
// await provider.SetAsync(CacheKey, isValid, _licenseCacheTimeout);
//
// _logger.LogInformation("Validating Kavita+ License - Complete");
// }
// catch (Exception ex)
// {
// _logger.LogError(ex, "There was an error talking with Kavita+ API for license validation. Rescheduling check in 30 mins");
// await provider.SetAsync(CacheKey, false, _licenseCacheTimeout);
// BackgroundJob.Schedule(() => ValidateLicenseStatus(), TimeSpan.FromMinutes(30));
// }
// }
/// <summary>
/// Checks licenses and updates cache
@ -192,13 +156,7 @@ public class LicenseService(
try
{
var response = await (Configuration.KavitaPlusApiUrl + "/api/license/check-sub")
.WithHeader("Accept", "application/json")
.WithHeader("User-Agent", "Kavita")
.WithHeader("x-license-key", license)
.WithHeader("x-installId", HashUtil.ServerToken())
.WithHeader("x-kavita-version", BuildInfo.Version)
.WithHeader("Content-Type", "application/json")
.WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs))
.WithKavitaPlusHeaders(license)
.PostJsonAsync(new LicenseValidDto()
{
License = license,
@ -230,6 +188,8 @@ public class LicenseService(
var provider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License);
await provider.RemoveAsync(CacheKey);
}
public async Task AddLicense(string license, string email, string? discordId)
@ -251,13 +211,7 @@ public class LicenseService(
{
var encryptedLicense = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey);
var response = await (Configuration.KavitaPlusApiUrl + "/api/license/reset")
.WithHeader("Accept", "application/json")
.WithHeader("User-Agent", "Kavita")
.WithHeader("x-license-key", encryptedLicense.Value)
.WithHeader("x-installId", HashUtil.ServerToken())
.WithHeader("x-kavita-version", BuildInfo.Version)
.WithHeader("Content-Type", "application/json")
.WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs))
.WithKavitaPlusHeaders(encryptedLicense.Value)
.PostJsonAsync(new ResetLicenseDto()
{
License = license.Trim(),
@ -283,4 +237,67 @@ public class LicenseService(
return false;
}
/// <summary>
/// Fetches information about the license from Kavita+. If there is no license or an exception, will return null and can be assumed it is not active
/// </summary>
/// <param name="forceCheck"></param>
/// <returns></returns>
public async Task<LicenseInfoDto?> GetLicenseInfo(bool forceCheck = false)
{
// Check if there is a license
var hasLicense =
!string.IsNullOrEmpty((await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey))
.Value);
if (!hasLicense) return null;
// Check the cache
var licenseInfoProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.LicenseInfo);
if (!forceCheck)
{
var cacheValue = await licenseInfoProvider.GetAsync<LicenseInfoDto>(LicenseInfoCacheKey);
if (cacheValue.HasValue) return cacheValue.Value;
}
try
{
var encryptedLicense = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey);
var response = await (Configuration.KavitaPlusApiUrl + "/api/license/info")
.WithKavitaPlusHeaders(encryptedLicense.Value)
.GetJsonAsync<LicenseInfoDto>();
// This indicates a mismatch on installId or no active subscription
if (response == null) return null;
// Ensure that current version is within the 3 version limit. Don't count Nightly releases or Hotfixes
var releases = await versionUpdaterService.GetAllReleases();
response.IsValidVersion = releases
.Where(r => !r.UpdateTitle.Contains("Hotfix")) // We don't care about Hotfix releases
.Where(r => !r.IsPrerelease || BuildInfo.Version.IsWithinStableRelease(new Version(r.UpdateVersion))) // Ensure we don't take current nightlies within the current/last stable
.Take(3)
.All(r => new Version(r.UpdateVersion) <= BuildInfo.Version);
response.HasLicense = hasLicense;
// Cache if the license is valid here as well
var licenseProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License);
await licenseProvider.SetAsync(CacheKey, response.IsActive, _licenseCacheTimeout);
// Cache the license info if IsActive and ExpirationDate > DateTime.UtcNow + 2
if (response.IsActive && response.ExpirationDate > DateTime.UtcNow.AddDays(2))
{
await licenseInfoProvider.SetAsync(LicenseInfoCacheKey, response, _licenseCacheTimeout);
}
return response;
}
catch (FlurlHttpException e)
{
logger.LogError(e, "An error happened during the request to Kavita+ API");
}
return null;
}
}