mirror of
https://github.com/aljazceru/BTCPayServerPlugins.git
synced 2025-12-17 07:34:24 +01:00
add bitcoin switch plugin
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="Build Solution" enabled="true" />
|
<option name="Build Solution" enabled="true" />
|
||||||
|
<option name="RunConfigurationTask" enabled="true" run_configuration_name="ConfigBuilder" run_configuration_type="DotNetProject" />
|
||||||
</method>
|
</method>
|
||||||
</configuration>
|
</configuration>
|
||||||
</component>
|
</component>
|
||||||
@@ -61,6 +61,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.MicroN
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.Subscriptions", "Plugins\BTCPayServer.Plugins.Subscriptions\BTCPayServer.Plugins.Subscriptions.csproj", "{994E5D32-849B-4276-82A9-2A18DBC98D39}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.Subscriptions", "Plugins\BTCPayServer.Plugins.Subscriptions\BTCPayServer.Plugins.Subscriptions.csproj", "{994E5D32-849B-4276-82A9-2A18DBC98D39}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.BitcoinSwitch", "Plugins\BTCPayServer.Plugins.BitcoinSwitch\BTCPayServer.Plugins.BitcoinSwitch.csproj", "{B4688F11-33F9-41AB-846E-09CE9ECF3E08}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -293,6 +295,14 @@ Global
|
|||||||
{994E5D32-849B-4276-82A9-2A18DBC98D39}.Altcoins-Debug|Any CPU.Build.0 = Debug|Any CPU
|
{994E5D32-849B-4276-82A9-2A18DBC98D39}.Altcoins-Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{994E5D32-849B-4276-82A9-2A18DBC98D39}.Altcoins-Release|Any CPU.ActiveCfg = Debug|Any CPU
|
{994E5D32-849B-4276-82A9-2A18DBC98D39}.Altcoins-Release|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{994E5D32-849B-4276-82A9-2A18DBC98D39}.Altcoins-Release|Any CPU.Build.0 = Debug|Any CPU
|
{994E5D32-849B-4276-82A9-2A18DBC98D39}.Altcoins-Release|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B4688F11-33F9-41AB-846E-09CE9ECF3E08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{B4688F11-33F9-41AB-846E-09CE9ECF3E08}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B4688F11-33F9-41AB-846E-09CE9ECF3E08}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{B4688F11-33F9-41AB-846E-09CE9ECF3E08}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B4688F11-33F9-41AB-846E-09CE9ECF3E08}.Altcoins-Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{B4688F11-33F9-41AB-846E-09CE9ECF3E08}.Altcoins-Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B4688F11-33F9-41AB-846E-09CE9ECF3E08}.Altcoins-Release|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{B4688F11-33F9-41AB-846E-09CE9ECF3E08}.Altcoins-Release|Any CPU.Build.0 = Debug|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{B19C9F52-DC47-466D-8B5C-2D202B7B003F} = {9E04ECE9-E304-4FF2-9CBC-83256E6C6962}
|
{B19C9F52-DC47-466D-8B5C-2D202B7B003F} = {9E04ECE9-E304-4FF2-9CBC-83256E6C6962}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<LangVersion>10</LangVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
<!-- -->
|
||||||
|
<!-- Plugin specific properties -->
|
||||||
|
<PropertyGroup>
|
||||||
|
<Product>Bitcoin Switch</Product>
|
||||||
|
<Description>Control harwdare using the POS as a switch</Description>
|
||||||
|
<Version>1.0.0</Version>
|
||||||
|
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||||
|
</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\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<AdditionalFiles Include="Views\Shared\BitcoinSwitch\FileSellerTemplateEditorItemDetail.cshtml" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Plugins.FileSeller;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Plugins.BitcoinSwitch;
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
public class BitcoinSwitchController:ControllerBase
|
||||||
|
{
|
||||||
|
private readonly BitcoinSwitchService _service;
|
||||||
|
|
||||||
|
public BitcoinSwitchController(BitcoinSwitchService service)
|
||||||
|
{
|
||||||
|
_service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("~/apps/{id}/pos/bitcoinswitch")]
|
||||||
|
public async Task Connect(string id)
|
||||||
|
{
|
||||||
|
if (HttpContext.WebSockets.IsWebSocketRequest)
|
||||||
|
{
|
||||||
|
using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||||
|
|
||||||
|
await Echo(id, webSocket);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Echo(string id, WebSocket webSocket)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_service.AppToSockets.Add(id, webSocket);
|
||||||
|
var buffer = new byte[1024 * 4];
|
||||||
|
var receiveResult = await webSocket.ReceiveAsync(
|
||||||
|
new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||||
|
|
||||||
|
while (!receiveResult.CloseStatus.HasValue && webSocket.State == WebSocketState.Open)
|
||||||
|
{
|
||||||
|
|
||||||
|
receiveResult = await webSocket.ReceiveAsync(
|
||||||
|
new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
await webSocket.CloseAsync(
|
||||||
|
receiveResult.CloseStatus.Value,
|
||||||
|
receiveResult.CloseStatusDescription,
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_service.AppToSockets.Remove(id, webSocket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using BTCPayServer.Abstractions.Contracts;
|
||||||
|
using BTCPayServer.Abstractions.Models;
|
||||||
|
using BTCPayServer.Plugins.FileSeller;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Plugins.BitcoinSwitch;
|
||||||
|
|
||||||
|
public class BitcoinSwitchPlugin : BaseBTCPayServerPlugin
|
||||||
|
{
|
||||||
|
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
|
||||||
|
{
|
||||||
|
new() {Identifier = nameof(BTCPayServer), Condition = ">=2.0.0"}
|
||||||
|
};
|
||||||
|
|
||||||
|
public override void Execute(IServiceCollection applicationBuilder)
|
||||||
|
{
|
||||||
|
applicationBuilder.AddSingleton<BitcoinSwitchService>();
|
||||||
|
applicationBuilder.AddHostedService<BitcoinSwitchService>(provider => provider.GetRequiredService<BitcoinSwitchService>());
|
||||||
|
applicationBuilder.AddUIExtension("app-template-editor-item-detail", "BitcoinSwitch/BitcoinSwitchPluginTemplateEditorItemDetail");
|
||||||
|
|
||||||
|
base.Execute(applicationBuilder);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Client.Models;
|
||||||
|
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 BitcoinSwitchEvent
|
||||||
|
{
|
||||||
|
public string AppId { get; set; }
|
||||||
|
public string Message { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class BitcoinSwitchService : EventHostedServiceBase
|
||||||
|
{
|
||||||
|
private readonly AppService _appService;
|
||||||
|
private readonly InvoiceRepository _invoiceRepository;
|
||||||
|
public BitcoinSwitchService(EventAggregator eventAggregator,
|
||||||
|
ILogger<BitcoinSwitchService> logger,
|
||||||
|
AppService appService,
|
||||||
|
InvoiceRepository invoiceRepository) : base(eventAggregator, logger)
|
||||||
|
{
|
||||||
|
_appService = appService;
|
||||||
|
_invoiceRepository = invoiceRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConcurrentMultiDictionary<string, WebSocket> AppToSockets { get; } = new();
|
||||||
|
|
||||||
|
|
||||||
|
protected override void SubscribeToEvents()
|
||||||
|
{
|
||||||
|
Subscribe<InvoiceEvent>();
|
||||||
|
Subscribe<BitcoinSwitchEvent>();
|
||||||
|
base.SubscribeToEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (evt is BitcoinSwitchEvent bitcoinSwitchEvent)
|
||||||
|
{
|
||||||
|
if (AppToSockets.TryGetValues(bitcoinSwitchEvent.AppId, out var sockets))
|
||||||
|
{
|
||||||
|
foreach (var socket in sockets)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await socket.SendAsync(
|
||||||
|
new ArraySegment<byte>(System.Text.Encoding.UTF8.GetBytes(bitcoinSwitchEvent.Message)),
|
||||||
|
WebSocketMessageType.Text, true, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evt is not InvoiceEvent invoiceEvent) return;
|
||||||
|
List<AppCartItem> 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("bitcoinswitchactivated", out var activated))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((!string.IsNullOrEmpty(invoiceEvent.Invoice.Metadata.ItemCode) ||
|
||||||
|
AppService.TryParsePosCartItems(invoiceEvent.Invoice.Metadata.PosData, out cartItems)))
|
||||||
|
{
|
||||||
|
var items = cartItems ?? new List<AppCartItem>();
|
||||||
|
if (!string.IsNullOrEmpty(invoiceEvent.Invoice.Metadata.ItemCode) &&
|
||||||
|
!items.Exists(cartItem => cartItem.Id == invoiceEvent.Invoice.Metadata.ItemCode))
|
||||||
|
{
|
||||||
|
items.Add(new AppCartItem()
|
||||||
|
{
|
||||||
|
Id = invoiceEvent.Invoice.Metadata.ItemCode,
|
||||||
|
Count = 1,
|
||||||
|
Price = invoiceEvent.Invoice.Price
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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("bitcoinswitch_gpio") is true &&
|
||||||
|
items.Exists(cartItem => cartItem.Id == item.Id)));
|
||||||
|
|
||||||
|
|
||||||
|
foreach (var valueTuple in apps)
|
||||||
|
{
|
||||||
|
foreach (var item1 in valueTuple.Items.Where(item =>
|
||||||
|
item.AdditionalData?.ContainsKey("bitcoinswitch_gpio") is true &&
|
||||||
|
items.Exists(cartItem => cartItem.Id == item.Id)))
|
||||||
|
{
|
||||||
|
var appId = valueTuple.Data.Id;
|
||||||
|
var gpio = item1.AdditionalData["bitcoinswitch_gpio"].Value<string>();
|
||||||
|
var duration = item1.AdditionalData.TryGetValue("bitcoinswitch_duration", out var durationObj) &&
|
||||||
|
durationObj.Type == JTokenType.Integer
|
||||||
|
? durationObj.Value<string>()
|
||||||
|
: "5000";
|
||||||
|
|
||||||
|
PushEvent(new BitcoinSwitchEvent()
|
||||||
|
{
|
||||||
|
AppId = appId,
|
||||||
|
Message = $"{gpio}-{duration}.0"
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
invoiceEvent.Invoice.Metadata.SetAdditionalData("bitcoinswitchactivated", "true");
|
||||||
|
await _invoiceRepository.UpdateInvoiceMetadata(invoiceEvent.InvoiceId, invoiceEvent.Invoice.StoreId,
|
||||||
|
invoiceEvent.Invoice.Metadata.ToJObject());
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
await base.ProcessEvent(evt, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
/// <summary>
|
||||||
|
/// https://stackoverflow.com/a/60719233/275504
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TKey"></typeparam>
|
||||||
|
/// <typeparam name="TValue"></typeparam>
|
||||||
|
public class ConcurrentMultiDictionary<TKey, TValue>
|
||||||
|
: IEnumerable<KeyValuePair<TKey, TValue[]>>
|
||||||
|
{
|
||||||
|
private class Bag : HashSet<TValue>
|
||||||
|
{
|
||||||
|
public bool IsDiscarded { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<TKey, Bag> _dictionary;
|
||||||
|
|
||||||
|
public ConcurrentMultiDictionary()
|
||||||
|
{
|
||||||
|
_dictionary = new ConcurrentDictionary<TKey, Bag>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Count => _dictionary.Count;
|
||||||
|
|
||||||
|
public bool Add(TKey key, TValue value)
|
||||||
|
{
|
||||||
|
var spinWait = new SpinWait();
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var bag = _dictionary.GetOrAdd(key, _ => new Bag());
|
||||||
|
lock (bag)
|
||||||
|
{
|
||||||
|
if (!bag.IsDiscarded) return bag.Add(value);
|
||||||
|
}
|
||||||
|
spinWait.SpinOnce();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AddOrReplace(TKey key, TValue value)
|
||||||
|
{
|
||||||
|
Remove(key, value);
|
||||||
|
return Add(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Remove(TKey key)
|
||||||
|
{
|
||||||
|
return _dictionary.TryRemove(key, out _);
|
||||||
|
}
|
||||||
|
public bool Remove(TKey key, out TValue[]? items)
|
||||||
|
{
|
||||||
|
if(_dictionary.TryRemove(key, out var x))
|
||||||
|
{
|
||||||
|
items = x.ToArray();
|
||||||
|
return true;
|
||||||
|
|
||||||
|
}
|
||||||
|
items = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
public bool Remove(TKey key, TValue value)
|
||||||
|
{
|
||||||
|
var spinWait = new SpinWait();
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (!_dictionary.TryGetValue(key, out var bag)) return false;
|
||||||
|
bool spinAndRetry = false;
|
||||||
|
lock (bag)
|
||||||
|
{
|
||||||
|
if (bag.IsDiscarded)
|
||||||
|
{
|
||||||
|
spinAndRetry = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bool valueRemoved = bag.Remove(value);
|
||||||
|
if (!valueRemoved) return false;
|
||||||
|
if (bag.Count != 0) return true;
|
||||||
|
bag.IsDiscarded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (spinAndRetry) { spinWait.SpinOnce(); continue; }
|
||||||
|
bool keyRemoved = _dictionary.TryRemove(key, out var currentBag);
|
||||||
|
Debug.Assert(keyRemoved, $"Key {key} was not removed");
|
||||||
|
Debug.Assert(bag == currentBag, $"Removed wrong bag");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetValues(TKey key, out TValue[] values)
|
||||||
|
{
|
||||||
|
if (!_dictionary.TryGetValue(key, out var bag)) { values = null; return false; }
|
||||||
|
bool isDiscarded;
|
||||||
|
lock (bag) { isDiscarded = bag.IsDiscarded; values = bag.ToArray(); }
|
||||||
|
if (isDiscarded) { values = null; return false; }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Contains(TKey key, TValue value)
|
||||||
|
{
|
||||||
|
if (!_dictionary.TryGetValue(key, out var bag)) return false;
|
||||||
|
lock (bag) return !bag.IsDiscarded && bag.Contains(value);
|
||||||
|
}
|
||||||
|
public bool Contains(TKey key, IEnumerable<TValue> value)
|
||||||
|
{
|
||||||
|
if (!_dictionary.TryGetValue(key, out var bag)) return false;
|
||||||
|
lock (bag) return !bag.IsDiscarded && value.Any(bag.Contains);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ContainsKey(TKey key) => _dictionary.ContainsKey(key);
|
||||||
|
|
||||||
|
public ICollection<TKey> Keys => _dictionary.Keys;
|
||||||
|
|
||||||
|
public IEnumerator<KeyValuePair<TKey, TValue[]>> GetEnumerator()
|
||||||
|
{
|
||||||
|
foreach (var key in _dictionary.Keys)
|
||||||
|
{
|
||||||
|
if (this.TryGetValues(key, out var values))
|
||||||
|
{
|
||||||
|
yield return new KeyValuePair<TKey, TValue[]>(key, values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||||
|
|
||||||
|
public bool ContainsValue(TValue value)
|
||||||
|
{
|
||||||
|
return _dictionary.Keys.Any(key => Contains(key, value));
|
||||||
|
}
|
||||||
|
public bool ContainsValue(IEnumerable<TValue> value)
|
||||||
|
{
|
||||||
|
return _dictionary.Keys.Any(key => Contains(key, value));
|
||||||
|
}
|
||||||
|
public IEnumerable<TKey> GetKeysContainingValue(IEnumerable<TValue> value)
|
||||||
|
{
|
||||||
|
return _dictionary.Keys.Where(key => Contains(key, value));
|
||||||
|
}
|
||||||
|
public IEnumerable<TKey> GetKeysContainingValue(TValue value)
|
||||||
|
{
|
||||||
|
return _dictionary.Keys.Where(key => Contains(key, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Relay
|
||||||
|
{
|
||||||
|
public static class MultiValueDictionaryExtensions
|
||||||
|
{
|
||||||
|
public static ConcurrentMultiDictionary<TKey, TValue> ToMultiValueDictionary<TInput, TKey, TValue>(this IEnumerable<TInput> collection, Func<TInput, TKey> keySelector, Func<TInput, TValue> valueSelector)
|
||||||
|
{
|
||||||
|
var dictionary = new ConcurrentMultiDictionary<TKey, TValue>();
|
||||||
|
foreach (var item in collection)
|
||||||
|
{
|
||||||
|
dictionary.Add(keySelector(item), valueSelector(item));
|
||||||
|
}
|
||||||
|
return dictionary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
Plugins/BTCPayServer.Plugins.BitcoinSwitch/README.md
Normal file
17
Plugins/BTCPayServer.Plugins.BitcoinSwitch/README.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Biotcoin Switch Plugin
|
||||||
|
|
||||||
|
This plugin allows you to connect your BTCPay Server to the Bitcoin Switch hardware, developed by the amazing LNURL team.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Go to the "Plugins" page of your BTCPay Server
|
||||||
|
2. Click on install under the "Bitcoin Switch" plugin listing
|
||||||
|
3. Restart BTCPay Server
|
||||||
|
4. Go to your point of sale or crowdfund app settings
|
||||||
|
5. Click on "Edit" on the item/perk you'd like to enable Bitcoin Switch for.
|
||||||
|
6. Specify the hardware GPIO pin and duration of activation
|
||||||
|
7. Close the editor
|
||||||
|
8. Choose Print Display in Point of Sale style (if you want to be able to print LNURL QR codes for each item specifically).
|
||||||
|
9. Save the app
|
||||||
|
10. Your websocket url is the point of sale url, appended with "/bitcoinswitch" and the scheme set to wss:// (e.g. wss://mybtcpay.com/apps/A9xD2nxuWzQTh33E9U6YvyyXrvA/pos/bitcoinswitch)
|
||||||
|
11. Upon purchase (invoice marked as settled), any open websockets will receive the message to activate (io-duration)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
<template v-if="editingItem">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Bitcoin Switch GPIO</label>
|
||||||
|
<input type="number" inputmode="numeric" min="0" step="1" class="form-control mb-2" :value="editingItem['bitcoinswitch_gpio'] || ''" v-on:change="if(event.target.value) Vue.set(editingItem, 'bitcoinswitch_gpio', event.target.value); else Vue.delete(editingItem, 'bitcoinswitch_gpio');"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" v-if="editingItem['bitcoinswitch_gpio']">
|
||||||
|
<label class="form-label">Bitcoin Switch Duration</label>
|
||||||
|
<input type="number" inputmode="numeric" min="1" step="1000" class="form-control mb-2" :value="editingItem['bitcoinswitch_duration'] || ''" asp-items="files" class="form-select w-auto" v-on:change="if(event.target.value) Vue.set(editingItem, 'bitcoinswitch_duration', event.target.value); else Vue.delete(editingItem, 'bitcoinswitch_duration');"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -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