mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-01-18 13:26:47 +01:00
Plugins flexibility PR (#2129)
* Plugins flexibility PR * Makes the BTCPayServerOptions.LoadArgs async to support Plugin hooks and actions * relax the private set modifiers in the BTCPayNetwork * Separate IPluginHookService from PluginService to reduce dependencies on DI * fix some small bugs around image path * Fix bug with new dbreeze migration where data dir was incorrect * Update BTCPayServer/Plugins/PluginHookService.cs Co-authored-by: rockstardev <5191402+rockstardev@users.noreply.github.com> * Update BTCPayServer/Plugins/PluginHookService.cs Co-authored-by: rockstardev <5191402+rockstardev@users.noreply.github.com> Co-authored-by: rockstardev <5191402+rockstardev@users.noreply.github.com>
This commit is contained in:
parent
e2e37a0db4
commit
b8da6847b9
@ -1,6 +0,0 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration default="false" name="Build and pack plugins" type="CompoundRunConfigurationType">
|
|
||||||
<toRun name="Pack Test Plugin" type="DotNetProject" />
|
|
||||||
<method v="2" />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
@ -21,7 +21,6 @@ namespace BTCPayServer.Abstractions.Models
|
|||||||
|
|
||||||
public abstract string Description { get; }
|
public abstract string Description { get; }
|
||||||
public bool SystemPlugin { get; set; }
|
public bool SystemPlugin { get; set; }
|
||||||
public bool SystemExtension { get; set; }
|
|
||||||
public virtual IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } = Array.Empty<IBTCPayServerPlugin.PluginDependency>();
|
public virtual IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } = Array.Empty<IBTCPayServerPlugin.PluginDependency>();
|
||||||
|
|
||||||
public virtual void Execute(IApplicationBuilder applicationBuilder,
|
public virtual void Execute(IApplicationBuilder applicationBuilder,
|
||||||
|
@ -5,7 +5,7 @@ namespace BTCPayServer.Abstractions.Services
|
|||||||
{
|
{
|
||||||
public abstract class PluginAction<T>:IPluginHookAction
|
public abstract class PluginAction<T>:IPluginHookAction
|
||||||
{
|
{
|
||||||
public string Hook { get; }
|
public abstract string Hook { get; }
|
||||||
public Task Execute(object args)
|
public Task Execute(object args)
|
||||||
{
|
{
|
||||||
return Execute(args is T args1 ? args1 : default);
|
return Execute(args is T args1 ? args1 : default);
|
||||||
|
@ -5,7 +5,8 @@ namespace BTCPayServer.Abstractions.Services
|
|||||||
{
|
{
|
||||||
public abstract class PluginHookFilter<T>:IPluginHookFilter
|
public abstract class PluginHookFilter<T>:IPluginHookFilter
|
||||||
{
|
{
|
||||||
public string Hook { get; }
|
public abstract string Hook { get; }
|
||||||
|
|
||||||
public Task<object> Execute(object args)
|
public Task<object> Execute(object args)
|
||||||
{
|
{
|
||||||
return Execute(args is T args1 ? args1 : default).ContinueWith(task => task.Result as object);
|
return Execute(args is T args1 ? args1 : default).ContinueWith(task => task.Result as object);
|
||||||
|
@ -53,7 +53,7 @@ namespace BTCPayServer
|
|||||||
public bool SupportRBF { get; internal set; }
|
public bool SupportRBF { get; internal set; }
|
||||||
public string LightningImagePath { get; set; }
|
public string LightningImagePath { get; set; }
|
||||||
public BTCPayDefaultSettings DefaultSettings { get; set; }
|
public BTCPayDefaultSettings DefaultSettings { get; set; }
|
||||||
public KeyPath CoinType { get; internal set; }
|
public KeyPath CoinType { get; set; }
|
||||||
|
|
||||||
public Dictionary<uint, DerivationType> ElectrumMapping = new Dictionary<uint, DerivationType>();
|
public Dictionary<uint, DerivationType> ElectrumMapping = new Dictionary<uint, DerivationType>();
|
||||||
|
|
||||||
@ -132,7 +132,7 @@ namespace BTCPayServer
|
|||||||
{
|
{
|
||||||
private string _blockExplorerLink;
|
private string _blockExplorerLink;
|
||||||
public bool ShowSyncSummary { get; set; } = true;
|
public bool ShowSyncSummary { get; set; } = true;
|
||||||
public string CryptoCode { get; internal set; }
|
public string CryptoCode { get; set; }
|
||||||
|
|
||||||
public string BlockExplorerLink
|
public string BlockExplorerLink
|
||||||
{
|
{
|
||||||
@ -161,7 +161,7 @@ namespace BTCPayServer
|
|||||||
}
|
}
|
||||||
|
|
||||||
public string CryptoImagePath { get; set; }
|
public string CryptoImagePath { get; set; }
|
||||||
public string[] DefaultRateRules { get; internal set; } = Array.Empty<string>();
|
public string[] DefaultRateRules { get; set; } = Array.Empty<string>();
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return CryptoCode;
|
return CryptoCode;
|
||||||
|
@ -8,8 +8,7 @@ namespace BTCPayServer
|
|||||||
{
|
{
|
||||||
public partial class BTCPayNetworkProvider
|
public partial class BTCPayNetworkProvider
|
||||||
{
|
{
|
||||||
readonly Dictionary<string, BTCPayNetworkBase> _Networks = new Dictionary<string, BTCPayNetworkBase>();
|
protected readonly Dictionary<string, BTCPayNetworkBase> _Networks = new Dictionary<string, BTCPayNetworkBase>();
|
||||||
|
|
||||||
|
|
||||||
private readonly NBXplorerNetworkProvider _NBXplorerNetworkProvider;
|
private readonly NBXplorerNetworkProvider _NBXplorerNetworkProvider;
|
||||||
public NBXplorerNetworkProvider NBXplorerNetworkProvider
|
public NBXplorerNetworkProvider NBXplorerNetworkProvider
|
||||||
|
@ -43,6 +43,7 @@ namespace BTCPayServer.PluginPacker
|
|||||||
}
|
}
|
||||||
ZipFile.CreateFromDirectory(directory, outputFile + ".btcpay", CompressionLevel.Optimal, false);
|
ZipFile.CreateFromDirectory(directory, outputFile + ".btcpay", CompressionLevel.Optimal, false);
|
||||||
File.WriteAllText(outputFile + ".btcpay.json", json);
|
File.WriteAllText(outputFile + ".btcpay.json", json);
|
||||||
|
Console.WriteLine($"Created {outputFile}.btcpay at {directory}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Type[] GetAllExtensionTypesFromAssembly(Assembly assembly)
|
private static Type[] GetAllExtensionTypesFromAssembly(Assembly assembly)
|
||||||
|
@ -1186,7 +1186,7 @@ namespace BTCPayServer.Controllers
|
|||||||
var res = paymentMethodId.PaymentType == PaymentTypes.BTCLike
|
var res = paymentMethodId.PaymentType == PaymentTypes.BTCLike
|
||||||
? Url.Content(network.CryptoImagePath)
|
? Url.Content(network.CryptoImagePath)
|
||||||
: Url.Content(network.LightningImagePath);
|
: Url.Content(network.LightningImagePath);
|
||||||
return "/" + res;
|
return Request.GetRelativePathOrAbsolute(res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ using BTCPayServer.Configuration;
|
|||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace BTCPayServer.HostedServices
|
namespace BTCPayServer.HostedServices
|
||||||
{
|
{
|
||||||
@ -18,9 +19,9 @@ namespace BTCPayServer.HostedServices
|
|||||||
private readonly InvoiceRepository _invoiceRepository;
|
private readonly InvoiceRepository _invoiceRepository;
|
||||||
private readonly SettingsRepository _settingsRepository;
|
private readonly SettingsRepository _settingsRepository;
|
||||||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||||
private readonly DataDirectories _datadirs;
|
private readonly IOptions<DataDirectories> _datadirs;
|
||||||
|
|
||||||
public DbMigrationsHostedService(InvoiceRepository invoiceRepository, SettingsRepository settingsRepository, ApplicationDbContextFactory dbContextFactory, DataDirectories datadirs)
|
public DbMigrationsHostedService(InvoiceRepository invoiceRepository, SettingsRepository settingsRepository, ApplicationDbContextFactory dbContextFactory, IOptions<DataDirectories> datadirs)
|
||||||
{
|
{
|
||||||
_invoiceRepository = invoiceRepository;
|
_invoiceRepository = invoiceRepository;
|
||||||
_settingsRepository = settingsRepository;
|
_settingsRepository = settingsRepository;
|
||||||
@ -48,7 +49,7 @@ namespace BTCPayServer.HostedServices
|
|||||||
private async Task MigratedInvoiceTextSearchToDb(int startFromPage)
|
private async Task MigratedInvoiceTextSearchToDb(int startFromPage)
|
||||||
{
|
{
|
||||||
// deleting legacy DBriize database if present
|
// deleting legacy DBriize database if present
|
||||||
var dbpath = Path.Combine(_datadirs.DataDir, "InvoiceDB");
|
var dbpath = Path.Combine(_datadirs.Value.DataDir, "InvoiceDB");
|
||||||
if (Directory.Exists(dbpath))
|
if (Directory.Exists(dbpath))
|
||||||
{
|
{
|
||||||
Directory.Delete(dbpath, true);
|
Directory.Delete(dbpath, true);
|
||||||
|
@ -70,7 +70,6 @@ namespace BTCPayServer.Hosting
|
|||||||
}
|
}
|
||||||
public static IServiceCollection AddBTCPayServer(this IServiceCollection services, IConfiguration configuration)
|
public static IServiceCollection AddBTCPayServer(this IServiceCollection services, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
services.AddSingleton<DataDirectories>();
|
|
||||||
services.AddSingleton<MvcNewtonsoftJsonOptions>(o => o.GetRequiredService<IOptions<MvcNewtonsoftJsonOptions>>().Value);
|
services.AddSingleton<MvcNewtonsoftJsonOptions>(o => o.GetRequiredService<IOptions<MvcNewtonsoftJsonOptions>>().Value);
|
||||||
services.AddDbContext<ApplicationDbContext>((provider, o) =>
|
services.AddDbContext<ApplicationDbContext>((provider, o) =>
|
||||||
{
|
{
|
||||||
@ -244,7 +243,7 @@ namespace BTCPayServer.Hosting
|
|||||||
|
|
||||||
services.TryAddSingleton<AppService>();
|
services.TryAddSingleton<AppService>();
|
||||||
services.AddSingleton<PluginService>();
|
services.AddSingleton<PluginService>();
|
||||||
services.AddSingleton<IPluginHookService>(provider => provider.GetService<PluginService>());
|
services.AddSingleton<IPluginHookService, PluginHookService>();
|
||||||
services.TryAddTransient<Safe>();
|
services.TryAddTransient<Safe>();
|
||||||
services.TryAddSingleton<Ganss.XSS.HtmlSanitizer>(o =>
|
services.TryAddSingleton<Ganss.XSS.HtmlSanitizer>(o =>
|
||||||
{
|
{
|
||||||
|
62
BTCPayServer/Plugins/PluginHookService.cs
Normal file
62
BTCPayServer/Plugins/PluginHookService.cs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Abstractions.Contracts;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Plugins
|
||||||
|
{
|
||||||
|
public class PluginHookService : IPluginHookService
|
||||||
|
{
|
||||||
|
private readonly IEnumerable<IPluginHookAction> _actions;
|
||||||
|
private readonly IEnumerable<IPluginHookFilter> _filters;
|
||||||
|
private readonly ILogger<PluginHookService> _logger;
|
||||||
|
|
||||||
|
public PluginHookService(IEnumerable<IPluginHookAction> actions, IEnumerable<IPluginHookFilter> filters,
|
||||||
|
ILogger<PluginHookService> logger)
|
||||||
|
{
|
||||||
|
_actions = actions;
|
||||||
|
_filters = filters;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger simple action hook for registered plugins
|
||||||
|
public async Task ApplyAction(string hook, object args)
|
||||||
|
{
|
||||||
|
var filters = _actions
|
||||||
|
.Where(filter => filter.Hook.Equals(hook, StringComparison.InvariantCultureIgnoreCase)).ToList();
|
||||||
|
foreach (IPluginHookAction pluginHookFilter in filters)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await pluginHookFilter.Execute(args);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, $"Action on hook {hook} failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger hook on which registered plugins can optionally return modified args or new object back
|
||||||
|
public async Task<object> ApplyFilter(string hook, object args)
|
||||||
|
{
|
||||||
|
var filters = _filters
|
||||||
|
.Where(filter => filter.Hook.Equals(hook, StringComparison.InvariantCultureIgnoreCase)).ToList();
|
||||||
|
foreach (IPluginHookFilter pluginHookFilter in filters)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
args = await pluginHookFilter.Execute(args);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, $"Filter on hook {hook} failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -16,23 +16,19 @@ using Newtonsoft.Json;
|
|||||||
|
|
||||||
namespace BTCPayServer.Plugins
|
namespace BTCPayServer.Plugins
|
||||||
{
|
{
|
||||||
public class PluginService: IPluginHookService
|
public class PluginService
|
||||||
{
|
{
|
||||||
private readonly IOptions<DataDirectories> _datadirs;
|
private readonly IOptions<DataDirectories> _dataDirectories;
|
||||||
private readonly BTCPayServerOptions _options;
|
private readonly BTCPayServerOptions _btcPayServerOptions;
|
||||||
private readonly HttpClient _githubClient;
|
private readonly HttpClient _githubClient;
|
||||||
private readonly IEnumerable<IPluginHookAction> _actions;
|
|
||||||
private readonly IEnumerable<IPluginHookFilter> _filters;
|
|
||||||
public PluginService(IEnumerable<IBTCPayServerPlugin> btcPayServerPlugins,
|
public PluginService(IEnumerable<IBTCPayServerPlugin> btcPayServerPlugins,
|
||||||
IHttpClientFactory httpClientFactory, IOptions<DataDirectories> datadirs, BTCPayServerOptions options, IEnumerable<IPluginHookAction> actions, IEnumerable<IPluginHookFilter> filters)
|
IHttpClientFactory httpClientFactory, BTCPayServerOptions btcPayServerOptions, IOptions<DataDirectories> dataDirectories)
|
||||||
{
|
{
|
||||||
LoadedPlugins = btcPayServerPlugins;
|
LoadedPlugins = btcPayServerPlugins;
|
||||||
_githubClient = httpClientFactory.CreateClient();
|
_githubClient = httpClientFactory.CreateClient();
|
||||||
_githubClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("btcpayserver", "1"));
|
_githubClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("btcpayserver", "1"));
|
||||||
_datadirs = datadirs;
|
_btcPayServerOptions = btcPayServerOptions;
|
||||||
_options = options;
|
_dataDirectories = dataDirectories;
|
||||||
_actions = actions;
|
|
||||||
_filters = filters;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<IBTCPayServerPlugin> LoadedPlugins { get; }
|
public IEnumerable<IBTCPayServerPlugin> LoadedPlugins { get; }
|
||||||
@ -40,7 +36,7 @@ namespace BTCPayServer.Plugins
|
|||||||
public async Task<IEnumerable<AvailablePlugin>> GetRemotePlugins()
|
public async Task<IEnumerable<AvailablePlugin>> GetRemotePlugins()
|
||||||
{
|
{
|
||||||
var resp = await _githubClient
|
var resp = await _githubClient
|
||||||
.GetStringAsync(new Uri($"https://api.github.com/repos/{_options.PluginRemote}/contents"));
|
.GetStringAsync(new Uri($"https://api.github.com/repos/{_btcPayServerOptions.PluginRemote}/contents"));
|
||||||
var files = JsonConvert.DeserializeObject<GithubFile[]>(resp);
|
var files = JsonConvert.DeserializeObject<GithubFile[]>(resp);
|
||||||
return await Task.WhenAll(files.Where(file => file.Name.EndsWith($"{PluginManager.BTCPayPluginSuffix}.json", StringComparison.InvariantCulture)).Select(async file =>
|
return await Task.WhenAll(files.Where(file => file.Name.EndsWith($"{PluginManager.BTCPayPluginSuffix}.json", StringComparison.InvariantCulture)).Select(async file =>
|
||||||
{
|
{
|
||||||
@ -51,9 +47,9 @@ namespace BTCPayServer.Plugins
|
|||||||
|
|
||||||
public async Task DownloadRemotePlugin(string plugin)
|
public async Task DownloadRemotePlugin(string plugin)
|
||||||
{
|
{
|
||||||
var dest = _datadirs.Value.PluginDir;
|
var dest = _dataDirectories.Value.PluginDir;
|
||||||
var resp = await _githubClient
|
var resp = await _githubClient
|
||||||
.GetStringAsync(new Uri($"https://api.github.com/repos/{_options.PluginRemote}/contents"));
|
.GetStringAsync(new Uri($"https://api.github.com/repos/{_btcPayServerOptions.PluginRemote}/contents"));
|
||||||
var files = JsonConvert.DeserializeObject<GithubFile[]>(resp);
|
var files = JsonConvert.DeserializeObject<GithubFile[]>(resp);
|
||||||
var ext = files.SingleOrDefault(file => file.Name == $"{plugin}{PluginManager.BTCPayPluginSuffix}");
|
var ext = files.SingleOrDefault(file => file.Name == $"{plugin}{PluginManager.BTCPayPluginSuffix}");
|
||||||
if (ext is null)
|
if (ext is null)
|
||||||
@ -68,19 +64,19 @@ namespace BTCPayServer.Plugins
|
|||||||
|
|
||||||
public void InstallPlugin(string plugin)
|
public void InstallPlugin(string plugin)
|
||||||
{
|
{
|
||||||
var dest = _datadirs.Value.PluginDir;
|
var dest = _dataDirectories.Value.PluginDir;
|
||||||
UninstallPlugin(plugin);
|
UninstallPlugin(plugin);
|
||||||
PluginManager.QueueCommands(dest, ("install", plugin));
|
PluginManager.QueueCommands(dest, ("install", plugin));
|
||||||
}
|
}
|
||||||
public void UpdatePlugin(string plugin)
|
public void UpdatePlugin(string plugin)
|
||||||
{
|
{
|
||||||
var dest = _datadirs.Value.PluginDir;
|
var dest = _dataDirectories.Value.PluginDir;
|
||||||
PluginManager.QueueCommands(dest, ("update", plugin));
|
PluginManager.QueueCommands(dest, ("update", plugin));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UploadPlugin(IFormFile plugin)
|
public async Task UploadPlugin(IFormFile plugin)
|
||||||
{
|
{
|
||||||
var dest = _datadirs.Value.PluginDir;
|
var dest = _dataDirectories.Value.PluginDir;
|
||||||
var filedest = Path.Combine(dest, plugin.FileName);
|
var filedest = Path.Combine(dest, plugin.FileName);
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(filedest));
|
Directory.CreateDirectory(Path.GetDirectoryName(filedest));
|
||||||
if (Path.GetExtension(filedest) == PluginManager.BTCPayPluginSuffix)
|
if (Path.GetExtension(filedest) == PluginManager.BTCPayPluginSuffix)
|
||||||
@ -92,7 +88,7 @@ namespace BTCPayServer.Plugins
|
|||||||
|
|
||||||
public void UninstallPlugin(string plugin)
|
public void UninstallPlugin(string plugin)
|
||||||
{
|
{
|
||||||
var dest = _datadirs.Value.PluginDir;
|
var dest = _dataDirectories.Value.PluginDir;
|
||||||
PluginManager.QueueCommands(dest, ("delete", plugin));
|
PluginManager.QueueCommands(dest, ("delete", plugin));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,34 +123,12 @@ namespace BTCPayServer.Plugins
|
|||||||
|
|
||||||
public (string command, string plugin)[] GetPendingCommands()
|
public (string command, string plugin)[] GetPendingCommands()
|
||||||
{
|
{
|
||||||
return PluginManager.GetPendingCommands(_datadirs.Value.PluginDir);
|
return PluginManager.GetPendingCommands(_dataDirectories.Value.PluginDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CancelCommands(string plugin)
|
public void CancelCommands(string plugin)
|
||||||
{
|
{
|
||||||
PluginManager.CancelCommands(_datadirs.Value.PluginDir, plugin);
|
PluginManager.CancelCommands(_dataDirectories.Value.PluginDir, plugin);
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ApplyAction(string hook, object args)
|
|
||||||
{
|
|
||||||
var filters = _actions
|
|
||||||
.Where(filter => filter.Hook.Equals(hook, StringComparison.InvariantCultureIgnoreCase)).ToList();
|
|
||||||
foreach (IPluginHookAction pluginHookFilter in filters)
|
|
||||||
{
|
|
||||||
await pluginHookFilter.Execute(args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<object> ApplyFilter(string hook, object args)
|
|
||||||
{
|
|
||||||
var filters = _filters
|
|
||||||
.Where(filter => filter.Hook.Equals(hook, StringComparison.InvariantCultureIgnoreCase)).ToList();
|
|
||||||
foreach (IPluginHookFilter pluginHookFilter in filters)
|
|
||||||
{
|
|
||||||
args = await pluginHookFilter.Execute(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
return args;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -278,5 +278,6 @@ Global
|
|||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{545AFC8E-7BC2-43D9-84CA-F9468F4FF295} = {1FC7F660-ADF1-4D55-B61A-85C6AB083C33}
|
{545AFC8E-7BC2-43D9-84CA-F9468F4FF295} = {1FC7F660-ADF1-4D55-B61A-85C6AB083C33}
|
||||||
{7DC94B25-1CFC-4170-AA41-7BA983E4C0B8} = {1FC7F660-ADF1-4D55-B61A-85C6AB083C33}
|
{7DC94B25-1CFC-4170-AA41-7BA983E4C0B8} = {1FC7F660-ADF1-4D55-B61A-85C6AB083C33}
|
||||||
|
{3E7D4925-0CA6-4A86-87BA-5BD00E5AA4A5} = {1FC7F660-ADF1-4D55-B61A-85C6AB083C33}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
Loading…
Reference in New Issue
Block a user