mirror of
https://github.com/aljazceru/BTCPayServerPlugins.git
synced 2025-12-17 07:34:24 +01:00
Add File Seller plugin
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
27
Plugins/BTCPayServer.Plugins.FileSeller/FileSellerPlugin.cs
Normal file
27
Plugins/BTCPayServer.Plugins.FileSeller/FileSellerPlugin.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
144
Plugins/BTCPayServer.Plugins.FileSeller/FileSellerService.cs
Normal file
144
Plugins/BTCPayServer.Plugins.FileSeller/FileSellerService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
Plugins/BTCPayServer.Plugins.FileSeller/README.md
Normal file
17
Plugins/BTCPayServer.Plugins.FileSeller/README.md
Normal 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.
|
||||
@@ -0,0 +1,6 @@
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Plugins.FileSeller
|
||||
@{
|
||||
|
||||
FileSellerService.UrlToUse = Context.Request.GetAbsoluteRootUri();
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,5 @@
|
||||
@using BTCPayServer.Abstractions.Services
|
||||
@inject Safe Safe
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@addTagHelper *, BTCPayServer
|
||||
@addTagHelper *, BTCPayServer.Abstractions
|
||||
Reference in New Issue
Block a user