forked from Kieran/void.cat
Postgres paywall support
Various fixes
This commit is contained in:
parent
3582431640
commit
e49c2fe870
@ -11,16 +11,16 @@ public class DownloadController : Controller
|
|||||||
{
|
{
|
||||||
private readonly IFileStore _storage;
|
private readonly IFileStore _storage;
|
||||||
private readonly IFileInfoManager _fileInfo;
|
private readonly IFileInfoManager _fileInfo;
|
||||||
private readonly IPaywallStore _paywall;
|
private readonly IPaywallOrderStore _paywallOrders;
|
||||||
private readonly ILogger<DownloadController> _logger;
|
private readonly ILogger<DownloadController> _logger;
|
||||||
|
|
||||||
public DownloadController(IFileStore storage, ILogger<DownloadController> logger, IFileInfoManager fileInfo,
|
public DownloadController(IFileStore storage, ILogger<DownloadController> logger, IFileInfoManager fileInfo,
|
||||||
IPaywallStore paywall)
|
IPaywallOrderStore paywall)
|
||||||
{
|
{
|
||||||
_storage = storage;
|
_storage = storage;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_fileInfo = fileInfo;
|
_fileInfo = fileInfo;
|
||||||
_paywall = paywall;
|
_paywallOrders = paywall;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpOptions]
|
[HttpOptions]
|
||||||
@ -90,7 +90,7 @@ public class DownloadController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check paywall
|
// check paywall
|
||||||
if (meta.Paywall != default && meta.Paywall.Service != PaywallServices.None)
|
if (meta.Paywall != default && meta.Paywall.Service != PaymentServices.None)
|
||||||
{
|
{
|
||||||
var orderId = Request.Headers.GetHeader("V-OrderId") ?? Request.Query["orderId"];
|
var orderId = Request.Headers.GetHeader("V-OrderId") ?? Request.Query["orderId"];
|
||||||
if (!await IsOrderPaid(orderId))
|
if (!await IsOrderPaid(orderId))
|
||||||
@ -111,7 +111,7 @@ public class DownloadController : Controller
|
|||||||
{
|
{
|
||||||
if (Guid.TryParse(orderId, out var oid))
|
if (Guid.TryParse(orderId, out var oid))
|
||||||
{
|
{
|
||||||
var order = await _paywall.GetOrder(oid);
|
var order = await _paywallOrders.Get(oid);
|
||||||
if (order?.Status == PaywallOrderStatus.Paid)
|
if (order?.Status == PaywallOrderStatus.Paid)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
|
@ -177,7 +177,7 @@ namespace VoidCat.Controllers
|
|||||||
var config = await _paywall.Get(gid);
|
var config = await _paywall.Get(gid);
|
||||||
|
|
||||||
var provider = await _paywallFactory.CreateProvider(config!.Service);
|
var provider = await _paywallFactory.CreateProvider(config!.Service);
|
||||||
return await provider.CreateOrder(file!);
|
return await provider.CreateOrder(file!.Paywall!);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -214,12 +214,17 @@ namespace VoidCat.Controllers
|
|||||||
|
|
||||||
if (req.Strike != default)
|
if (req.Strike != default)
|
||||||
{
|
{
|
||||||
await _paywall.Add(gid, req.Strike!);
|
await _paywall.Add(gid, new StrikePaywallConfig()
|
||||||
|
{
|
||||||
|
Service = PaymentServices.Strike,
|
||||||
|
Handle = req.Strike.Handle,
|
||||||
|
Cost = req.Strike.Cost
|
||||||
|
});
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
// if none set, set NoPaywallConfig
|
// if none set, delete config
|
||||||
await _paywall.Add(gid, new NoPaywallConfig());
|
await _paywall.Delete(gid);
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,4 +208,7 @@ public static class Extensions
|
|||||||
var hashParts = vu.Password.Split(":");
|
var hashParts = vu.Password.Split(":");
|
||||||
return vu.Password == password.Hash(hashParts[0], hashParts.Length == 3 ? hashParts[1] : null);
|
return vu.Password == password.Hash(hashParts[0], hashParts.Length == 3 ? hashParts[1] : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool HasPostgres(this VoidSettings settings)
|
||||||
|
=> !string.IsNullOrEmpty(settings.Postgres);
|
||||||
}
|
}
|
@ -1,16 +1,56 @@
|
|||||||
namespace VoidCat.Model.Paywall;
|
namespace VoidCat.Model.Paywall;
|
||||||
|
|
||||||
public enum PaywallServices
|
/// <summary>
|
||||||
|
/// Payment services supported by the system
|
||||||
|
/// </summary>
|
||||||
|
public enum PaymentServices
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// No service
|
||||||
|
/// </summary>
|
||||||
None,
|
None,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Strike.me payment service
|
||||||
|
/// </summary>
|
||||||
Strike
|
Strike
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract record PaywallConfig(PaywallServices Service, PaywallMoney Cost);
|
/// <summary>
|
||||||
|
/// Base paywall config
|
||||||
public record NoPaywallConfig() : PaywallConfig(PaywallServices.None, new PaywallMoney(0m, PaywallCurrencies.BTC));
|
/// </summary>
|
||||||
|
public abstract class PaywallConfig
|
||||||
public record StrikePaywallConfig(PaywallMoney Cost) : PaywallConfig(PaywallServices.Strike, Cost)
|
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// File this config is for
|
||||||
|
/// </summary>
|
||||||
|
public Guid File { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service used to pay the paywall
|
||||||
|
/// </summary>
|
||||||
|
public PaymentServices Service { get; init; } = PaymentServices.None;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The cost for the paywall to pass
|
||||||
|
/// </summary>
|
||||||
|
public PaywallMoney Cost { get; init; } = new(0m, PaywallCurrencies.BTC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public sealed class NoPaywallConfig : PaywallConfig
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Paywall config for <see cref="PaymentServices.Strike"/> service
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Cost"></param>
|
||||||
|
public sealed class StrikePaywallConfig : PaywallConfig
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Strike username to pay to
|
||||||
|
/// </summary>
|
||||||
public string Handle { get; init; } = null!;
|
public string Handle { get; init; } = null!;
|
||||||
}
|
}
|
@ -1,13 +1,69 @@
|
|||||||
namespace VoidCat.Model.Paywall;
|
namespace VoidCat.Model.Paywall;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Status of paywall order
|
||||||
|
/// </summary>
|
||||||
public enum PaywallOrderStatus : byte
|
public enum PaywallOrderStatus : byte
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Invoice is not paid yet
|
||||||
|
/// </summary>
|
||||||
Unpaid,
|
Unpaid,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoice is paid
|
||||||
|
/// </summary>
|
||||||
Paid,
|
Paid,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoice has expired and cant be paid
|
||||||
|
/// </summary>
|
||||||
Expired
|
Expired
|
||||||
}
|
}
|
||||||
|
|
||||||
public record PaywallOrder(Guid Id, PaywallMoney Price, PaywallOrderStatus Status);
|
/// <summary>
|
||||||
|
/// Base paywall order
|
||||||
|
/// </summary>
|
||||||
|
public class PaywallOrder
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Unique id of the order
|
||||||
|
/// </summary>
|
||||||
|
public Guid Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// File id this order is for
|
||||||
|
/// </summary>
|
||||||
|
public Guid File { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service used to generate this order
|
||||||
|
/// </summary>
|
||||||
|
public PaymentServices Service { get; init; }
|
||||||
|
|
||||||
public record LightningPaywallOrder(Guid Id, PaywallMoney Price, PaywallOrderStatus Status, string LnInvoice,
|
/// <summary>
|
||||||
DateTimeOffset Expire) : PaywallOrder(Id, Price, Status);
|
/// The price of the order
|
||||||
|
/// </summary>
|
||||||
|
public PaywallMoney Price { get; init; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current status of the order
|
||||||
|
/// </summary>
|
||||||
|
public PaywallOrderStatus Status { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A paywall order lightning network invoice
|
||||||
|
/// </summary>
|
||||||
|
public class LightningPaywallOrder : PaywallOrder
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Lightning invoice
|
||||||
|
/// </summary>
|
||||||
|
public string Invoice { get; init; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Expire time of the order
|
||||||
|
/// </summary>
|
||||||
|
public DateTime Expire { get; init; }
|
||||||
|
}
|
@ -132,6 +132,7 @@ services.AddAuthorization((opt) =>
|
|||||||
//
|
//
|
||||||
services.AddTransient<RazorPartialToStringRenderer>();
|
services.AddTransient<RazorPartialToStringRenderer>();
|
||||||
services.AddTransient<IMigration, PopulateMetadataId>();
|
services.AddTransient<IMigration, PopulateMetadataId>();
|
||||||
|
services.AddTransient<IMigration, MigrateToPostgres>();
|
||||||
|
|
||||||
// file storage
|
// file storage
|
||||||
services.AddStorage(voidSettings);
|
services.AddStorage(voidSettings);
|
||||||
@ -141,7 +142,7 @@ services.AddTransient<IAggregateStatsCollector, AggregateStatsCollector>();
|
|||||||
services.AddTransient<IStatsCollector, PrometheusStatsCollector>();
|
services.AddTransient<IStatsCollector, PrometheusStatsCollector>();
|
||||||
|
|
||||||
// paywall
|
// paywall
|
||||||
services.AddVoidPaywall();
|
services.AddPaywallServices(voidSettings);
|
||||||
|
|
||||||
// users
|
// users
|
||||||
services.AddUserServices(voidSettings);
|
services.AddUserServices(voidSettings);
|
||||||
|
@ -4,5 +4,5 @@ namespace VoidCat.Services.Abstractions;
|
|||||||
|
|
||||||
public interface IPaywallFactory
|
public interface IPaywallFactory
|
||||||
{
|
{
|
||||||
ValueTask<IPaywallProvider> CreateProvider(PaywallServices svc);
|
ValueTask<IPaywallProvider> CreateProvider(PaymentServices svc);
|
||||||
}
|
}
|
17
VoidCat/Services/Abstractions/IPaywallOrderStore.cs
Normal file
17
VoidCat/Services/Abstractions/IPaywallOrderStore.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
using VoidCat.Model.Paywall;
|
||||||
|
|
||||||
|
namespace VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Paywall order store
|
||||||
|
/// </summary>
|
||||||
|
public interface IPaywallOrderStore : IBasicStore<PaywallOrder>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Update the status of an order
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="order"></param>
|
||||||
|
/// <param name="status"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
ValueTask UpdateStatus(Guid order, PaywallOrderStatus status);
|
||||||
|
}
|
@ -1,11 +1,23 @@
|
|||||||
using VoidCat.Model;
|
|
||||||
using VoidCat.Model.Paywall;
|
using VoidCat.Model.Paywall;
|
||||||
|
|
||||||
namespace VoidCat.Services.Abstractions;
|
namespace VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provider to generate orders for a specific config
|
||||||
|
/// </summary>
|
||||||
public interface IPaywallProvider
|
public interface IPaywallProvider
|
||||||
{
|
{
|
||||||
ValueTask<PaywallOrder?> CreateOrder(PublicVoidFile file);
|
/// <summary>
|
||||||
|
/// Create an order with the provider
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="file"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
ValueTask<PaywallOrder?> CreateOrder(PaywallConfig file);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the status of an existing order with the provider
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id"></param>
|
||||||
|
/// <returns></returns>
|
||||||
ValueTask<PaywallOrder?> GetOrderStatus(Guid id);
|
ValueTask<PaywallOrder?> GetOrderStatus(Guid id);
|
||||||
}
|
}
|
@ -2,8 +2,9 @@ using VoidCat.Model.Paywall;
|
|||||||
|
|
||||||
namespace VoidCat.Services.Abstractions;
|
namespace VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Store for paywall configs
|
||||||
|
/// </summary>
|
||||||
public interface IPaywallStore : IBasicStore<PaywallConfig>
|
public interface IPaywallStore : IBasicStore<PaywallConfig>
|
||||||
{
|
{
|
||||||
ValueTask<PaywallOrder?> GetOrder(Guid id);
|
|
||||||
ValueTask SaveOrder(PaywallOrder order);
|
|
||||||
}
|
}
|
@ -8,13 +8,11 @@ namespace VoidCat.Services.Files;
|
|||||||
public class LocalDiskFileMetadataStore : IFileMetadataStore
|
public class LocalDiskFileMetadataStore : IFileMetadataStore
|
||||||
{
|
{
|
||||||
private const string MetadataDir = "metadata-v3";
|
private const string MetadataDir = "metadata-v3";
|
||||||
private readonly ILogger<LocalDiskFileMetadataStore> _logger;
|
|
||||||
private readonly VoidSettings _settings;
|
private readonly VoidSettings _settings;
|
||||||
|
|
||||||
public LocalDiskFileMetadataStore(VoidSettings settings, ILogger<LocalDiskFileMetadataStore> logger)
|
public LocalDiskFileMetadataStore(VoidSettings settings)
|
||||||
{
|
{
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
_logger = logger;
|
|
||||||
|
|
||||||
var metaPath = Path.Combine(_settings.DataDirectory, MetadataDir);
|
var metaPath = Path.Combine(_settings.DataDirectory, MetadataDir);
|
||||||
if (!Directory.Exists(metaPath))
|
if (!Directory.Exists(metaPath))
|
||||||
@ -134,7 +132,6 @@ public class LocalDiskFileMetadataStore : IFileMetadataStore
|
|||||||
var path = MapMeta(id);
|
var path = MapMeta(id);
|
||||||
if (File.Exists(path))
|
if (File.Exists(path))
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Deleting metadata file {Path}", path);
|
|
||||||
File.Delete(path);
|
File.Delete(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using VoidCat.Services.Abstractions;
|
using VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
namespace VoidCat.Services.InMemory;
|
namespace VoidCat.Services.InMemory;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public class InMemoryCache : ICache
|
public class InMemoryCache : ICache
|
||||||
{
|
{
|
||||||
private readonly IMemoryCache _cache;
|
private readonly IMemoryCache _cache;
|
||||||
@ -12,30 +14,38 @@ public class InMemoryCache : ICache
|
|||||||
_cache = cache;
|
_cache = cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public ValueTask<T?> Get<T>(string key)
|
public ValueTask<T?> Get<T>(string key)
|
||||||
{
|
{
|
||||||
return ValueTask.FromResult(_cache.Get<T?>(key));
|
var json = _cache.Get<string>(key);
|
||||||
|
if (string.IsNullOrEmpty(json)) return default;
|
||||||
|
|
||||||
|
return ValueTask.FromResult(JsonConvert.DeserializeObject<T?>(json));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public ValueTask Set<T>(string key, T value, TimeSpan? expire = null)
|
public ValueTask Set<T>(string key, T value, TimeSpan? expire = null)
|
||||||
{
|
{
|
||||||
|
var json = JsonConvert.SerializeObject(value);
|
||||||
if (expire.HasValue)
|
if (expire.HasValue)
|
||||||
{
|
{
|
||||||
_cache.Set(key, value, expire.Value);
|
_cache.Set(key, json, expire.Value);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_cache.Set(key, value);
|
_cache.Set(key, json);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ValueTask.CompletedTask;
|
return ValueTask.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public ValueTask<string[]> GetList(string key)
|
public ValueTask<string[]> GetList(string key)
|
||||||
{
|
{
|
||||||
return ValueTask.FromResult(_cache.Get<string[]>(key) ?? Array.Empty<string>());
|
return ValueTask.FromResult(_cache.Get<string[]>(key) ?? Array.Empty<string>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public ValueTask AddToList(string key, string value)
|
public ValueTask AddToList(string key, string value)
|
||||||
{
|
{
|
||||||
var list = new HashSet<string>(GetList(key).Result);
|
var list = new HashSet<string>(GetList(key).Result);
|
||||||
@ -44,6 +54,7 @@ public class InMemoryCache : ICache
|
|||||||
return ValueTask.CompletedTask;
|
return ValueTask.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public ValueTask RemoveFromList(string key, string value)
|
public ValueTask RemoveFromList(string key, string value)
|
||||||
{
|
{
|
||||||
var list = new HashSet<string>(GetList(key).Result);
|
var list = new HashSet<string>(GetList(key).Result);
|
||||||
@ -52,10 +63,10 @@ public class InMemoryCache : ICache
|
|||||||
return ValueTask.CompletedTask;
|
return ValueTask.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public ValueTask Delete(string key)
|
public ValueTask Delete(string key)
|
||||||
{
|
{
|
||||||
_cache.Remove(key);
|
_cache.Remove(key);
|
||||||
return ValueTask.CompletedTask;
|
return ValueTask.CompletedTask;
|
||||||
;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -39,14 +39,27 @@ public class Init : Migration
|
|||||||
|
|
||||||
Create.Table("Paywall")
|
Create.Table("Paywall")
|
||||||
.WithColumn("File").AsGuid().ForeignKey("Files", "Id").OnDelete(Rule.Cascade).PrimaryKey()
|
.WithColumn("File").AsGuid().ForeignKey("Files", "Id").OnDelete(Rule.Cascade).PrimaryKey()
|
||||||
.WithColumn("Type").AsInt16()
|
.WithColumn("Service").AsInt16()
|
||||||
.WithColumn("Currency").AsInt16()
|
.WithColumn("Currency").AsInt16()
|
||||||
.WithColumn("Amount").AsDecimal();
|
.WithColumn("Amount").AsDecimal();
|
||||||
|
|
||||||
Create.Table("PaywallStrike")
|
Create.Table("PaywallStrike")
|
||||||
.WithColumn("File").AsGuid().ForeignKey("Files", "Id").OnDelete(Rule.Cascade).PrimaryKey()
|
.WithColumn("File").AsGuid().ForeignKey("Paywall", "File").OnDelete(Rule.Cascade).PrimaryKey()
|
||||||
.WithColumn("Handle").AsString();
|
.WithColumn("Handle").AsString();
|
||||||
|
|
||||||
|
Create.Table("PaywallOrder")
|
||||||
|
.WithColumn("Id").AsGuid().PrimaryKey()
|
||||||
|
.WithColumn("File").AsGuid().ForeignKey("Files", "Id").OnDelete(Rule.Cascade).Indexed()
|
||||||
|
.WithColumn("Service").AsInt16()
|
||||||
|
.WithColumn("Currency").AsInt16()
|
||||||
|
.WithColumn("Amount").AsDecimal()
|
||||||
|
.WithColumn("Status").AsInt16().Indexed();
|
||||||
|
|
||||||
|
Create.Table("PaywallOrderLightning")
|
||||||
|
.WithColumn("Order").AsGuid().ForeignKey("PaywallOrder", "Id").OnDelete(Rule.Cascade).PrimaryKey()
|
||||||
|
.WithColumn("Invoice").AsString()
|
||||||
|
.WithColumn("Expire").AsDateTimeOffset();
|
||||||
|
|
||||||
Create.Table("UserRoles")
|
Create.Table("UserRoles")
|
||||||
.WithColumn("User").AsGuid().ForeignKey("Users", "Id").OnDelete(Rule.Cascade).Indexed()
|
.WithColumn("User").AsGuid().ForeignKey("Users", "Id").OnDelete(Rule.Cascade).Indexed()
|
||||||
.WithColumn("Role").AsString().NotNullable();
|
.WithColumn("Role").AsString().NotNullable();
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
using VoidCat.Model;
|
|
||||||
using VoidCat.Services.Abstractions;
|
|
||||||
using VoidCat.Services.Files;
|
|
||||||
|
|
||||||
namespace VoidCat.Services.Migrations;
|
|
||||||
|
|
||||||
public class LocalDiskToPostgres : IMigration
|
|
||||||
{
|
|
||||||
private readonly ILogger<LocalDiskToPostgres> _logger;
|
|
||||||
private readonly ILoggerFactory _loggerFactory;
|
|
||||||
private readonly VoidSettings _settings;
|
|
||||||
|
|
||||||
public LocalDiskToPostgres(VoidSettings settings, ILoggerFactory loggerFactory, IFileInfoManager fileInfoManager,
|
|
||||||
IUserUploadsStore userUploadsStore, IAggregateStatsCollector statsCollector)
|
|
||||||
{
|
|
||||||
_logger = loggerFactory.CreateLogger<LocalDiskToPostgres>();
|
|
||||||
_settings = settings;
|
|
||||||
_loggerFactory = loggerFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask<IMigration.MigrationResult> Migrate(string[] args)
|
|
||||||
{
|
|
||||||
if (!args.Contains("--migrate-local-to-postgres"))
|
|
||||||
{
|
|
||||||
return IMigration.MigrationResult.Skipped;
|
|
||||||
}
|
|
||||||
|
|
||||||
var metaStore =
|
|
||||||
new LocalDiskFileMetadataStore(_settings, _loggerFactory.CreateLogger<LocalDiskFileMetadataStore>());
|
|
||||||
|
|
||||||
var files = await metaStore.ListFiles<SecretVoidFileMeta>(new(0, Int32.MaxValue));
|
|
||||||
await foreach (var file in files.Results)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Migrating file {File}", file.Id);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return IMigration.MigrationResult.ExitCompleted;
|
|
||||||
}
|
|
||||||
}
|
|
86
VoidCat/Services/Migrations/MigrateToPostgres.cs
Normal file
86
VoidCat/Services/Migrations/MigrateToPostgres.cs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
using VoidCat.Model;
|
||||||
|
using VoidCat.Services.Abstractions;
|
||||||
|
using VoidCat.Services.Files;
|
||||||
|
using VoidCat.Services.Paywall;
|
||||||
|
|
||||||
|
namespace VoidCat.Services.Migrations;
|
||||||
|
|
||||||
|
public class MigrateToPostgres : IMigration
|
||||||
|
{
|
||||||
|
private readonly ILogger<MigrateToPostgres> _logger;
|
||||||
|
private readonly VoidSettings _settings;
|
||||||
|
private readonly IFileMetadataStore _fileMetadata;
|
||||||
|
private readonly ICache _cache;
|
||||||
|
private readonly IPaywallStore _paywallStore;
|
||||||
|
|
||||||
|
public MigrateToPostgres(VoidSettings settings, ILogger<MigrateToPostgres> logger, IFileMetadataStore fileMetadata,
|
||||||
|
ICache cache, IPaywallStore paywallStore)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_settings = settings;
|
||||||
|
_fileMetadata = fileMetadata;
|
||||||
|
_cache = cache;
|
||||||
|
_paywallStore = paywallStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<IMigration.MigrationResult> Migrate(string[] args)
|
||||||
|
{
|
||||||
|
if (args.Contains("--migrate-local-metadata-to-postgres"))
|
||||||
|
{
|
||||||
|
await MigrateFiles();
|
||||||
|
return IMigration.MigrationResult.ExitCompleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.Contains("--migrate-cache-paywall-to-postgres"))
|
||||||
|
{
|
||||||
|
await MigratePaywall();
|
||||||
|
return IMigration.MigrationResult.ExitCompleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
return IMigration.MigrationResult.Skipped;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task MigrateFiles()
|
||||||
|
{
|
||||||
|
var localDiskMetaStore =
|
||||||
|
new LocalDiskFileMetadataStore(_settings);
|
||||||
|
|
||||||
|
var files = await localDiskMetaStore.ListFiles<SecretVoidFileMeta>(new(0, int.MaxValue));
|
||||||
|
await foreach (var file in files.Results)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _fileMetadata.Set(file.Id, file);
|
||||||
|
await localDiskMetaStore.Delete(file.Id);
|
||||||
|
_logger.LogInformation("Migrated file metadata for {File}", file.Id);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to migrate file metadata for {File}", file.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task MigratePaywall()
|
||||||
|
{
|
||||||
|
var cachePaywallStore = new CachePaywallStore(_cache);
|
||||||
|
|
||||||
|
var files = await _fileMetadata.ListFiles<VoidFileMeta>(new(0, int.MaxValue));
|
||||||
|
await foreach (var file in files.Results)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var old = await cachePaywallStore.Get(file.Id);
|
||||||
|
if (old != default)
|
||||||
|
{
|
||||||
|
await _paywallStore.Add(file.Id, old);
|
||||||
|
_logger.LogInformation("Migrated paywall config for {File}", file.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to migrate paywall config for {File}", file.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
VoidCat/Services/Paywall/CachePaywallOrderStore.cs
Normal file
26
VoidCat/Services/Paywall/CachePaywallOrderStore.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using VoidCat.Model.Paywall;
|
||||||
|
using VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
|
namespace VoidCat.Services.Paywall;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="IPaywallOrderStore"/>
|
||||||
|
public class CachePaywallOrderStore : BasicCacheStore<PaywallOrder>, IPaywallOrderStore
|
||||||
|
{
|
||||||
|
public CachePaywallOrderStore(ICache cache) : base(cache)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async ValueTask UpdateStatus(Guid order, PaywallOrderStatus status)
|
||||||
|
{
|
||||||
|
var old = await Get(order);
|
||||||
|
if (old == default) return;
|
||||||
|
|
||||||
|
old.Status = status;
|
||||||
|
|
||||||
|
await Add(order, old);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override string MapKey(Guid id) => $"paywall:order:{id}";
|
||||||
|
}
|
28
VoidCat/Services/Paywall/CachePaywallStore.cs
Normal file
28
VoidCat/Services/Paywall/CachePaywallStore.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using VoidCat.Model.Paywall;
|
||||||
|
using VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
|
namespace VoidCat.Services.Paywall;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="IPaywallStore"/>
|
||||||
|
public class CachePaywallStore : BasicCacheStore<PaywallConfig>, IPaywallStore
|
||||||
|
{
|
||||||
|
public CachePaywallStore(ICache database)
|
||||||
|
: base(database)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override async ValueTask<PaywallConfig?> Get(Guid id)
|
||||||
|
{
|
||||||
|
var cfg = await Cache.Get<NoPaywallConfig>(MapKey(id));
|
||||||
|
return cfg?.Service switch
|
||||||
|
{
|
||||||
|
PaymentServices.None => cfg,
|
||||||
|
PaymentServices.Strike => await Cache.Get<StrikePaywallConfig>(MapKey(id)),
|
||||||
|
_ => default
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override string MapKey(Guid id) => $"paywall:config:{id}";
|
||||||
|
}
|
@ -1,6 +1,5 @@
|
|||||||
using VoidCat.Model.Paywall;
|
using VoidCat.Model.Paywall;
|
||||||
using VoidCat.Services.Abstractions;
|
using VoidCat.Services.Abstractions;
|
||||||
using VoidCat.Services.Strike;
|
|
||||||
|
|
||||||
namespace VoidCat.Services.Paywall;
|
namespace VoidCat.Services.Paywall;
|
||||||
|
|
||||||
@ -13,26 +12,12 @@ public class PaywallFactory : IPaywallFactory
|
|||||||
_services = services;
|
_services = services;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValueTask<IPaywallProvider> CreateProvider(PaywallServices svc)
|
public ValueTask<IPaywallProvider> CreateProvider(PaymentServices svc)
|
||||||
{
|
{
|
||||||
return ValueTask.FromResult<IPaywallProvider>(svc switch
|
return ValueTask.FromResult<IPaywallProvider>(svc switch
|
||||||
{
|
{
|
||||||
PaywallServices.Strike => _services.GetRequiredService<StrikePaywallProvider>(),
|
PaymentServices.Strike => _services.GetRequiredService<StrikePaywallProvider>(),
|
||||||
_ => throw new ArgumentException("Must have a paywall config", nameof(svc))
|
_ => throw new ArgumentException("Must have a paywall config", nameof(svc))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public static class Paywall
|
|
||||||
{
|
|
||||||
public static void AddVoidPaywall(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
services.AddTransient<IPaywallFactory, PaywallFactory>();
|
|
||||||
services.AddTransient<IPaywallStore, PaywallStore>();
|
|
||||||
|
|
||||||
// strike
|
|
||||||
services.AddTransient<StrikeApi>();
|
|
||||||
services.AddTransient<StrikePaywallProvider>();
|
|
||||||
services.AddTransient<IPaywallProvider>((svc) => svc.GetRequiredService<StrikePaywallProvider>());
|
|
||||||
}
|
|
||||||
}
|
}
|
32
VoidCat/Services/Paywall/PaywallStartup.cs
Normal file
32
VoidCat/Services/Paywall/PaywallStartup.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
using VoidCat.Model;
|
||||||
|
using VoidCat.Services.Abstractions;
|
||||||
|
using VoidCat.Services.Strike;
|
||||||
|
|
||||||
|
namespace VoidCat.Services.Paywall;
|
||||||
|
|
||||||
|
public static class PaywallStartup
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Add services required to use paywall functions
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services"></param>
|
||||||
|
/// <param name="settings"></param>
|
||||||
|
public static void AddPaywallServices(this IServiceCollection services, VoidSettings settings)
|
||||||
|
{
|
||||||
|
services.AddTransient<IPaywallFactory, PaywallFactory>();
|
||||||
|
if (settings.HasPostgres())
|
||||||
|
{
|
||||||
|
services.AddTransient<IPaywallStore, PostgresPaywallStore>();
|
||||||
|
services.AddTransient<IPaywallOrderStore, PostgresPaywallOrderStore>();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
services.AddTransient<IPaywallStore, CachePaywallStore>();
|
||||||
|
services.AddTransient<IPaywallOrderStore, CachePaywallOrderStore>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// strike
|
||||||
|
services.AddTransient<StrikeApi>();
|
||||||
|
services.AddTransient<StrikePaywallProvider>();
|
||||||
|
}
|
||||||
|
}
|
@ -1,38 +0,0 @@
|
|||||||
using VoidCat.Model.Paywall;
|
|
||||||
using VoidCat.Services.Abstractions;
|
|
||||||
|
|
||||||
namespace VoidCat.Services.Paywall;
|
|
||||||
|
|
||||||
public class PaywallStore : BasicCacheStore<PaywallConfig>, IPaywallStore
|
|
||||||
{
|
|
||||||
public PaywallStore(ICache database)
|
|
||||||
: base(database)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override async ValueTask<PaywallConfig?> Get(Guid id)
|
|
||||||
{
|
|
||||||
var cfg = await Cache.Get<NoPaywallConfig>(MapKey(id));
|
|
||||||
return cfg?.Service switch
|
|
||||||
{
|
|
||||||
PaywallServices.None => cfg,
|
|
||||||
PaywallServices.Strike => await Cache.Get<StrikePaywallConfig>(MapKey(id)),
|
|
||||||
_ => default
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask<PaywallOrder?> GetOrder(Guid id)
|
|
||||||
{
|
|
||||||
return await Cache.Get<PaywallOrder>(OrderKey(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
public ValueTask SaveOrder(PaywallOrder order)
|
|
||||||
{
|
|
||||||
return Cache.Set(OrderKey(order.Id), order,
|
|
||||||
order.Status == PaywallOrderStatus.Paid ? TimeSpan.FromDays(1) : TimeSpan.FromSeconds(5));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override string MapKey(Guid id) => $"paywall:config:{id}";
|
|
||||||
private string OrderKey(Guid id) => $"paywall:order:{id}";
|
|
||||||
}
|
|
104
VoidCat/Services/Paywall/PostgresPaywallOrderStore.cs
Normal file
104
VoidCat/Services/Paywall/PostgresPaywallOrderStore.cs
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
using Dapper;
|
||||||
|
using VoidCat.Model.Paywall;
|
||||||
|
using VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
|
namespace VoidCat.Services.Paywall;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public class PostgresPaywallOrderStore : IPaywallOrderStore
|
||||||
|
{
|
||||||
|
private readonly PostgresConnectionFactory _connection;
|
||||||
|
|
||||||
|
public PostgresPaywallOrderStore(PostgresConnectionFactory connection)
|
||||||
|
{
|
||||||
|
_connection = connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async ValueTask<PaywallOrder?> Get(Guid id)
|
||||||
|
{
|
||||||
|
await using var conn = await _connection.Get();
|
||||||
|
var order = await conn.QuerySingleOrDefaultAsync<DtoPaywallOrder>(
|
||||||
|
@"select * from ""PaywallOrder"" where ""Id"" = :id", new {id});
|
||||||
|
if (order.Service is PaymentServices.Strike)
|
||||||
|
{
|
||||||
|
var lnDetails = await conn.QuerySingleAsync<LightningPaywallOrder>(
|
||||||
|
@"select * from ""PaywallOrderLightning"" where ""Order"" = :id", new
|
||||||
|
{
|
||||||
|
id = order.Id
|
||||||
|
});
|
||||||
|
return new LightningPaywallOrder
|
||||||
|
{
|
||||||
|
Id = order.Id,
|
||||||
|
File = order.File,
|
||||||
|
Price = new(order.Amount, order.Currency),
|
||||||
|
Service = order.Service,
|
||||||
|
Status = order.Status,
|
||||||
|
Invoice = lnDetails.Invoice,
|
||||||
|
Expire = lnDetails.Expire
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ValueTask<IReadOnlyList<PaywallOrder>> Get(Guid[] ids)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async ValueTask Add(Guid id, PaywallOrder obj)
|
||||||
|
{
|
||||||
|
await using var conn = await _connection.Get();
|
||||||
|
await using var txn = await conn.BeginTransactionAsync();
|
||||||
|
await conn.ExecuteAsync(
|
||||||
|
@"insert into ""PaywallOrder""(""Id"", ""File"", ""Service"", ""Currency"", ""Amount"", ""Status"")
|
||||||
|
values(:id, :file, :service, :currency, :amt, :status)",
|
||||||
|
new
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
file = obj.File,
|
||||||
|
service = (int) obj.Service,
|
||||||
|
currency = (int) obj.Price.Currency,
|
||||||
|
amt = obj.Price.Amount, // :amount wasn't working?
|
||||||
|
status = (int) obj.Status
|
||||||
|
});
|
||||||
|
|
||||||
|
if (obj is LightningPaywallOrder ln)
|
||||||
|
{
|
||||||
|
await conn.ExecuteAsync(
|
||||||
|
@"insert into ""PaywallOrderLightning""(""Order"", ""Invoice"", ""Expire"") values(:order, :invoice, :expire)",
|
||||||
|
new
|
||||||
|
{
|
||||||
|
order = id,
|
||||||
|
invoice = ln.Invoice,
|
||||||
|
expire = ln.Expire.ToUniversalTime()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await txn.CommitAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async ValueTask Delete(Guid id)
|
||||||
|
{
|
||||||
|
await using var conn = await _connection.Get();
|
||||||
|
await conn.ExecuteAsync(@"delete from ""PaywallOrder"" where ""Id"" = :id", new {id});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async ValueTask UpdateStatus(Guid order, PaywallOrderStatus status)
|
||||||
|
{
|
||||||
|
await using var conn = await _connection.Get();
|
||||||
|
await conn.ExecuteAsync(@"update ""PaywallOrder"" set ""Status"" = :status where ""Id"" = :id",
|
||||||
|
new {id = order, status = (int) status});
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class DtoPaywallOrder : PaywallOrder
|
||||||
|
{
|
||||||
|
public PaywallCurrencies Currency { get; init; }
|
||||||
|
public decimal Amount { get; init; }
|
||||||
|
}
|
||||||
|
}
|
93
VoidCat/Services/Paywall/PostgresPaywallStore.cs
Normal file
93
VoidCat/Services/Paywall/PostgresPaywallStore.cs
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
using Dapper;
|
||||||
|
using VoidCat.Model.Paywall;
|
||||||
|
using VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
|
namespace VoidCat.Services.Paywall;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public sealed class PostgresPaywallStore : IPaywallStore
|
||||||
|
{
|
||||||
|
private readonly PostgresConnectionFactory _connection;
|
||||||
|
|
||||||
|
public PostgresPaywallStore(PostgresConnectionFactory connection)
|
||||||
|
{
|
||||||
|
_connection = connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async ValueTask<PaywallConfig?> Get(Guid id)
|
||||||
|
{
|
||||||
|
await using var conn = await _connection.Get();
|
||||||
|
var svc = await conn.QuerySingleOrDefaultAsync<DtoPaywallConfig>(
|
||||||
|
@"select * from ""Paywall"" where ""File"" = :file", new {file = id});
|
||||||
|
if (svc != default)
|
||||||
|
{
|
||||||
|
switch (svc.Service)
|
||||||
|
{
|
||||||
|
case PaymentServices.Strike:
|
||||||
|
{
|
||||||
|
var handle =
|
||||||
|
await conn.ExecuteScalarAsync<string>(
|
||||||
|
@"select ""Handle"" from ""PaywallStrike"" where ""File"" = :file", new {file = id});
|
||||||
|
return new StrikePaywallConfig
|
||||||
|
{
|
||||||
|
Cost = new(svc.Amount, svc.Currency),
|
||||||
|
File = svc.File,
|
||||||
|
Handle = handle,
|
||||||
|
Service = PaymentServices.Strike
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ValueTask<IReadOnlyList<PaywallConfig>> Get(Guid[] ids)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async ValueTask Add(Guid id, PaywallConfig obj)
|
||||||
|
{
|
||||||
|
await using var conn = await _connection.Get();
|
||||||
|
await using var txn = await conn.BeginTransactionAsync();
|
||||||
|
await conn.ExecuteAsync(
|
||||||
|
@"insert into ""Paywall""(""File"", ""Service"", ""Amount"", ""Currency"") values(:file, :service, :amount, :currency)
|
||||||
|
on conflict(""File"") do update set ""Service"" = :service, ""Amount"" = :amount, ""Currency"" = :currency",
|
||||||
|
new
|
||||||
|
{
|
||||||
|
file = id,
|
||||||
|
service = (int)obj.Service,
|
||||||
|
amount = obj.Cost.Amount,
|
||||||
|
currency = obj.Cost.Currency
|
||||||
|
});
|
||||||
|
|
||||||
|
if (obj is StrikePaywallConfig sc)
|
||||||
|
{
|
||||||
|
await conn.ExecuteAsync(@"insert into ""PaywallStrike""(""File"", ""Handle"") values(:file, :handle)
|
||||||
|
on conflict(""File"") do update set ""Handle"" = :handle", new
|
||||||
|
{
|
||||||
|
file = id,
|
||||||
|
handle = sc.Handle
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await txn.CommitAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async ValueTask Delete(Guid id)
|
||||||
|
{
|
||||||
|
await using var conn = await _connection.Get();
|
||||||
|
await conn.ExecuteAsync(@"delete from ""Paywall"" where ""File"" = :file", new {file = id});
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class DtoPaywallConfig : PaywallConfig
|
||||||
|
{
|
||||||
|
public PaywallCurrencies Currency { get; init; }
|
||||||
|
public decimal Amount { get; init; }
|
||||||
|
}
|
||||||
|
}
|
@ -6,31 +6,31 @@ using VoidCat.Services.Strike;
|
|||||||
|
|
||||||
namespace VoidCat.Services.Paywall;
|
namespace VoidCat.Services.Paywall;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public class StrikePaywallProvider : IPaywallProvider
|
public class StrikePaywallProvider : IPaywallProvider
|
||||||
{
|
{
|
||||||
private readonly ILogger<StrikePaywallProvider> _logger;
|
private readonly ILogger<StrikePaywallProvider> _logger;
|
||||||
private readonly StrikeApi _strike;
|
private readonly StrikeApi _strike;
|
||||||
private readonly IPaywallStore _store;
|
private readonly IPaywallOrderStore _orderStore;
|
||||||
|
|
||||||
public StrikePaywallProvider(ILogger<StrikePaywallProvider> logger, StrikeApi strike, IPaywallStore store)
|
public StrikePaywallProvider(ILogger<StrikePaywallProvider> logger, StrikeApi strike, IPaywallOrderStore orderStore)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_strike = strike;
|
_strike = strike;
|
||||||
_store = store;
|
_orderStore = orderStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<PaywallOrder?> CreateOrder(PublicVoidFile file)
|
/// <inheritdoc />
|
||||||
|
public async ValueTask<PaywallOrder?> CreateOrder(PaywallConfig config)
|
||||||
{
|
{
|
||||||
IsStrikePaywall(file.Paywall, out var strikeConfig);
|
IsStrikePaywall(config, out var strikeConfig);
|
||||||
var config = file.Paywall!;
|
|
||||||
|
|
||||||
_logger.LogInformation("Generating invoice for {Currency} {Amount}", config.Cost.Currency, config.Cost.Amount);
|
_logger.LogInformation("Generating invoice for {Currency} {Amount}", config.Cost.Currency, config.Cost.Amount);
|
||||||
|
|
||||||
var currency = MapCurrency(strikeConfig!.Cost.Currency);
|
var currency = MapCurrency(strikeConfig.Cost.Currency);
|
||||||
if (currency == Currencies.USD)
|
if (currency == Currencies.USD)
|
||||||
{
|
{
|
||||||
// map USD to USDT if USD is not available and USDT is
|
// map USD to USDT if USD is not available and USDT is
|
||||||
var profile = await _strike.GetProfile(strikeConfig!.Handle);
|
var profile = await _strike.GetProfile(strikeConfig.Handle);
|
||||||
if (profile != default)
|
if (profile != default)
|
||||||
{
|
{
|
||||||
var usd = profile.Currencies.FirstOrDefault(a => a.Currency == Currencies.USD);
|
var usd = profile.Currencies.FirstOrDefault(a => a.Currency == Currencies.USD);
|
||||||
@ -50,44 +50,70 @@ public class StrikePaywallProvider : IPaywallProvider
|
|||||||
Amount = strikeConfig.Cost.Amount.ToString(CultureInfo.InvariantCulture),
|
Amount = strikeConfig.Cost.Amount.ToString(CultureInfo.InvariantCulture),
|
||||||
Currency = currency
|
Currency = currency
|
||||||
},
|
},
|
||||||
Description = file.Metadata?.Name
|
Description = config.File.ToBase58()
|
||||||
});
|
});
|
||||||
if (invoice != default)
|
if (invoice != default)
|
||||||
{
|
{
|
||||||
var quote = await _strike.GetInvoiceQuote(invoice.InvoiceId);
|
var quote = await _strike.GetInvoiceQuote(invoice.InvoiceId);
|
||||||
if (quote != default)
|
if (quote != default)
|
||||||
{
|
{
|
||||||
var order = new LightningPaywallOrder(invoice.InvoiceId, config.Cost, PaywallOrderStatus.Unpaid,
|
var order = new LightningPaywallOrder
|
||||||
quote.LnInvoice!,
|
{
|
||||||
quote.Expiration);
|
Id = invoice.InvoiceId,
|
||||||
await _store.SaveOrder(order);
|
File = config.File,
|
||||||
|
Service = PaymentServices.Strike,
|
||||||
|
Price = config.Cost,
|
||||||
|
Status = PaywallOrderStatus.Unpaid,
|
||||||
|
Invoice = quote.LnInvoice!,
|
||||||
|
Expire = DateTime.SpecifyKind(quote.Expiration.DateTime, DateTimeKind.Utc)
|
||||||
|
};
|
||||||
|
await _orderStore.Add(order.Id, order);
|
||||||
return order;
|
return order;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogWarning("Failed to get quote for invoice: {Id}", invoice.InvoiceId);
|
_logger.LogWarning("Failed to get quote for invoice: {Id}", invoice.InvoiceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogWarning("Failed to get invoice for config: {Config}", config);
|
_logger.LogWarning("Failed to get invoice for config: File={File}, Service={Service}", config.File,
|
||||||
|
config.Service.ToString());
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async ValueTask<PaywallOrder?> GetOrderStatus(Guid id)
|
public async ValueTask<PaywallOrder?> GetOrderStatus(Guid id)
|
||||||
{
|
{
|
||||||
var order = await _store.GetOrder(id);
|
var order = await _orderStore.Get(id);
|
||||||
if (order == default)
|
if (order is {Status: PaywallOrderStatus.Paid or PaywallOrderStatus.Expired}) return order;
|
||||||
|
|
||||||
|
var providerOrder = await _strike.GetInvoice(id);
|
||||||
|
if (providerOrder != default)
|
||||||
{
|
{
|
||||||
var invoice = await _strike.GetInvoice(id);
|
var status = MapStatus(providerOrder.State);
|
||||||
if (invoice != default)
|
await _orderStore.UpdateStatus(id, status);
|
||||||
|
|
||||||
|
return new()
|
||||||
{
|
{
|
||||||
order = new(id, new(decimal.Parse(invoice.Amount!.Amount!), MapCurrency(invoice.Amount.Currency)),
|
Id = id,
|
||||||
MapStatus(invoice.State));
|
Price = new(decimal.Parse(providerOrder!.Amount!.Amount!),
|
||||||
await _store.SaveOrder(order);
|
MapCurrency(providerOrder.Amount!.Currency!.Value)),
|
||||||
}
|
Service = PaymentServices.Strike,
|
||||||
|
Status = status
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return order;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private PaywallOrderStatus MapStatus(InvoiceState providerOrderState)
|
||||||
|
=> providerOrderState switch
|
||||||
|
{
|
||||||
|
InvoiceState.UNPAID => PaywallOrderStatus.Unpaid,
|
||||||
|
InvoiceState.PENDING => PaywallOrderStatus.Unpaid,
|
||||||
|
InvoiceState.PAID => PaywallOrderStatus.Paid,
|
||||||
|
InvoiceState.CANCELLED => PaywallOrderStatus.Expired,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(providerOrderState), providerOrderState, null)
|
||||||
|
};
|
||||||
|
|
||||||
private static Currencies MapCurrency(PaywallCurrencies c)
|
private static Currencies MapCurrency(PaywallCurrencies c)
|
||||||
=> c switch
|
=> c switch
|
||||||
{
|
{
|
||||||
@ -98,34 +124,24 @@ public class StrikePaywallProvider : IPaywallProvider
|
|||||||
_ => throw new ArgumentOutOfRangeException(nameof(c), c, null)
|
_ => throw new ArgumentOutOfRangeException(nameof(c), c, null)
|
||||||
};
|
};
|
||||||
|
|
||||||
private static PaywallCurrencies MapCurrency(Currencies? c)
|
private static PaywallCurrencies MapCurrency(Currencies c)
|
||||||
=> c switch
|
=> c switch
|
||||||
{
|
{
|
||||||
Currencies.BTC => PaywallCurrencies.BTC,
|
Currencies.BTC => PaywallCurrencies.BTC,
|
||||||
Currencies.USD => PaywallCurrencies.USD,
|
Currencies.USD => PaywallCurrencies.USD,
|
||||||
|
Currencies.USDT => PaywallCurrencies.USD,
|
||||||
Currencies.EUR => PaywallCurrencies.EUR,
|
Currencies.EUR => PaywallCurrencies.EUR,
|
||||||
Currencies.GBP => PaywallCurrencies.GBP,
|
Currencies.GBP => PaywallCurrencies.GBP,
|
||||||
Currencies.USDT => PaywallCurrencies.USD,
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(c), c, null)
|
_ => throw new ArgumentOutOfRangeException(nameof(c), c, null)
|
||||||
};
|
};
|
||||||
|
|
||||||
private static PaywallOrderStatus MapStatus(InvoiceState s)
|
private static void IsStrikePaywall(PaywallConfig? cfg, out StrikePaywallConfig strikeConfig)
|
||||||
=> s switch
|
|
||||||
{
|
|
||||||
InvoiceState.UNPAID => PaywallOrderStatus.Unpaid,
|
|
||||||
InvoiceState.PENDING => PaywallOrderStatus.Unpaid,
|
|
||||||
InvoiceState.PAID => PaywallOrderStatus.Paid,
|
|
||||||
InvoiceState.CANCELLED => PaywallOrderStatus.Expired,
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(s), s, null)
|
|
||||||
};
|
|
||||||
|
|
||||||
private static void IsStrikePaywall(PaywallConfig? cfg, out StrikePaywallConfig? strikeConfig)
|
|
||||||
{
|
{
|
||||||
if (cfg?.Service != PaywallServices.Strike)
|
if (cfg?.Service != PaymentServices.Strike)
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Must be strike paywall");
|
throw new ArgumentException("Must be strike paywall");
|
||||||
}
|
}
|
||||||
|
|
||||||
strikeConfig = cfg as StrikePaywallConfig;
|
strikeConfig = (cfg as StrikePaywallConfig)!;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,6 +3,8 @@ using System.Text;
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Converters;
|
using Newtonsoft.Json.Converters;
|
||||||
|
|
||||||
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
namespace VoidCat.Services.Strike;
|
namespace VoidCat.Services.Strike;
|
||||||
|
|
||||||
public class StrikeApi
|
public class StrikeApi
|
||||||
|
@ -16,7 +16,9 @@ export function FileEdit(props) {
|
|||||||
const [name, setName] = useState(meta?.name);
|
const [name, setName] = useState(meta?.name);
|
||||||
const [description, setDescription] = useState(meta?.description);
|
const [description, setDescription] = useState(meta?.description);
|
||||||
|
|
||||||
const privateFile = profile?.id === file?.uploader?.id ? file : JSON.parse(window.localStorage.getItem(file.id));
|
const privateFile = file?.uploader?.id && profile?.id === file.uploader.id
|
||||||
|
? file
|
||||||
|
: JSON.parse(window.localStorage.getItem(file.id));
|
||||||
if (!privateFile || privateFile?.metadata?.editSecret === null) {
|
if (!privateFile || privateFile?.metadata?.editSecret === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ export function LightningPaywall(props) {
|
|||||||
const file = props.file;
|
const file = props.file;
|
||||||
const order = props.order;
|
const order = props.order;
|
||||||
const onPaid = props.onPaid;
|
const onPaid = props.onPaid;
|
||||||
const link = `lightning:${order.lnInvoice}`;
|
const link = `lightning:${order.invoice}`;
|
||||||
|
|
||||||
function openInvoice() {
|
function openInvoice() {
|
||||||
let a = document.createElement("a");
|
let a = document.createElement("a");
|
||||||
|
Loading…
Reference in New Issue
Block a user