forked from Kieran/void.cat
Add strike paywall (incomplete)
This commit is contained in:
parent
e098b2c0f0
commit
0eda25ba00
@ -1,6 +1,7 @@
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using VoidCat.Model;
|
||||
using VoidCat.Model.Paywall;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Controllers;
|
||||
@ -9,12 +10,14 @@ namespace VoidCat.Controllers;
|
||||
public class DownloadController : Controller
|
||||
{
|
||||
private readonly IFileStore _storage;
|
||||
private readonly IPaywallStore _paywall;
|
||||
private readonly ILogger<DownloadController> _logger;
|
||||
|
||||
public DownloadController(IFileStore storage, ILogger<DownloadController> logger)
|
||||
public DownloadController(IFileStore storage, ILogger<DownloadController> logger, IPaywallStore paywall)
|
||||
{
|
||||
_storage = storage;
|
||||
_logger = logger;
|
||||
_paywall = paywall;
|
||||
}
|
||||
|
||||
[HttpOptions]
|
||||
@ -32,8 +35,9 @@ public class DownloadController : Controller
|
||||
{
|
||||
var gid = id.FromBase58Guid();
|
||||
var voidFile = await SetupDownload(gid);
|
||||
if (voidFile == default) return;
|
||||
|
||||
var egressReq = new EgressRequest(gid, GetRanges(Request, (long) voidFile!.Metadata!.Size));
|
||||
var egressReq = new EgressRequest(gid, GetRanges(Request, (long)voidFile!.Metadata!.Size));
|
||||
if (egressReq.Ranges.Count() > 1)
|
||||
{
|
||||
_logger.LogWarning("Multi-range request not supported!");
|
||||
@ -45,10 +49,10 @@ public class DownloadController : Controller
|
||||
}
|
||||
else if (egressReq.Ranges.Count() == 1)
|
||||
{
|
||||
Response.StatusCode = (int) HttpStatusCode.PartialContent;
|
||||
Response.StatusCode = (int)HttpStatusCode.PartialContent;
|
||||
if (egressReq.Ranges.Sum(a => a.Size) == 0)
|
||||
{
|
||||
Response.StatusCode = (int) HttpStatusCode.RequestedRangeNotSatisfiable;
|
||||
Response.StatusCode = (int)HttpStatusCode.RequestedRangeNotSatisfiable;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -78,6 +82,26 @@ public class DownloadController : Controller
|
||||
return null;
|
||||
}
|
||||
|
||||
// check paywall
|
||||
if (meta.Paywall != default)
|
||||
{
|
||||
var orderId = Request.Headers.GetHeader("V-OrderId");
|
||||
if (string.IsNullOrEmpty(orderId))
|
||||
{
|
||||
Response.StatusCode = (int)HttpStatusCode.PaymentRequired;
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
var order = await _paywall.GetOrder(orderId.FromBase58Guid());
|
||||
if (order?.Status != PaywallStatus.Paid)
|
||||
{
|
||||
Response.StatusCode = (int)HttpStatusCode.PaymentRequired;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Response.Headers.XFrameOptions = "SAMEORIGIN";
|
||||
Response.Headers.ContentDisposition = $"inline; filename=\"{meta?.Metadata?.Name}\"";
|
||||
Response.ContentType = meta?.Metadata?.MimeType ?? "application/octet-stream";
|
||||
@ -110,4 +134,4 @@ public class DownloadController : Controller
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Newtonsoft.Json;
|
||||
using VoidCat.Model;
|
||||
using VoidCat.Services;
|
||||
using VoidCat.Model.Paywall;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Controllers
|
||||
@ -12,10 +12,14 @@ namespace VoidCat.Controllers
|
||||
public class UploadController : Controller
|
||||
{
|
||||
private readonly IFileStore _storage;
|
||||
private readonly IFileMetadataStore _metadata;
|
||||
private readonly IPaywallStore _paywall;
|
||||
|
||||
public UploadController(IFileStore storage)
|
||||
public UploadController(IFileStore storage, IFileMetadataStore metadata, IPaywallStore paywall)
|
||||
{
|
||||
_storage = storage;
|
||||
_metadata = metadata;
|
||||
_paywall = paywall;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@ -78,6 +82,32 @@ namespace VoidCat.Controllers
|
||||
{
|
||||
return _storage.Get(id.FromBase58Guid());
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{id}/paywall")]
|
||||
public ValueTask<PaywallOrder?> CreateOrder([FromRoute] string id)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{id}/paywall")]
|
||||
public async Task<IActionResult> SetPaywallConfig([FromRoute] string id, [FromBody] SetPaywallConfigRequest req)
|
||||
{
|
||||
var gid = id.FromBase58Guid();
|
||||
var meta = await _metadata.Get(gid);
|
||||
if (meta == default) return NotFound();
|
||||
|
||||
if (req.EditSecret != meta.EditSecret) return Unauthorized();
|
||||
|
||||
if (req.Strike != default)
|
||||
{
|
||||
await _paywall.SetConfig(gid, req.Strike!);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
return BadRequest();
|
||||
}
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||
@ -104,4 +134,12 @@ namespace VoidCat.Controllers
|
||||
public static UploadResult Error(string message)
|
||||
=> new(false, null, message);
|
||||
}
|
||||
|
||||
public record SetPaywallConfigRequest
|
||||
{
|
||||
[JsonConverter(typeof(Base58GuidConverter))]
|
||||
public Guid EditSecret { get; init; }
|
||||
|
||||
public StrikePaywallConfig? Strike { get; init; }
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
namespace VoidCat.Model;
|
||||
|
||||
public record Paywall
|
||||
{
|
||||
public PaywallServices Service { get; init; }
|
||||
|
||||
public PaywallConfig? Config { get; init; }
|
||||
}
|
||||
|
||||
public enum PaywallServices
|
||||
{
|
||||
None,
|
||||
Strike
|
||||
}
|
||||
|
||||
public enum PaywallCurrencies
|
||||
{
|
||||
BTC,
|
||||
USD,
|
||||
EUR,
|
||||
GBP
|
||||
}
|
||||
|
||||
public abstract record PaywallConfig
|
||||
{
|
||||
public PaywallCurrencies Currency { get; init; }
|
||||
public decimal Cost { get; init; }
|
||||
}
|
||||
|
||||
public record StrikePaywallConfig(string Handle) : PaywallConfig;
|
12
VoidCat/Model/Paywall/Paywall.cs
Normal file
12
VoidCat/Model/Paywall/Paywall.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace VoidCat.Model.Paywall;
|
||||
|
||||
public enum PaywallServices
|
||||
{
|
||||
None,
|
||||
Strike
|
||||
}
|
||||
|
||||
public abstract record PaywallConfig(PaywallServices Service, [property: JsonConverter(typeof(JsonStringEnumConverter))]PaywallMoney Cost);
|
||||
public record StrikePaywallConfig(string Handle, PaywallMoney Cost) : PaywallConfig(PaywallServices.Strike, Cost);
|
14
VoidCat/Model/Paywall/PaywallMoney.cs
Normal file
14
VoidCat/Model/Paywall/PaywallMoney.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace VoidCat.Model.Paywall;
|
||||
|
||||
public record PaywallMoney(decimal Amount, PaywallCurrencies Currency);
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum PaywallCurrencies : byte
|
||||
{
|
||||
BTC = 0,
|
||||
USD = 1,
|
||||
EUR = 2,
|
||||
GBP = 3
|
||||
}
|
11
VoidCat/Model/Paywall/PaywallOrder.cs
Normal file
11
VoidCat/Model/Paywall/PaywallOrder.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace VoidCat.Model.Paywall;
|
||||
|
||||
public enum PaywallStatus : byte
|
||||
{
|
||||
Unpaid,
|
||||
Paid,
|
||||
Expired
|
||||
}
|
||||
|
||||
public abstract record PaywallOrder(Guid Id, PaywallMoney Price, PaywallStatus Status);
|
||||
public record LightningPaywallOrder(Guid Id, PaywallMoney Price, PaywallStatus Status, string LnInvoice) : PaywallOrder(Id, Price, Status);
|
@ -1,4 +1,5 @@
|
||||
using Newtonsoft.Json;
|
||||
using VoidCat.Model.Paywall;
|
||||
|
||||
namespace VoidCat.Model
|
||||
{
|
||||
@ -18,7 +19,7 @@ namespace VoidCat.Model
|
||||
/// <summary>
|
||||
/// Optional paywall config
|
||||
/// </summary>
|
||||
public Paywall? Paywall { get; init; }
|
||||
public PaywallConfig? Paywall { get; init; }
|
||||
}
|
||||
|
||||
public sealed record PublicVoidFile : VoidFile<VoidFileMeta>
|
||||
|
@ -1,4 +1,6 @@
|
||||
namespace VoidCat.Model
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Model
|
||||
{
|
||||
public class VoidSettings
|
||||
{
|
||||
@ -9,6 +11,8 @@
|
||||
public JwtSettings JwtSettings { get; init; } = new("void_cat_internal", "default_key");
|
||||
|
||||
public string? Redis { get; init; }
|
||||
|
||||
public StrikeApiSettings? Strike { get; init; }
|
||||
}
|
||||
|
||||
public sealed record TorSettings(Uri TorControl, string PrivateKey, string ControlPassword);
|
||||
|
@ -1,12 +1,15 @@
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Newtonsoft.Json;
|
||||
using Prometheus;
|
||||
using StackExchange.Redis;
|
||||
using VoidCat.Model;
|
||||
using VoidCat.Services;
|
||||
using VoidCat.Services.Abstractions;
|
||||
using VoidCat.Services.InMemory;
|
||||
using VoidCat.Services.Migrations;
|
||||
using VoidCat.Services.Redis;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var services = builder.Services;
|
||||
@ -27,7 +30,10 @@ if (useRedis)
|
||||
}
|
||||
|
||||
services.AddRouting();
|
||||
services.AddControllers().AddNewtonsoftJson();
|
||||
services.AddControllers().AddNewtonsoftJson((opt) =>
|
||||
{
|
||||
opt.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
|
||||
});
|
||||
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
@ -53,6 +59,7 @@ if (useRedis)
|
||||
services.AddScoped<RedisStatsController>();
|
||||
services.AddScoped<IStatsCollector>(svc => svc.GetRequiredService<RedisStatsController>());
|
||||
services.AddScoped<IStatsReporter>(svc => svc.GetRequiredService<RedisStatsController>());
|
||||
services.AddScoped<IPaywallStore, RedisPaywallStore>();
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -60,6 +67,7 @@ else
|
||||
services.AddScoped<InMemoryStatsController>();
|
||||
services.AddScoped<IStatsReporter>(svc => svc.GetRequiredService<InMemoryStatsController>());
|
||||
services.AddScoped<IStatsCollector>(svc => svc.GetRequiredService<InMemoryStatsController>());
|
||||
services.AddScoped<IPaywallStore, InMemoryPaywallStore>();
|
||||
}
|
||||
|
||||
var app = builder.Build();
|
||||
|
24
VoidCat/Services/Abstractions/IPaywallFactory.cs
Normal file
24
VoidCat/Services/Abstractions/IPaywallFactory.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using VoidCat.Model.Paywall;
|
||||
|
||||
namespace VoidCat.Services.Abstractions;
|
||||
|
||||
public interface IPaywallFactory
|
||||
{
|
||||
ValueTask<IPaywallProvider> CreateStrikeProvider();
|
||||
}
|
||||
|
||||
public interface IPaywallProvider
|
||||
{
|
||||
ValueTask<PaywallOrder?> CreateOrder(PaywallConfig config);
|
||||
|
||||
ValueTask<PaywallOrder?> GetOrderStatus(Guid id);
|
||||
}
|
||||
|
||||
public interface IPaywallStore
|
||||
{
|
||||
ValueTask<PaywallOrder?> GetOrder(Guid id);
|
||||
ValueTask SaveOrder(PaywallOrder order);
|
||||
|
||||
ValueTask<PaywallConfig?> GetConfig(Guid id);
|
||||
ValueTask SetConfig(Guid id, PaywallConfig config);
|
||||
}
|
317
VoidCat/Services/Abstractions/StrikeApi.cs
Normal file
317
VoidCat/Services/Abstractions/StrikeApi.cs
Normal file
@ -0,0 +1,317 @@
|
||||
namespace VoidCat.Services.Abstractions;
|
||||
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
public class StrikeApi
|
||||
{
|
||||
private readonly ILogger<StrikeApi> _logger;
|
||||
private readonly HttpClient _client;
|
||||
private readonly StrikeApiSettings _settings;
|
||||
|
||||
public StrikeApi(StrikeApiSettings settings, ILogger<StrikeApi> logger)
|
||||
{
|
||||
_client = new HttpClient
|
||||
{
|
||||
BaseAddress = settings.Uri ?? new Uri("https://api.strike.me/")
|
||||
};
|
||||
|
||||
_settings = settings;
|
||||
_logger = logger;
|
||||
|
||||
_client.DefaultRequestHeaders.Add("Authorization", $"Bearer {settings.ApiKey}");
|
||||
}
|
||||
|
||||
public Task<Invoice?> GenerateInvoice(CreateInvoiceRequest invoiceRequest)
|
||||
{
|
||||
var path = !string.IsNullOrEmpty(invoiceRequest.Handle)
|
||||
? $"/v1/invoices/handle/{invoiceRequest.Handle}"
|
||||
: "/v1/invoices";
|
||||
|
||||
return SendRequest<Invoice>(HttpMethod.Post, path, invoiceRequest);
|
||||
}
|
||||
|
||||
public Task<Profile?> GetProfile(string handle)
|
||||
{
|
||||
return SendRequest<Profile>(HttpMethod.Get, $"/v1/accounts/handle/{handle}/profile");
|
||||
}
|
||||
|
||||
public Task<Profile?> GetProfile(Guid id)
|
||||
{
|
||||
return SendRequest<Profile>(HttpMethod.Get, $"/v1/accounts/{id}/profile");
|
||||
}
|
||||
|
||||
public Task<Invoice?> GetInvoice(Guid id)
|
||||
{
|
||||
return SendRequest<Invoice>(HttpMethod.Get, $"/v1/invoices/{id}");
|
||||
}
|
||||
|
||||
public Task<InvoiceQuote?> GetInvoiceQuote(Guid id)
|
||||
{
|
||||
return SendRequest<InvoiceQuote>(HttpMethod.Post, $"/v1/invoices/{id}/quote");
|
||||
}
|
||||
|
||||
public Task<IEnumerable<WebhookSubscription>?> GetWebhookSubscriptions()
|
||||
{
|
||||
return SendRequest<IEnumerable<WebhookSubscription>>(HttpMethod.Get, "/v1/subscriptions");
|
||||
}
|
||||
|
||||
public Task<WebhookSubscription?> CreateWebhook(NewWebhook hook)
|
||||
{
|
||||
return SendRequest<WebhookSubscription>(HttpMethod.Post, "/v1/subscriptions", hook);
|
||||
}
|
||||
|
||||
public Task DeleteWebhook(Guid id)
|
||||
{
|
||||
return SendRequest<object>(HttpMethod.Delete, $"/v1/subscriptions/{id}");
|
||||
}
|
||||
|
||||
private async Task<TReturn?> SendRequest<TReturn>(HttpMethod method, string path, object? bodyObj = default)
|
||||
where TReturn : class
|
||||
{
|
||||
var request = new HttpRequestMessage(method, path);
|
||||
if (bodyObj != default)
|
||||
{
|
||||
var reqJson = JsonConvert.SerializeObject(bodyObj);
|
||||
request.Content = new StringContent(reqJson, Encoding.UTF8, "application/json");
|
||||
}
|
||||
|
||||
var rsp = await _client.SendAsync(request);
|
||||
var okResponse = method.Method switch
|
||||
{
|
||||
"POST" => HttpStatusCode.Created,
|
||||
_ => HttpStatusCode.OK
|
||||
};
|
||||
|
||||
var json = await rsp.Content.ReadAsStringAsync();
|
||||
_logger.LogInformation(json);
|
||||
return rsp.StatusCode == okResponse ? JsonConvert.DeserializeObject<TReturn>(json) : default;
|
||||
}
|
||||
}
|
||||
|
||||
public class Profile
|
||||
{
|
||||
[JsonProperty("handle")]
|
||||
public string Handle { get; init; } = null;
|
||||
|
||||
[JsonProperty("avatarUrl")]
|
||||
public string? AvatarUrl { get; init; }
|
||||
|
||||
[JsonProperty("description")]
|
||||
public string? Description { get; init; }
|
||||
|
||||
[JsonProperty("canReceive")]
|
||||
public bool CanReceive { get; init; }
|
||||
|
||||
[JsonProperty("currencies")]
|
||||
public List<AvailableCurrency> Currencies { get; init; } = new();
|
||||
}
|
||||
|
||||
public class InvoiceQuote
|
||||
{
|
||||
[JsonProperty("quoteId")]
|
||||
public Guid QuoteId { get; init; }
|
||||
|
||||
[JsonProperty("description")]
|
||||
public string? Description { get; init; }
|
||||
|
||||
[JsonProperty("lnInvoice")]
|
||||
public string? LnInvoice { get; init; }
|
||||
|
||||
[JsonProperty("onchainAddress")]
|
||||
public string? OnChainAddress { get; init; }
|
||||
|
||||
[JsonProperty("expiration")]
|
||||
public DateTimeOffset Expiration { get; init; }
|
||||
|
||||
[JsonProperty("expirationInSec")]
|
||||
public ulong ExpirationSec { get; init; }
|
||||
|
||||
[JsonProperty("targetAmount")]
|
||||
public CurrencyAmount? TargetAmount { get; init; }
|
||||
|
||||
[JsonProperty("sourceAmount")]
|
||||
public CurrencyAmount? SourceAmount { get; init; }
|
||||
|
||||
[JsonProperty("conversionRate")]
|
||||
public ConversionRate? ConversionRate { get; init; }
|
||||
}
|
||||
|
||||
public class ConversionRate
|
||||
{
|
||||
[JsonProperty("amount")]
|
||||
public string? Amount { get; init; }
|
||||
|
||||
[JsonProperty("sourceCurrency")]
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public Currencies Source { get; init; }
|
||||
|
||||
[JsonProperty("targetCurrency")]
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public Currencies Target { get; init; }
|
||||
}
|
||||
|
||||
public class ErrorResponse : Exception
|
||||
{
|
||||
public ErrorResponse(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class CreateInvoiceRequest
|
||||
{
|
||||
[JsonProperty("correlationId")]
|
||||
public string? CorrelationId { get; init; }
|
||||
|
||||
[JsonProperty("description")]
|
||||
public string? Description { get; init; }
|
||||
|
||||
[JsonProperty("amount")]
|
||||
public CurrencyAmount? Amount { get; init; }
|
||||
|
||||
[JsonProperty("handle")]
|
||||
public string? Handle { get; init; }
|
||||
}
|
||||
|
||||
public class CurrencyAmount
|
||||
{
|
||||
[JsonProperty("amount")]
|
||||
public string? Amount { get; init; }
|
||||
|
||||
[JsonProperty("currency")]
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public Currencies? Currency { get; init; }
|
||||
}
|
||||
|
||||
public class AvailableCurrency
|
||||
{
|
||||
[JsonProperty("currency")]
|
||||
public Currencies Currency { get; init; }
|
||||
|
||||
[JsonProperty("isDefaultCurrency")]
|
||||
public bool IsDefault { get; init; }
|
||||
|
||||
[JsonProperty("isAvailable")]
|
||||
public bool IsAvailable { get; init; }
|
||||
}
|
||||
|
||||
public enum Currencies
|
||||
{
|
||||
BTC,
|
||||
USD,
|
||||
EUR,
|
||||
GBP,
|
||||
USDT
|
||||
}
|
||||
|
||||
public class Invoice
|
||||
{
|
||||
[JsonProperty("invoiceId")]
|
||||
public Guid InvoiceId { get; init; }
|
||||
|
||||
[JsonProperty("amount")]
|
||||
public CurrencyAmount? Amount { get; init; }
|
||||
|
||||
[JsonProperty("state")]
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public InvoiceState State { get; set; }
|
||||
|
||||
[JsonProperty("created")]
|
||||
public DateTimeOffset? Created { get; init; }
|
||||
|
||||
[JsonProperty("correlationId")]
|
||||
public string? CorrelationId { get; init; }
|
||||
|
||||
[JsonProperty("description")]
|
||||
public string? Description { get; init; }
|
||||
|
||||
[JsonProperty("issuerId")]
|
||||
public Guid? IssuerId { get; init; }
|
||||
|
||||
[JsonProperty("receiverId")]
|
||||
public Guid? ReceiverId { get; init; }
|
||||
|
||||
[JsonProperty("payerId")]
|
||||
public Guid? PayerId { get; init; }
|
||||
}
|
||||
|
||||
public abstract class WebhookBase
|
||||
{
|
||||
[JsonProperty("webhookUrl")]
|
||||
public Uri? Uri { get; init; }
|
||||
|
||||
[JsonProperty("webhookVersion")]
|
||||
public string? Version { get; init; }
|
||||
|
||||
[JsonProperty("enabled")]
|
||||
public bool? Enabled { get; init; }
|
||||
|
||||
[JsonProperty("eventTypes")]
|
||||
public HashSet<string>? EventTypes { get; init; }
|
||||
}
|
||||
|
||||
public sealed class NewWebhook : WebhookBase
|
||||
{
|
||||
[JsonProperty("secret")]
|
||||
public string? Secret { get; init; }
|
||||
}
|
||||
|
||||
public sealed class WebhookSubscription : WebhookBase
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public Guid? Id { get; init; }
|
||||
|
||||
[JsonProperty("created")]
|
||||
public DateTimeOffset? Created { get; init; }
|
||||
}
|
||||
|
||||
public class WebhookData
|
||||
{
|
||||
[JsonProperty("entityId")]
|
||||
public Guid? EntityId { get; set; }
|
||||
|
||||
[JsonProperty("changes")]
|
||||
public List<string>? Changes { get; set; }
|
||||
}
|
||||
|
||||
public class WebhookEvent
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public Guid? Id { get; set; }
|
||||
|
||||
[JsonProperty("eventType")]
|
||||
public string? EventType { get; set; }
|
||||
|
||||
[JsonProperty("webhookVersion")]
|
||||
public string? WebhookVersion { get; set; }
|
||||
|
||||
[JsonProperty("data")]
|
||||
public WebhookData? Data { get; set; }
|
||||
|
||||
[JsonProperty("created")]
|
||||
public DateTimeOffset? Created { get; set; }
|
||||
|
||||
[JsonProperty("deliverySuccess")]
|
||||
public bool? DeliverySuccess { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Id = {Id}, EntityId = {Data?.EntityId}, Event = {EventType}";
|
||||
}
|
||||
}
|
||||
|
||||
public enum InvoiceState
|
||||
{
|
||||
UNPAID,
|
||||
PENDING,
|
||||
PAID,
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
public class StrikeApiSettings
|
||||
{
|
||||
public Uri? Uri { get; init; }
|
||||
public string? ApiKey { get; init; }
|
||||
}
|
37
VoidCat/Services/InMemory/InMemoryPaywallStore.cs
Normal file
37
VoidCat/Services/InMemory/InMemoryPaywallStore.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using VoidCat.Model.Paywall;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services.InMemory;
|
||||
|
||||
public class InMemoryPaywallStore : IPaywallStore
|
||||
{
|
||||
private readonly IMemoryCache _cache;
|
||||
|
||||
public InMemoryPaywallStore(IMemoryCache cache)
|
||||
{
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
public ValueTask<PaywallConfig?> GetConfig(Guid id)
|
||||
{
|
||||
return ValueTask.FromResult(_cache.Get(id) as PaywallConfig);
|
||||
}
|
||||
|
||||
public ValueTask SetConfig(Guid id, PaywallConfig config)
|
||||
{
|
||||
_cache.Set(id, config);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask<PaywallOrder?> GetOrder(Guid id)
|
||||
{
|
||||
return ValueTask.FromResult(_cache.Get(id) as PaywallOrder);
|
||||
}
|
||||
|
||||
public ValueTask SaveOrder(PaywallOrder order)
|
||||
{
|
||||
_cache.Set(order.Id, order);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services;
|
||||
namespace VoidCat.Services.InMemory;
|
||||
|
||||
public class InMemoryStatsController : IStatsCollector, IStatsReporter
|
||||
{
|
@ -12,13 +12,15 @@ public class LocalDiskFileStore : IFileStore
|
||||
private readonly VoidSettings _settings;
|
||||
private readonly IAggregateStatsCollector _stats;
|
||||
private readonly IFileMetadataStore _metadataStore;
|
||||
private readonly IPaywallStore _paywallStore;
|
||||
|
||||
public LocalDiskFileStore(VoidSettings settings, IAggregateStatsCollector stats,
|
||||
IFileMetadataStore metadataStore)
|
||||
IFileMetadataStore metadataStore, IPaywallStore paywallStore)
|
||||
{
|
||||
_settings = settings;
|
||||
_stats = stats;
|
||||
_metadataStore = metadataStore;
|
||||
_paywallStore = paywallStore;
|
||||
|
||||
if (!Directory.Exists(_settings.DataDirectory))
|
||||
{
|
||||
@ -31,7 +33,8 @@ public class LocalDiskFileStore : IFileStore
|
||||
return new ()
|
||||
{
|
||||
Id = id,
|
||||
Metadata = await _metadataStore.GetPublic(id)
|
||||
Metadata = await _metadataStore.GetPublic(id),
|
||||
Paywall = await _paywallStore.GetConfig(id)
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,6 @@ public class WrongFile
|
||||
[JsonConverter(typeof(Base58GuidConverter))]
|
||||
public Guid Id { get; init; }
|
||||
public WrongMeta? Metadata { get; init; }
|
||||
public Paywall? Paywall { get; init; }
|
||||
}
|
||||
|
||||
public class WrongMeta
|
||||
|
46
VoidCat/Services/Redis/RedisPaywallStore.cs
Normal file
46
VoidCat/Services/Redis/RedisPaywallStore.cs
Normal file
@ -0,0 +1,46 @@
|
||||
using Newtonsoft.Json;
|
||||
using StackExchange.Redis;
|
||||
using VoidCat.Model.Paywall;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services.Redis;
|
||||
|
||||
public class RedisPaywallStore : IPaywallStore
|
||||
{
|
||||
private readonly IDatabase _database;
|
||||
|
||||
public RedisPaywallStore(IDatabase database)
|
||||
{
|
||||
_database = database;
|
||||
}
|
||||
|
||||
public async ValueTask<PaywallConfig?> GetConfig(Guid id)
|
||||
{
|
||||
var json = await _database.StringGetAsync(ConfigKey(id));
|
||||
var cfg = JsonConvert.DeserializeObject<PaywallConfig>(json);
|
||||
return cfg?.Service switch
|
||||
{
|
||||
PaywallServices.Strike => JsonConvert.DeserializeObject<StrikePaywallConfig>(json),
|
||||
_ => default
|
||||
};
|
||||
}
|
||||
|
||||
public async ValueTask SetConfig(Guid id, PaywallConfig config)
|
||||
{
|
||||
await _database.StringSetAsync(ConfigKey(id), JsonConvert.SerializeObject(config));
|
||||
}
|
||||
|
||||
public async ValueTask<PaywallOrder?> GetOrder(Guid id)
|
||||
{
|
||||
var json = await _database.StringGetAsync(OrderKey(id));
|
||||
return JsonConvert.DeserializeObject<PaywallOrder>(json);
|
||||
}
|
||||
|
||||
public async ValueTask SaveOrder(PaywallOrder order)
|
||||
{
|
||||
await _database.StringSetAsync(OrderKey(order.Id), JsonConvert.SerializeObject(order));
|
||||
}
|
||||
|
||||
private RedisKey ConfigKey(Guid id) => $"paywall:config:{id}";
|
||||
private RedisKey OrderKey(Guid id) => $"paywall:order:{id}";
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
using StackExchange.Redis;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services;
|
||||
namespace VoidCat.Services.Redis;
|
||||
|
||||
public class RedisStatsController : IStatsReporter, IStatsCollector
|
||||
{
|
@ -29,4 +29,11 @@ export const ZiB = Math.pow(1024, 7);
|
||||
/**
|
||||
* @constant {number} - Size of 1 YiB
|
||||
*/
|
||||
export const YiB = Math.pow(1024, 8);
|
||||
export const YiB = Math.pow(1024, 8);
|
||||
|
||||
export const PaywallCurrencies = {
|
||||
BTC: 0,
|
||||
USD: 1,
|
||||
EUR: 2,
|
||||
GBP: 3
|
||||
}
|
9
VoidCat/spa/src/FileEdit.css
Normal file
9
VoidCat/spa/src/FileEdit.css
Normal file
@ -0,0 +1,9 @@
|
||||
.file-edit {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.file-edit > div {
|
||||
flex: 1;
|
||||
}
|
47
VoidCat/spa/src/FileEdit.js
Normal file
47
VoidCat/spa/src/FileEdit.js
Normal file
@ -0,0 +1,47 @@
|
||||
import {useState} from "react";
|
||||
|
||||
import "./FileEdit.css";
|
||||
import {StrikePaywallConfig} from "./StrikePaywallConfig";
|
||||
|
||||
export function FileEdit(props) {
|
||||
const [paywall, setPaywall] = useState();
|
||||
|
||||
const privateFile = JSON.parse(window.localStorage.getItem(props.file.id));
|
||||
if (!privateFile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function renderPaywallConfig() {
|
||||
switch (paywall) {
|
||||
case 1: {
|
||||
return <StrikePaywallConfig file={privateFile}/>
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const meta = props.file.metadata;
|
||||
return (
|
||||
<div className="file-edit">
|
||||
<div>
|
||||
<h3>File info</h3>
|
||||
<dl>
|
||||
<dt>Filename:</dt>
|
||||
<dd><input type="text" value={meta.name}/></dd>
|
||||
<dt>Description:</dt>
|
||||
<dd><input type="text" value={meta.description}/></dd>
|
||||
</dl>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<h3>Paywall Config</h3>
|
||||
Type:
|
||||
<select onChange={(e) => setPaywall(parseInt(e.target.value))}>
|
||||
<option value={0}>None</option>
|
||||
<option value={1}>Strike</option>
|
||||
</select>
|
||||
{renderPaywallConfig()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
21
VoidCat/spa/src/FilePaywall.js
Normal file
21
VoidCat/spa/src/FilePaywall.js
Normal file
@ -0,0 +1,21 @@
|
||||
import {ConstName} from "./Util";
|
||||
import {PaywallCurrencies} from "./Const";
|
||||
import {useState} from "react";
|
||||
|
||||
export function FilePaywall(props) {
|
||||
const file = props.file;
|
||||
const pw = file.paywall;
|
||||
|
||||
const [order, setOrder] = useState();
|
||||
|
||||
async function fetchOrder() {
|
||||
let req = await fetch("")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="paywall">
|
||||
<h3>You must pay {ConstName(PaywallCurrencies, pw.cost.currency)} {pw.cost.amount} to view this file.</h3>
|
||||
<button onClick={fetchOrder}>Pay</button>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -3,6 +3,8 @@ import {useParams} from "react-router-dom";
|
||||
import {TextPreview} from "./TextPreview";
|
||||
|
||||
import "./FilePreview.css";
|
||||
import {FileEdit} from "./FileEdit";
|
||||
import {FilePaywall} from "./FilePaywall";
|
||||
|
||||
export function FilePreview() {
|
||||
const params = useParams();
|
||||
@ -17,6 +19,10 @@ export function FilePreview() {
|
||||
}
|
||||
|
||||
function renderTypes() {
|
||||
if(info.paywall) {
|
||||
return <FilePaywall file={info}/>;
|
||||
}
|
||||
|
||||
let link = `/d/${info.id}`;
|
||||
if (info.metadata) {
|
||||
switch (info.metadata.mimeType) {
|
||||
@ -53,6 +59,7 @@ export function FilePreview() {
|
||||
<Fragment>
|
||||
this.Download(<a className="btn" href={`/d/${info.id}`}>{info.metadata?.name ?? info.id}</a>)
|
||||
{renderTypes()}
|
||||
<FileEdit file={info}/>
|
||||
</Fragment>
|
||||
) : "Not Found"}
|
||||
</div>
|
||||
|
@ -139,6 +139,7 @@ export function FileUpload(props) {
|
||||
if (xhr.ok) {
|
||||
setUState(UploadState.Done);
|
||||
setResult(xhr.file);
|
||||
window.localStorage.setItem(xhr.file.id, JSON.stringify(xhr.file));
|
||||
} else {
|
||||
setUState(UploadState.Failed);
|
||||
setResult(xhr.errorMessage);
|
||||
|
56
VoidCat/spa/src/StrikePaywallConfig.js
Normal file
56
VoidCat/spa/src/StrikePaywallConfig.js
Normal file
@ -0,0 +1,56 @@
|
||||
import {useState} from "react";
|
||||
import {PaywallCurrencies} from "./Const";
|
||||
|
||||
export function StrikePaywallConfig(props) {
|
||||
const editSecret = props.file.metadata.editSecret;
|
||||
const id = props.file.id;
|
||||
|
||||
const [username, setUsername] = useState("hrf");
|
||||
const [currency, setCurrency] = useState(PaywallCurrencies.USD);
|
||||
const [price, setPrice] = useState(1);
|
||||
|
||||
async function saveStrikeConfig() {
|
||||
let cfg = {
|
||||
editSecret,
|
||||
strike: {
|
||||
handle: username,
|
||||
cost: {
|
||||
currency: currency,
|
||||
amount: price
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let req = await fetch(`/upload/${id}/paywall`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(cfg),
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
if (!req.ok) {
|
||||
alert("Error settings paywall config!");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<dl>
|
||||
<dt>Stike username:</dt>
|
||||
<dd><input type="text" value={username} onChange={(e) => setUsername(e.target.value)}/></dd>
|
||||
<dt>Currency:</dt>
|
||||
<dd>
|
||||
<select onChange={(e) => setCurrency(parseInt(e.target.value))}>
|
||||
<option selected={currency === PaywallCurrencies.BTC} value={PaywallCurrencies.BTC}>BTC</option>
|
||||
<option selected={currency === PaywallCurrencies.USD} value={PaywallCurrencies.USD}>USD</option>
|
||||
<option selected={currency === PaywallCurrencies.EUR} value={PaywallCurrencies.EUR}>EUR</option>
|
||||
<option selected={currency === PaywallCurrencies.GBP} value={PaywallCurrencies.GBP}>GBP</option>
|
||||
</select>
|
||||
</dd>
|
||||
<dt>Price:</dt>
|
||||
<dd><input type="number" value={price} onChange={(e) => setPrice(parseFloat(e.target.value))}/></dd>
|
||||
</dl>
|
||||
<button onClick={saveStrikeConfig}>Save</button>
|
||||
</div>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user