diff --git a/BTCPayServerPlugins.sln b/BTCPayServerPlugins.sln index 17f3cfa..1978044 100644 --- a/BTCPayServerPlugins.sln +++ b/BTCPayServerPlugins.sln @@ -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} diff --git a/BTCPayServerPlugins.sln.DotSettings.user b/BTCPayServerPlugins.sln.DotSettings.user index b2109e6..0a4ed66 100644 --- a/BTCPayServerPlugins.sln.DotSettings.user +++ b/BTCPayServerPlugins.sln.DotSettings.user @@ -1,3 +1,4 @@  + True True \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.FileSeller/BTCPayServer.Plugins.FileSeller.csproj b/Plugins/BTCPayServer.Plugins.FileSeller/BTCPayServer.Plugins.FileSeller.csproj new file mode 100644 index 0000000..13268a5 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.FileSeller/BTCPayServer.Plugins.FileSeller.csproj @@ -0,0 +1,46 @@ + + + + net6.0 + 10 + + + + + File Seller + Allows you to sell files through the point of sale/crowdfund apps. + 1.0.0 + + + + true + false + true + + + + + + StaticWebAssetsEnabled=false + false + runtime;native;build;buildTransitive;contentFiles + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.FileSeller/FileSellerPlugin.cs b/Plugins/BTCPayServer.Plugins.FileSeller/FileSellerPlugin.cs new file mode 100644 index 0000000..9fdf154 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.FileSeller/FileSellerPlugin.cs @@ -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(); + applicationBuilder.AddSingleton(new UIExtension("FileSeller/Detect", + "header-nav")); + applicationBuilder.AddSingleton(new UIExtension("FileSeller/Detect", + "checkout-end")); + applicationBuilder.AddSingleton(new UIExtension("FileSeller/FileSellerTemplateEditorItemDetail", + "app-template-editor-item-detail")); + + + base.Execute(applicationBuilder); + } +} \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.FileSeller/FileSellerService.cs b/Plugins/BTCPayServer.Plugins.FileSeller/FileSellerService.cs new file mode 100644 index 0000000..d31afb3 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.FileSeller/FileSellerService.cs @@ -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 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(); + base.SubscribeToEvents(); + } + + protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken) + { + if (evt is not InvoiceEvent invoiceEvent) return; + Dictionary 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(); + 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(); + return (Data: data, Settings: (object) possettings, + Items: AppService.Parse(possettings.Template)); + case CrowdfundAppType.AppType: + var cfsettings = data.GetSettings(); + 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(); + + 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(); + 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); + } + } +} \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.FileSeller/README.md b/Plugins/BTCPayServer.Plugins.FileSeller/README.md new file mode 100644 index 0000000..eac956d --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.FileSeller/README.md @@ -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. diff --git a/Plugins/BTCPayServer.Plugins.FileSeller/Views/Shared/FileSeller/Detect.cshtml b/Plugins/BTCPayServer.Plugins.FileSeller/Views/Shared/FileSeller/Detect.cshtml new file mode 100644 index 0000000..4ce4c70 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.FileSeller/Views/Shared/FileSeller/Detect.cshtml @@ -0,0 +1,6 @@ +@using BTCPayServer.Abstractions.Extensions +@using BTCPayServer.Plugins.FileSeller +@{ + + FileSellerService.UrlToUse = Context.Request.GetAbsoluteRootUri(); +} \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.FileSeller/Views/Shared/FileSeller/FileSellerTemplateEditorItemDetail.cshtml b/Plugins/BTCPayServer.Plugins.FileSeller/Views/Shared/FileSeller/FileSellerTemplateEditorItemDetail.cshtml new file mode 100644 index 0000000..5ee41b5 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.FileSeller/Views/Shared/FileSeller/FileSellerTemplateEditorItemDetail.cshtml @@ -0,0 +1,19 @@ +@using BTCPayServer.Storage.Services +@using BTCPayServer.Services +@using Microsoft.AspNetCore.Identity +@using BTCPayServer.Data +@inject StoredFileRepository StoredFileRepository +@inject UserManager UserManager +@inject UserService UserService +@{ + var userId = UserManager.GetUserId(User); + var files = (await StoredFileRepository.GetFiles(new StoredFileRepository.FilesQuery() + { + UserIds = await UserService.IsAdminUser(userId) ? Array.Empty() : new[] {userId}, + })).Select(file => new SelectListItem(file.FileName, file.Id)).Prepend(new SelectListItem("No file", "")); +} +
+ + + 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. Upload files here +
\ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.FileSeller/_ViewImports.cshtml b/Plugins/BTCPayServer.Plugins.FileSeller/_ViewImports.cshtml new file mode 100644 index 0000000..d897d63 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.FileSeller/_ViewImports.cshtml @@ -0,0 +1,5 @@ +@using BTCPayServer.Abstractions.Services +@inject Safe Safe +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, BTCPayServer +@addTagHelper *, BTCPayServer.Abstractions \ No newline at end of file