Add File Seller plugin

This commit is contained in:
Kukks
2023-06-19 08:54:10 +02:00
parent f43af81f46
commit b8d244380f
9 changed files with 275 additions and 0 deletions

View File

@@ -53,6 +53,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.Dynami
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.Prism", "Plugins\BTCPayServer.Plugins.Prism\BTCPayServer.Plugins.Prism.csproj", "{9BADDA0B-A5AB-4D51-9EBE-67C08C459DC7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.FileSeller", "Plugins\BTCPayServer.Plugins.FileSeller\BTCPayServer.Plugins.FileSeller.csproj", "{F2D81B6A-E1EA-4900-BF9A-924D2CA951DD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -253,6 +255,14 @@ Global
{9BADDA0B-A5AB-4D51-9EBE-67C08C459DC7}.Altcoins-Debug|Any CPU.Build.0 = Debug|Any CPU
{9BADDA0B-A5AB-4D51-9EBE-67C08C459DC7}.Altcoins-Release|Any CPU.ActiveCfg = Debug|Any CPU
{9BADDA0B-A5AB-4D51-9EBE-67C08C459DC7}.Altcoins-Release|Any CPU.Build.0 = Debug|Any CPU
{F2D81B6A-E1EA-4900-BF9A-924D2CA951DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F2D81B6A-E1EA-4900-BF9A-924D2CA951DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F2D81B6A-E1EA-4900-BF9A-924D2CA951DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F2D81B6A-E1EA-4900-BF9A-924D2CA951DD}.Release|Any CPU.Build.0 = Release|Any CPU
{F2D81B6A-E1EA-4900-BF9A-924D2CA951DD}.Altcoins-Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F2D81B6A-E1EA-4900-BF9A-924D2CA951DD}.Altcoins-Debug|Any CPU.Build.0 = Debug|Any CPU
{F2D81B6A-E1EA-4900-BF9A-924D2CA951DD}.Altcoins-Release|Any CPU.ActiveCfg = Release|Any CPU
{F2D81B6A-E1EA-4900-BF9A-924D2CA951DD}.Altcoins-Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{B19C9F52-DC47-466D-8B5C-2D202B7B003F} = {9E04ECE9-E304-4FF2-9CBC-83256E6C6962}

View File

@@ -1,3 +1,4 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UnloadedProject/UnloadedProjects/=b4e2ed08_002D4ad3_002D4648_002D8bdb_002D3107200460b9_0023BTCPayServer_002EPlugins_002ELiquidPlus/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Nostr/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@@ -0,0 +1,46 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>10</LangVersion>
</PropertyGroup>
<!-- -->
<!-- Plugin specific properties -->
<PropertyGroup>
<Product>File Seller</Product>
<Description>Allows you to sell files through the point of sale/crowdfund apps.</Description>
<Version>1.0.0</Version>
</PropertyGroup>
<!-- Plugin development properties -->
<PropertyGroup>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<PreserveCompilationContext>false</PreserveCompilationContext>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>
<!-- This will make sure that referencing BTCPayServer doesn't put any artifact in the published directory -->
<ItemDefinitionGroup>
<ProjectReference>
<Properties>StaticWebAssetsEnabled=false</Properties>
<Private>false</Private>
<ExcludeAssets>runtime;native;build;buildTransitive;contentFiles</ExcludeAssets>
</ProjectReference>
</ItemDefinitionGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\**" />
<ProjectReference Include="..\..\submodules\btcpayserver\BTCPayServer\BTCPayServer.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Resources\js\" />
<Folder Include="Views\" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="Views\Shared\FileSeller\Detect.cshtml" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,27 @@
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Abstractions.Services;
using Microsoft.Extensions.DependencyInjection;
namespace BTCPayServer.Plugins.FileSeller;
public class FileSellerPlugin : BaseBTCPayServerPlugin
{
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
{
new() { Identifier = nameof(BTCPayServer), Condition = ">=1.10.3" }
};
public override void Execute(IServiceCollection applicationBuilder)
{
applicationBuilder.AddHostedService<FileSellerService>();
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("FileSeller/Detect",
"header-nav"));
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("FileSeller/Detect",
"checkout-end"));
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("FileSeller/FileSellerTemplateEditorItemDetail",
"app-template-editor-item-detail"));
base.Execute(applicationBuilder);
}
}

View File

@@ -0,0 +1,144 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Events;
using BTCPayServer.HostedServices;
using BTCPayServer.Plugins.Crowdfund;
using BTCPayServer.Plugins.PointOfSale;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Storage.Services;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Plugins.FileSeller
{
public class FileSellerService:EventHostedServiceBase
{
private readonly AppService _appService;
private readonly FileService _fileService;
private readonly InvoiceRepository _invoiceRepository;
private readonly StoredFileRepository _storedFileRepository;
public FileSellerService(EventAggregator eventAggregator,
ILogger<FileSellerService> logger,
AppService appService,
FileService fileService,
InvoiceRepository invoiceRepository,
StoredFileRepository storedFileRepository) : base(eventAggregator, logger)
{
_appService = appService;
_fileService = fileService;
_invoiceRepository = invoiceRepository;
_storedFileRepository = storedFileRepository;
}
public static Uri UrlToUse { get; set; }
protected override void SubscribeToEvents()
{
Subscribe<InvoiceEvent>();
base.SubscribeToEvents();
}
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
{
if (evt is not InvoiceEvent invoiceEvent) return;
Dictionary<string, int> cartItems = null;
if (invoiceEvent.Name is not (InvoiceEvent.Completed or InvoiceEvent.MarkedCompleted or InvoiceEvent.Confirmed ))
{
return;
}
var appIds = AppService.GetAppInternalTags(invoiceEvent.Invoice);
if (!appIds.Any())
{
return;
}
if(invoiceEvent.Invoice.Metadata.AdditionalData.TryGetValue("fileselleractivated", out var activated))
{
return;
}
if ((!string.IsNullOrEmpty(invoiceEvent.Invoice.Metadata.ItemCode) ||
AppService.TryParsePosCartItems(invoiceEvent.Invoice.Metadata.PosData, out cartItems)))
{
var items = cartItems ?? new Dictionary<string, int>();
if (!string.IsNullOrEmpty(invoiceEvent.Invoice.Metadata.ItemCode))
{
items.TryAdd(invoiceEvent.Invoice.Metadata.ItemCode, 1);
}
var apps = (await _appService.GetApps(appIds)).Select(data =>
{
switch (data.AppType)
{
case PointOfSaleAppType.AppType:
var possettings = data.GetSettings<PointOfSaleSettings>();
return (Data: data, Settings: (object) possettings,
Items: AppService.Parse(possettings.Template));
case CrowdfundAppType.AppType:
var cfsettings = data.GetSettings<CrowdfundSettings>();
return (Data: data, Settings: cfsettings,
Items: AppService.Parse(cfsettings.PerksTemplate));
default:
return (null, null, null);
}
}).Where(tuple => tuple.Data != null && tuple.Items.Any(item =>
item.AdditionalData?.ContainsKey("file") is true &&
items.ContainsKey(item.Id)));
var fileIds = new HashSet<string>();
foreach (var valueTuple in apps)
{
foreach (var item1 in valueTuple.Items.Where(item =>
item.AdditionalData?.ContainsKey("file") is true &&
items.ContainsKey(item.Id)))
{
var fileId = item1.AdditionalData["file"].Value<string>();
fileIds.Add(fileId);
}
}
var loadedFiles = await _storedFileRepository.GetFiles(new StoredFileRepository.FilesQuery()
{
Id = fileIds.ToArray()
});
var productLinkTasks = loadedFiles.ToDictionary(file =>file,file => _fileService.GetTemporaryFileUrl(UrlToUse, file.Id, DateTimeOffset.MaxValue, true));
var res = await Task.WhenAll(productLinkTasks.Values);
if (res.Any(s => !string.IsNullOrEmpty(s)))
{
var productTitleToFile = productLinkTasks.Select(pair => (pair.Key.FileName, pair.Value.Result))
.Where(s => s.Result is not null)
.ToDictionary(tuple => tuple.FileName, tuple => tuple.Result);
var receiptData = new JObject();
receiptData.Add("Downloadable Content", JObject.FromObject(productTitleToFile) );
if (invoiceEvent.Invoice.Metadata.AdditionalData?.TryGetValue("receiptData",
out var existingReceiptData) is true && existingReceiptData is JObject existingReceiptDataObj )
{
receiptData.Merge(existingReceiptDataObj);
}
invoiceEvent.Invoice.Metadata.SetAdditionalData("receiptData", receiptData);
}
invoiceEvent.Invoice.Metadata.SetAdditionalData("fileselleractivated", "true");
await _invoiceRepository.UpdateInvoiceMetadata(invoiceEvent.InvoiceId, invoiceEvent.Invoice.StoreId,
invoiceEvent.Invoice.Metadata.ToJObject());
}
await base.ProcessEvent(evt, cancellationToken);
}
}
}

View File

@@ -0,0 +1,17 @@
# File Seller Plugin
This plugin allows you to sell files on your BTCPay Server.
## Installation
1. Go to the "Plugins" page of your BTCPay Server
2. Click on install under the "File Seller" plugin listing
3. Restart BTCPay Server
4. Go to Server Settings => Files
5. Ensure that there is a file storage provider configured
6. Click on "Add File"
7. Go to your point of sale or crowdfund app settings
8. Click on "Edit" on the item/perk you'd like to provide a file upon purchase.
9. Select the file you'd like to provide from the new dropdown list titled "File"
10. Close the editor and save the app
11. Upon purchase (invoice marked as settled), a new section will appear on the invoice receipt with a link for each file unlocked.

View File

@@ -0,0 +1,6 @@
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Plugins.FileSeller
@{
FileSellerService.UrlToUse = Context.Request.GetAbsoluteRootUri();
}

View File

@@ -0,0 +1,19 @@
@using BTCPayServer.Storage.Services
@using BTCPayServer.Services
@using Microsoft.AspNetCore.Identity
@using BTCPayServer.Data
@inject StoredFileRepository StoredFileRepository
@inject UserManager<ApplicationUser> UserManager
@inject UserService UserService
@{
var userId = UserManager.GetUserId(User);
var files = (await StoredFileRepository.GetFiles(new StoredFileRepository.FilesQuery()
{
UserIds = await UserService.IsAdminUser(userId) ? Array.Empty<string>() : new[] {userId},
})).Select(file => new SelectListItem(file.FileName, file.Id)).Prepend(new SelectListItem("No file", ""));
}
<div class="form-group">
<label class="form-label">Downloadable file</label>
<select :value="editingItem['file'] || ''" asp-items="files" class="form-select w-auto" v-on:change=" if(event.target.value) editingItem['file']= event.target.value; else delete editingItem['file'];"></select>
<span class="form-text">If a file is selected, when a user buys this item, a download link is generated in the payment receipt once the invoice is settled. <a target="_blank" asp-action="Files" asp-controller="UIServer">Upload files here</a></span>
</div>

View File

@@ -0,0 +1,5 @@
@using BTCPayServer.Abstractions.Services
@inject Safe Safe
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, BTCPayServer
@addTagHelper *, BTCPayServer.Abstractions