Initial Release

This commit is contained in:
Adil El Farissi
2023-10-15 20:42:51 +00:00
commit acd03a4b42
31 changed files with 4444 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto
+454
View File
@@ -0,0 +1,454 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# Tye
.tye/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
##
## Visual studio for Mac
##
# globs
Makefile.in
*.userprefs
*.usertasks
config.make
config.status
aclocal.m4
install-sh
autom4te.cache/
*.tar.gz
tarballs/
test-results/
# Mac bundle stuff
*.dmg
*.app
# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# JetBrains Rider
.idea/
*.sln.iml
##
## Visual Studio Code
##
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
+3
View File
@@ -0,0 +1,3 @@
[submodule "btcpayserver"]
path = btcpayserver
url = https://github.com/btcpayserver/btcpayserver
@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>10</LangVersion>
</PropertyGroup>
<!-- Plugin specific properties -->
<PropertyGroup>
<Product>OpenSimulator Plugin For BTCPay</Product>
<Description>The OpenSimulator Plugin implement a permissioned public interface to allow safe interactions with the OpenSimulator's virtual worlds LSL scripted objects without exposing sensive data like the Greenfield API keys.</Description>
<Version>0.1.1</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>
<ProjectReference Include="..\btcpayserver\BTCPayServer\BTCPayServer.csproj" />
<EmbeddedResource Include="Resources\**" />
</ItemGroup>
<ItemGroup>
<Folder Include="Resources" />
</ItemGroup>
</Project>
@@ -0,0 +1,25 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BTCPayServer.Plugins.OpenSimulator", "BTCPayServer.Plugins.OpenSimulator.csproj", "{CE4CEC2C-435F-403B-9624-67AE42687DA4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{CE4CEC2C-435F-403B-9624-67AE42687DA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CE4CEC2C-435F-403B-9624-67AE42687DA4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CE4CEC2C-435F-403B-9624-67AE42687DA4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CE4CEC2C-435F-403B-9624-67AE42687DA4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2E20C937-22FE-4F86-8351-773B86CDD0DB}
EndGlobalSection
EndGlobal
@@ -0,0 +1,89 @@
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Plugins.OpenSimulator.Models;
using BTCPayServer.Plugins.OpenSimulator.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Plugins.OpenSimulator;
[Route("stores")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[AutoValidateAntiforgeryToken]
public class UIOpenSimulatorController : Controller
{
private readonly OpenSimulatorService _OpenSimulatorService;
public UIOpenSimulatorController(OpenSimulatorService OpenSimService)
{
_OpenSimulatorService = OpenSimService;
}
[HttpGet("{storeId}/plugins/opensim")]
public async Task<IActionResult> Index()
{
return View(new OpenSimulatorPageViewModel {
Data = await _OpenSimulatorService.GetAuthorizations(HttpContext.GetCurrentStoreId()),
StoreID = HttpContext.GetCurrentStoreId(),
StoreDefaultPaymetMethod = HttpContext.GetStoreData().GetDefaultPaymentId().ToString(),
StoreDefaultCurrency = HttpContext.GetStoreData().GetStoreBlob().DefaultCurrency,
ServerIP = HttpContext.Connection.LocalIpAddress.ToString()
});
}
[HttpPost("{storeId}/plugins/opensim")]
public async Task<IActionResult> AuthorizationsManager([FromForm] OpenSimAuthorizationFormData model)
{
if(string.IsNullOrEmpty(model.Id))
ModelState.AddModelError("Store", "Missing Id parameter.");
if(string.IsNullOrEmpty(model.StoreId) || HttpContext.GetCurrentStoreId() != model.StoreId)
ModelState.AddModelError("Store", "Missing or invalid StoreId parameter.");
if(string.IsNullOrEmpty(model.AvatarId))
ModelState.AddModelError("Store", "Missing AvatarId parameter.");
if(string.IsNullOrEmpty(model.ObjectId))
ModelState.AddModelError("Store", "Missing ObjectId parameter.");
if (string.IsNullOrEmpty(model.Task))
ModelState.AddModelError("Store", "Missing Task parameter.");
if (!ModelState.IsValid)
return View();
if(model.Task == "remove"){
var a = await _OpenSimulatorService.RemoveObjectAuthorization(model);
if(a > 0){
return Json(new{
status = "removed",
statusDesc = "Authorization successfully removed."
});
}else{
return Json(new{
status = "failed to remove",
statusDesc = "The removal of the authorization for object UUID : "+ model.ObjectId +" failed."
});
}
}else if(model.Task == "authorize"){
var auth = await _OpenSimulatorService.AuthorizeObject(model);
if(auth > 0){
return Json(new{
status = "authorized",
statusDesc = "Authorization successfully done."
});
}else{
return Json(new{
status = "failed to authorize",
statusDesc = "The authorization for object UUID : "+ model.ObjectId +" failed."
});
}
}
return null;
}
}
@@ -0,0 +1,242 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using BTCPayServer.Controllers.Greenfield;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using NicolasDorier.RateLimits;
using BTCPayServer.Plugins.OpenSimulator.Models;
using BTCPayServer.Plugins.OpenSimulator.Services;
namespace BTCPayServer.Plugins.OpenSimulator.Controllers
{
public class UIOpenSimulatorPublicController : Controller
{
public UIOpenSimulatorPublicController(UIInvoiceController invoiceController,
StoreRepository storeRepository, OpenSimulatorService openSimService, LinkGenerator linkGenerator)
{
_InvoiceController = invoiceController;
_StoreRepository = storeRepository;
_OpenSimulatorService = openSimService;
_linkGenerator = linkGenerator;
}
private readonly UIInvoiceController _InvoiceController;
private readonly StoreRepository _StoreRepository;
private readonly OpenSimulatorService _OpenSimulatorService;
private readonly LinkGenerator _linkGenerator;
[HttpPost]
[Route("opensim/authorization")]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
public async Task<IActionResult> OpenSimHandle([FromForm(Name = "action")] string action, [FromHeader] OpenSimAuthorizationData obj ,CancellationToken cancellationToken)
{
var store = await _StoreRepository.FindStore(obj.StoreId);
if (store == null)
return Json(new {
ospError = "Invalid store"
});
if (obj == null || string.IsNullOrEmpty(obj.AvatarId) || !Request.Headers.ContainsKey("x-secondlife-shard"))
return Json(new {
ospError = "This endpoint interact only with 3D objects from the OpenSimulator virtual worlds using LSL scripts."
});
var count = await _OpenSimulatorService.GetPendingAuthorizationsCount(obj.StoreId, obj.AvatarId);
if ( obj != null && count >= 3)
return Json(new {
ospError = "Limits: You have already 3 objects waiting your authorization. Please, authorize or delete this 3 objects and retry."
});
if (string.IsNullOrEmpty(action))
return Json(new {
ospError = "Missing (action) parameter."
});
switch(action){
case "register":
try
{
var e = await _OpenSimulatorService.CheckIfExists(obj.StoreId, obj.AvatarId, obj.ObjectId);
if( e == null){
var registred = await _OpenSimulatorService.AddNewObjectAuthorization(obj);
e = await _OpenSimulatorService.CheckIfExists(obj.StoreId, obj.AvatarId, obj.ObjectId);
if(registred && e != null){
return Json(new {
status = "success",
authorized = e.ObjectAuthorization.ToString()
});
}
else {
return Json(new {
status = "fail",
authorized = "False"
});
}
}
else {
return Json(new {
status = "registred",
authorized = e.ObjectAuthorization.ToString()
});
}
}
catch (ArgumentNullException e)
{
return Json(new {
ospError = e.Message
});
}
catch (OperationCanceledException e)
{
return Json(new {
ospError = e.Message
});
}
case "check":
try
{
var o = await _OpenSimulatorService.CheckIfExists(obj.StoreId, obj.AvatarId, obj.ObjectId);
if(o != null && o.ObjectId != null){
return Json(new {
status = "registred",
authorized = o.ObjectAuthorization.ToString()
});
}
else {
return Json(new {
status = "unknown"
});
}
}
catch (ArgumentNullException e)
{
return Json(new {
ospError = e.Message
});
}
catch (OperationCanceledException e)
{
return Json(new {
ospError = e.Message
});
}
default:
return Ok();
}
}
[HttpGet]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
[Route("opensim/invoices")]
public async Task<IActionResult> OpenSimInvoiceHandle(OpenSimulatorInvoiceModel model)
{
return await OpenSimInvoiceHandle(model, null, CancellationToken.None);
}
[HttpPost]
[Route("opensim/invoices")]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
[RateLimitsFilter(ZoneLimits.PublicInvoices, Scope = RateLimitsScope.RemoteAddress)]
public async Task<IActionResult> OpenSimInvoiceHandle([FromForm] OpenSimulatorInvoiceModel model, [FromHeader] OpenSimAuthorizationData obj ,CancellationToken cancellationToken)
{
var store = await _StoreRepository.FindStore(model.StoreId);
if (store == null)
return Json(new {
ospError = "Invalid store"
});
var a = await _OpenSimulatorService.isAuthorized(obj.StoreId, obj.AvatarId, obj.ObjectId, obj.AvatarHomeURL, obj.ObjectURL);
if(a == null)
return Json(new {
ospError = "This avatar or object is not authorized to create invoices in this store."
});
if (model == null || (model.Price is decimal v ? v <= 0 : false))
return Json(new {
ospError = "Price must be greater than 0"
});
if (model.PosData is not null && model.PosData.Length > 500 )
return Json(new {
ospError = "PosData must be under 500 characters"
});
if (string.IsNullOrEmpty(model.ServerIpn))
return Json(new {
ospError = "Missing LSL script endpoint URL"
});
if (string.IsNullOrEmpty(model.OrderId))
return Json(new {
ospError = "Missing the Order ID"
});
InvoiceEntity invoice = null;
try
{
invoice = await _InvoiceController.CreateInvoiceCoreRaw(new CreateInvoiceRequest()
{
Amount = model.Price,
Currency = model.Currency,
Metadata = new InvoiceMetadata()
{
ItemDesc = model.CheckoutDesc,
OrderId = model.OrderId,
PosDataLegacy = model.PosData
}.ToJObject(),
Checkout = new ()
{
RedirectURL = model.BrowserRedirect ?? store?.StoreWebsite,
DefaultPaymentMethod = model.DefaultPaymentMethod
}
}, store, HttpContext.Request.GetAbsoluteRoot(),
entityManipulator: (entity) =>
{
entity.NotificationEmail = model.NotifyEmail;
entity.NotificationURLTemplate = model.ServerIpn;
entity.FullNotifications = true;
},
cancellationToken: cancellationToken);
}
catch (BitpayHttpException e)
{
return Json(new{
ospError = e.Message
});
}
var url = GreenfieldInvoiceController.ToModel(invoice, _linkGenerator, HttpContext.Request).CheckoutLink;
if (!string.IsNullOrEmpty(model.CheckoutQueryString))
{
var additionalParamValues = HttpUtility.ParseQueryString(model.CheckoutQueryString);
var uriBuilder = new UriBuilder(url);
var paramValues = HttpUtility.ParseQueryString(uriBuilder.Query);
paramValues.Add(additionalParamValues);
uriBuilder.Query = paramValues.ToString()!;
url = uriBuilder.Uri.AbsoluteUri;
}
return Json(new
{
InvoiceId = invoice.Id,
InvoiceUrl = url
});
}
}
}
@@ -0,0 +1,26 @@
using System;
using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;
namespace BTCPayServer.Plugins.OpenSimulator.Data;
public class OpenSimulatorData
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public string Id { get; set; }
public string StoreId { get; set; }
public string AvatarName { get; set; }
public string AvatarId { get; set; }
[Url]
public string AvatarHomeURL { get; set; }
public string ObjectName { get; set; }
public string ObjectId { get; set; }
public string ObjectRegion { get; set; }
public string ObjectLocation { get; set; }
[Url]
public string ObjectURL { get; set; }
public bool ObjectAuthorization { get; set; }
public DateTimeOffset Timestamp { get; set; }
}
@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using BTCPayServer.Plugins.OpenSimulator.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace BTCPayServer.Plugins.OpenSimulator;
public class OpenSimulatorDbContext : DbContext
{
private readonly bool _designTime;
public OpenSimulatorDbContext(DbContextOptions<OpenSimulatorDbContext> options, bool designTime = false)
: base(options)
{
_designTime = designTime;
}
public DbSet<OpenSimulatorData> Authorizations { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.HasDefaultSchema("BTCPayServer.Plugins.OpenSimulator");
if (Database.IsSqlite() && !_designTime)
// SQLite does not have proper support for DateTimeOffset via Entity Framework Core, see the limitations
// here: https://docs.microsoft.com/en-us/ef/core/providers/sqlite/limitations#query-limitations
// To work around this, when the Sqlite database provider is used, all model properties of type DateTimeOffset
// use the DateTimeOffsetToBinaryConverter
// Based on: https://github.com/aspnet/EntityFrameworkCore/issues/10784#issuecomment-415769754
// This only supports millisecond precision, but should be sufficient for most use cases.
foreach (IMutableEntityType entityType in modelBuilder.Model.GetEntityTypes())
{
IEnumerable<PropertyInfo> properties = entityType.ClrType.GetProperties()
.Where(p => p.PropertyType == typeof(DateTimeOffset));
foreach (PropertyInfo property in properties)
modelBuilder
.Entity(entityType.Name)
.Property(property.Name)
.HasConversion(new DateTimeOffsetToBinaryConverter());
}
}
}
@@ -0,0 +1,47 @@
using System;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
namespace BTCPayServer.Plugins.OpenSimulator.Migrations
{
[DbContext(typeof(OpenSimulatorDbContext))]
[Migration("20230704154419_Init")]
public partial class Init : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: "BTCPayServer.Plugins.OpenSimulator");
migrationBuilder.CreateTable(
name: "Authorizations",
schema: "BTCPayServer.Plugins.OpenSimulator",
columns: table => new
{
Id = table.Column<string>(nullable: false),
StoreId = table.Column<string>(nullable: false),
AvatarName = table.Column<string>(nullable: false),
AvatarId = table.Column<string>(nullable: false),
AvatarHomeURL = table.Column<string>(nullable: false),
ObjectName = table.Column<string>(nullable: false),
ObjectId = table.Column<string>(nullable: false),
ObjectRegion = table.Column<string>(nullable: false),
ObjectLocation = table.Column<string>(nullable: false),
ObjectURL = table.Column<string>(nullable: false),
ObjectAuthorization = table.Column<bool>(nullable: false),
Timestamp = table.Column<DateTimeOffset>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Authorizations", x => x.Id);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
/* migrationBuilder.DropTable(
name: "Authorizations",
schema: "BTCPayServer.Plugins.OpenSimulator");*/
}
}
}
@@ -0,0 +1,74 @@
// <auto-generated />
using System;
using BTCPayServer.Plugins.OpenSimulator;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace BTCPayServer.Plugins.OpenSimulator.Migrations
{
[DbContext(typeof(OpenSimulatorDbContext))]
partial class OpenSimulatorDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("BTCPayServer.Plugins.OpenSimulator")
.HasAnnotation("ProductVersion", "0.1.1");
modelBuilder.Entity("BTCPayServer.Plugins.OpenSimulator.Data.OpenSimulatorData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("StoreId")
.HasColumnType("TEXT");
b.Property<string>("AvatarName")
.HasColumnType("TEXT");
b.Property<string>("AvatarId")
.HasColumnType("TEXT");
b.Property<string>("AvatarHomeURL")
.HasColumnType("TEXT");
b.Property<string>("ObjectName")
.HasColumnType("TEXT");
b.Property<string>("ObjectId")
.HasColumnType("TEXT");
b.Property<string>("ObjectRegion")
.HasColumnType("TEXT");
b.Property<string>("ObjectLocation")
.HasColumnType("TEXT");
b.Property<string>("ObjectURL")
.HasColumnType("TEXT");
b.Property<bool>("ObjectAuthorization")
.HasColumnType("INTEGER")
.HasDefaultValue(false);
b.Property<DateTimeOffset>("Timestamp")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("StoreId");
b.HasIndex("AvatarId");
b.HasIndex("ObjectId");
b.ToTable("Authorizations");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,50 @@
using System;
using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Plugins.OpenSimulator.Models;
public class OpenSimAuthorizationData
{
public string Id { get; set; }
[Required]
[FromHeader(Name = "x-opensim-store-id")]
public string StoreId { get; set; }
[Required]
[FromHeader(Name = "x-secondlife-owner-name")]
public string AvatarName { get; set; }
[Required]
[FromHeader(Name = "x-secondlife-owner-key")]
public string AvatarId { get; set; }
[Url]
[Required]
[FromHeader(Name = "x-opensim-owner-home-url")]
public string AvatarHomeURL { get; set; }
[Required]
[FromHeader(Name = "x-secondlife-object-name")]
public string ObjectName { get; set; }
[Required]
[FromHeader(Name = "x-secondlife-object-key")]
public string ObjectId { get; set; }
[Required]
[FromHeader(Name = "x-secondlife-region")]
public string ObjectRegion { get; set; }
[Required]
[FromHeader(Name = "x-secondlife-local-position")]
public string ObjectLocation { get; set; }
[Url]
[Required]
[FromHeader(Name = "x-opensim-object-host-url")]
public string ObjectURL { get; set; }
public bool ObjectAuthorization { get; set; }
public string Task { get; set; }
public DateTimeOffset Timestamp { get; set; }
}
@@ -0,0 +1,25 @@
using System;
using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Plugins.OpenSimulator.Models;
public class OpenSimAuthorizationFormData
{
public string Id { get; set; }
public string StoreId { get; set; }
public string AvatarId { get; set; }
public string ObjectId { get; set; }
public bool ObjectAuthorization { get; set; }
public DateTimeOffset Timestamp { get; set; }
public string Task { get; set; }
}
@@ -0,0 +1,35 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BTCPayServer.ModelBinders;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Validation;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Plugins.OpenSimulator.Models
{
public class OpenSimulatorInvoiceModel
{
[ModelBinder(BinderType = typeof(InvariantDecimalModelBinder))]
public decimal? Price { get; set; }
[Required]
public string Currency { get; set; }
public string DefaultPaymentMethod { get; set; }
public PaymentMethodOptionViewModel.Format[] PaymentMethods { get; set; }
public string CheckoutDesc { get; set; }
public string OrderId { get; set; }
[Url]
public string ServerIpn { get; set; }
[Url]
public string BrowserRedirect { get; set; }
[MailboxAddress]
public string NotifyEmail { get; set; }
public string StoreId { get; set; }
public string CheckoutQueryString { get; set; }
public string PosData { get; set; }
}
}
@@ -0,0 +1,12 @@
using System.Collections.Generic;
using BTCPayServer.Plugins.OpenSimulator.Data;
namespace BTCPayServer.Plugins.OpenSimulator.Models;
public class OpenSimulatorPageViewModel
{
public List<OpenSimulatorData> Data { get; set; }
public string StoreID { get; set; }
public string StoreDefaultPaymetMethod { get; set; }
public string StoreDefaultCurrency { get; set; }
public string ServerIP { get; set; }
}
@@ -0,0 +1,51 @@
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Plugins.OpenSimulator.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
namespace BTCPayServer.Plugins.OpenSimulator;
public class OpenSimulatorMigrationRunner : IHostedService
{
private readonly OpenSimulatorDbContextFactory _OpenSimulatorDbContextFactory;
private readonly OpenSimulatorService _OpenSimulatorService;
private readonly ISettingsRepository _settingsRepository;
public OpenSimulatorMigrationRunner(
ISettingsRepository settingsRepository,
OpenSimulatorDbContextFactory OpenSimulatorDbContextFactory,
OpenSimulatorService OpenSimulatorService)
{
_settingsRepository = settingsRepository;
_OpenSimulatorDbContextFactory = OpenSimulatorDbContextFactory;
_OpenSimulatorService = OpenSimulatorService;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
OpenSimulatorDataMigrationHistory settings = await _settingsRepository.GetSettingAsync<OpenSimulatorDataMigrationHistory>() ??
new OpenSimulatorDataMigrationHistory();
await using var ctx = _OpenSimulatorDbContextFactory.CreateContext();
await ctx.Database.MigrateAsync(cancellationToken);
// settings migrations
if (!settings.Updated)
{
settings.Updated = true;
await _settingsRepository.UpdateSetting(settings);
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public class OpenSimulatorDataMigrationHistory
{
public bool Updated { get; set; }
}
}
@@ -0,0 +1,6 @@
namespace BTCPayServer.Plugins.OpenSimulator.Views;
public enum OpenSimulatorNavPages
{
Index
}
@@ -0,0 +1,28 @@
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Abstractions.Services;
using BTCPayServer.Plugins.OpenSimulator.Services;
using Microsoft.Extensions.DependencyInjection;
namespace BTCPayServer.Plugins.OpenSimulator;
public class OpenSimulatorPlugin : BaseBTCPayServerPlugin
{
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
{
new() { Identifier = nameof(BTCPayServer), Condition = ">=1.11.6" }
};
public override void Execute(IServiceCollection services)
{
services.AddSingleton<IUIExtension>(new UIExtension("OpenSimulatorHeaderNav", "header-nav"));
services.AddHostedService<OpenSimulatorMigrationRunner>();
services.AddSingleton<OpenSimulatorService>();
services.AddSingleton<OpenSimulatorDbContextFactory>();
services.AddDbContext<OpenSimulatorDbContext>((provider, o) =>
{
OpenSimulatorDbContextFactory factory = provider.GetRequiredService<OpenSimulatorDbContextFactory>();
factory.ConfigureBuilder(o);
});
}
}
@@ -0,0 +1,158 @@
#pendingAutorizations, .bg-red{
margin-top: 0px; border-top: 1px solid rgb(70, 0, 0); background-color: rgb(20, 0, 0); color: #fff;
}
#authorizedObjects, .bg-green{
margin-top: 0px; border-top: 1px solid rgb(0, 70, 0); background-color: rgb(0, 20, 0); color: #fff;
}
#pendingAutorizations thead tr th, #authorizedObjects thead tr th{
font-weight: bold;
color: #fff;
text-shadow: 1px 1px 1px #000;
}
#pendingAutorizations, #authorizedObjects, #pendingAutorizations td, #authorizedObjects td, .form-label{
color: #eee;
}
#pendingAutorizations a, #authorizedObjects a{
color: #ff0;
}
#pendingAutorizations a:hover, #authorizedObjects a:hover{
color: rgb(0, 255, 0);
}
.badge{
cursor: pointer;
margin-right: 5px;
color:#fff;
text-shadow: 1px 1px 1px #000;
font-weight: bold;
}
.badge-settled, .sNav-bg{
background: #003300; /* Old browsers */
background: -moz-linear-gradient(bottom, #003300 0%, #00AE00 100%); /* FF3.6+ */
background: -webkit-gradient(linear, bottom, top, color-stop(0%,#003300), color-stop(100%,#00AE00)); /* Chrome,Safari4+ */
background: -webkit-linear-gradient(bottom, #003300 0%,#00AE00 100%); /* Chrome10+,Safari5.1+ */
background: -o-linear-gradient(bottom, #003300 0%,#00AE00 100%); /* Opera 11.10+ */
background: -ms-linear-gradient(bottom, #003300 0%,#00AE00 100%); /* IE10+ */
background: linear-gradient(to top, #003300 0%,#00AE00 100%); /* W3C */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#003300', endColorstr='#00AE00',GradientType=1 ); /* IE6-9 */
}
.badge-invalid{
background-color: #f56954 !important;
background: #f00; /* Old browsers */
background: -moz-linear-gradient(bottom, #f00 0%, #370101 100%); /* FF3.6+ */
background: -webkit-gradient(linear, bottom, top, color-stop(0%,#f00), color-stop(100%,#370101)); /* Chrome,Safari4+ */
background: -webkit-linear-gradient(bottom, #f00 0%,#370101 100%); /* Chrome10+,Safari5.1+ */
background: -o-linear-gradient(bottom, #f00 0%,#370101 100%); /* Opera 11.10+ */
background: -ms-linear-gradient(bottom, #f00 0%,#370101 100%); /* IE10+ */
background: linear-gradient(to bottom, #f00 0%,#370101 100%); /* W3C */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f00', endColorstr='#370101',GradientType=1 ); /* IE6-9 */
}
.badge-processing, .bg-yellow, #scriptsNav li:hover{
background-color: #f39c12 !important;
background: -moz-linear-gradient(bottom, #f39c12 0%, #875504 100%); /* FF3.6+ */
background: -webkit-gradient(linear, bottom, top, color-stop(0%,#f39c12), color-stop(100%,#875504)); /* Chrome,Safari4+ */
background: -webkit-linear-gradient(bottom, #f39c12 0%,#875504 100%); /* Chrome10+,Safari5.1+ */
background: -o-linear-gradient(bottom, #f39c12 0%,#875504 100%); /* Opera 11.10+ */
background: -ms-linear-gradient(bottom, #f39c12 0%,#875504 100%); /* IE10+ */
background: linear-gradient(to bottom, #f39c12 0%,#875504 100%); /* W3C */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f39c12', endColorstr='#875504',GradientType=1 ); /* IE6-9 */
}
.badge-info, .bg-aqua{
background-color: #00c0ef !important;
background: -moz-linear-gradient(bottom, #00c0ef 0%, #007682 100%); /* FF3.6+ */
background: -webkit-gradient(linear, bottom, top, color-stop(0%,#00c0ef), color-stop(100%,#007682)); /* Chrome,Safari4+ */
background: -webkit-linear-gradient(bottom, #00c0ef 0%,#007682 100%); /* Chrome10+,Safari5.1+ */
background: -o-linear-gradient(bottom, #00c0ef 0%,#007682 100%); /* Opera 11.10+ */
background: -ms-linear-gradient(bottom, #00c0ef 0%,#007682 100%); /* IE10+ */
background: linear-gradient(to bottom, #00c0ef 0%,#007682 100%); /* W3C */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#00c0ef', endColorstr='#007682',GradientType=1 ); /* IE6-9 */
}
button{border: none;}
.btn{border: 1px solid #030;}
.tableTitle{
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
margin-left: -8px;
}
.tableTitle h6{
margin-bottom: 0;
}
.form-group input, .form-group select{
width: 100%;
}
.labelWarning{
font-size: x-small;
color: #f00;
}
#scriptsNav{
margin: 0;
padding: 0;
}
#scriptsNav li{
display: inline-block;
padding: 5px 15px;
color: #fff;
cursor: pointer;
text-shadow: 0px 0px 3px #000;
}
#scriptsNav li:first-child{
border-top-left-radius: 10px;
}
#scriptsNav li:last-child{
border-top-right-radius: 10px;
}
#scriptsNav li .fa{
font-size: 16px;
margin-right: 5px;
}
select, input{
height: 27px;
}
#osDocumentationPage p{
margin-bottom: 15px;
}
#osDocumentationPage pre{
padding: 10px !important;
}
#osDocumentationPage pre code{
padding: 0px !important;
margin: 0;
color: #0f0;
}
#osDocumentationPage pre code a{
color: #ff0;
}
#osDocumentationPage pre code a:hover{
color: #fff;
}
.commented{
color: #f39c12;
}
.objectDetails.collapsing{
transition: none;
}
.sticky-header{
margin-top: 0;
}
.osPage h5{
margin-bottom: 15px;
}
.alert{
border-left: 10px solid #aaa;
color: #000;
font-weight: bold;
margin-left: 10px;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long
@@ -0,0 +1,33 @@
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Options;
namespace BTCPayServer.Plugins.OpenSimulator;
public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<OpenSimulatorDbContext>
{
public OpenSimulatorDbContext CreateDbContext(string[] args)
{
DbContextOptionsBuilder<OpenSimulatorDbContext> builder = new DbContextOptionsBuilder<OpenSimulatorDbContext>();
builder.UseSqlite("Data Source=opensim.db");
return new OpenSimulatorDbContext(builder.Options, true);
}
}
public class OpenSimulatorDbContextFactory : BaseDbContextFactory<OpenSimulatorDbContext>
{
public OpenSimulatorDbContextFactory(IOptions<DatabaseOptions> options) : base(options, "BTCPayServer.Plugins.OpenSimulator")
{
}
public override OpenSimulatorDbContext CreateContext()
{
DbContextOptionsBuilder<OpenSimulatorDbContext> builder = new DbContextOptionsBuilder<OpenSimulatorDbContext>();
ConfigureBuilder(builder);
return new OpenSimulatorDbContext(builder.Options);
}
}
@@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using BTCPayServer.Plugins.OpenSimulator.Data;
using BTCPayServer.Plugins.OpenSimulator.Models;
using Microsoft.EntityFrameworkCore;
namespace BTCPayServer.Plugins.OpenSimulator.Services;
public class OpenSimulatorService
{
private readonly OpenSimulatorDbContextFactory _OpenSimDbContextFactory;
public OpenSimulatorService(OpenSimulatorDbContextFactory OpenSimDbContextFactory)
{
_OpenSimDbContextFactory = OpenSimDbContextFactory;
}
public async Task <bool> AddNewObjectAuthorization(OpenSimAuthorizationData model)
{
await using var context = _OpenSimDbContextFactory.CreateContext();
await context.Authorizations.AddAsync(new OpenSimulatorData {
StoreId = model.StoreId,
AvatarName = model.AvatarName,
AvatarId = model.AvatarId,
AvatarHomeURL = model.AvatarHomeURL,
ObjectName = model.ObjectName,
ObjectId = model.ObjectId,
ObjectRegion = model.ObjectRegion,
ObjectLocation = model.ObjectLocation,
ObjectURL = model.ObjectURL,
ObjectAuthorization = false,
Timestamp = DateTimeOffset.UtcNow });
var res = await context.SaveChangesAsync();
if(res > 0){
return true;
}
else{
return false;
}
}
public async Task <int> RemoveObjectAuthorization(OpenSimAuthorizationFormData model)
{
await using var context = _OpenSimDbContextFactory.CreateContext();
var authorization = await context.FindAsync<OpenSimulatorData>(model.Id);
if (authorization is not null)
context.Remove(authorization);
return await context.SaveChangesAsync();
}
public async Task <int> AuthorizeObject(OpenSimAuthorizationFormData model)
{
await using var context = _OpenSimDbContextFactory.CreateContext();
OpenSimulatorData osdModel = await context.Authorizations.Where(i => i.Id == model.Id && i.StoreId == model.StoreId && i.AvatarId == model.AvatarId && i.ObjectId == model.ObjectId).FirstOrDefaultAsync();
if (string.IsNullOrEmpty(osdModel.Id))
{
return 0;
}
osdModel.ObjectAuthorization = true;
osdModel.Timestamp = DateTimeOffset.UtcNow;
context.Update(osdModel);
return await context.SaveChangesAsync();
}
public async Task<List<OpenSimulatorData>> GetDestinationsGuide()
{
await using var context = _OpenSimDbContextFactory.CreateContext();
return await context.Authorizations.Where(a => a.ObjectAuthorization == true).ToListAsync();
}
public async Task<List<OpenSimulatorData>> GetAuthorizations(string storeId)
{
await using var context = _OpenSimDbContextFactory.CreateContext();
return await context.Authorizations.Where(a => a.StoreId == storeId).ToListAsync();
}
public async Task<int> GetPendingAuthorizationsCount(string storeId, string avatarId)
{
await using var context = _OpenSimDbContextFactory.CreateContext();
return await context.Authorizations.Where(a => a.StoreId == storeId && a.AvatarId == avatarId && a.ObjectAuthorization == false).CountAsync();
}
public async Task<List<OpenSimulatorData>> GetPendingAuthorizations(string storeId, string avatarId, string objectId)
{
await using var context = _OpenSimDbContextFactory.CreateContext();
return await context.Authorizations.Where(a => a.StoreId == storeId && a.AvatarId == avatarId && a.ObjectId == objectId && a.ObjectAuthorization == false).ToListAsync();
}
public async Task<OpenSimulatorData> isAuthorized(string storeId, string avatarId, string objectId, string avatarHomeURL, string objectURL)
{
await using var context = _OpenSimDbContextFactory.CreateContext();
return await context.Authorizations.Where(a => a.StoreId == storeId && a.AvatarId == avatarId && a.ObjectId == objectId && a.AvatarHomeURL == avatarHomeURL && a.ObjectURL == objectURL && a.ObjectAuthorization == true)
.FirstOrDefaultAsync();
}
public async Task <OpenSimulatorData> CheckIfExists(string storeId, string avatarId, string objectId)
{
await using var context = _OpenSimDbContextFactory.CreateContext();
return await context.Authorizations.Where(a => a.StoreId == storeId && a.AvatarId == avatarId && a.ObjectId == objectId)
.FirstOrDefaultAsync();
}
}
@@ -0,0 +1,14 @@
@{
Layout = "_LayoutSimple";
var allErrors = ViewData.ModelState.Values.SelectMany(v => v.Errors.Select(b => b.ErrorMessage));
}
<div class="row">
<div class="col-lg-12">
<h2>OpenSimulator request failed</h2>
@foreach (var error in allErrors)
{
<div class="alert alert-danger mt-4">@error</div>
}
</div>
</div>
@@ -0,0 +1,14 @@
@{
Layout = "_LayoutSimple";
var allErrors = ViewData.ModelState.Values.SelectMany(v => v.Errors.Select(b => b.ErrorMessage));
}
<div class="row">
<div class="col-lg-12">
<h2>OpenSimulator request failed</h2>
@foreach (var error in allErrors)
{
<div class="alert alert-danger mt-4">@error</div>
}
</div>
</div>
@@ -0,0 +1,20 @@
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Plugins.OpenSimulator.Views
@using Microsoft.AspNetCore.Mvc.TagHelpers
@inject IScopeProvider ScopeProvider
@{
var storeId = ScopeProvider.GetCurrentStoreId();
}
@if (storeId != null)
{
<li class="nav-item">
<a asp-controller="UIOpenSimulator" asp-action="Index" asp-route-storeId="@storeId" class="nav-link @ViewData.IsActivePage(OpenSimulatorNavPages.Index)">
<svg role="img" class="icon icon-plugin" viewBox="0 0 24 24">
<path d="M12.0002 10.2354L4.73633 7.38747M12.0002 10.2354L19.2642 7.38747M12.0002 10.2354V19.5M5.21166 7.01614L11.2783 4.6375C11.7412 4.45417 12.2566 4.45417 12.7196 4.6375L18.7862 7.01614C19.0023 7.1083 19.1858 7.26312 19.3131 7.46062C19.4404 7.65812 19.5055 7.88923 19.5002 8.12413V15.876C19.5058 16.1106 19.441 16.3415 19.3142 16.539C19.1874 16.7365 19.0045 16.8915 18.7888 16.984L12.7222 19.3633C12.259 19.5453 11.7441 19.5453 11.2809 19.3633L5.21433 16.984C4.9982 16.8919 4.81466 16.737 4.68739 16.5395C4.56012 16.342 4.49496 16.1109 4.50033 15.876V8.12413C4.49475 7.88953 4.55951 7.65864 4.68628 7.46117C4.81305 7.26371 4.99603 7.10871 5.21166 7.01614Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>OpenSimulator</span>
</a>
</li>
}
@@ -0,0 +1,877 @@
@using System.Text.Json
@using BTCPayServer.Client
@using BTCPayServer.Security
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Plugins.OpenSimulator.Views
@using Microsoft.AspNetCore.Mvc.TagHelpers
@inject ContentSecurityPolicies Csp
@model BTCPayServer.Plugins.OpenSimulator.Models.OpenSimulatorPageViewModel
@{
ViewData.SetActivePage(OpenSimulatorNavPages.Index, "OpenSimulator Plugin");
var nonce = NBitcoin.RandomUtils.GetUInt256().ToString().Substring(0, 32);
Csp.Add("script-src", $"'nonce-{nonce}'");
}
@section PageHeadContent {
<link href="~/Resources/css/osp.css" rel="stylesheet" type="text/css" asp-append-version="true">
}
@section PageFootContent {
<script src="~/Resources/js/opensimulator.js" asp-append-version="true"></script>
}
<img id="opensimLogo" src="~/Resources/img/opensimLogo.png" style="position: relative;top:-10px;width:64px" asp-append-version="true">&nbsp;<h2 style="display: inline;">@ViewData["Title"]</h2>&nbsp;&nbsp;<span class="ospVersion">v0.1.1</span>
<div class="sticky-header mb-l">
<nav id="SectionNav">
<div class="nav">
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(OpenSimulatorNavPages.Index))" class="nav-link @ViewData.IsActivePage(OpenSimulatorNavPages.Index)" asp-controller="UIOpenSimulator" asp-action="Index" asp-route-storeId="@Model.StoreID">Authorizations Manager</a>
<a id="osScripts" href="" class="nav-link">LSL Scripts</a>
<a id="osDocumentation" href="" class="nav-link">Documentation</a>
<a id="osCredits" href="" class="nav-link">Credits</a>
<vc:ui-extension-point location="store-nav" model="@Model"/>
</div>
</nav>
</div>
<div id="SectionNav-Index-Page" class="osPage">
<form id="authorizationRequest" asp-controller="UIOpenSimulator" asp-action="AuthorizationsManager">
<input id="itemId" type="hidden" name="Id" value="">
<input type="hidden" name="StoreId" value="@Model.StoreID">
<input id="itemAvatarId" type="hidden" name="AvatarId" value="">
<input id="itemObjectId" type="hidden" name="ObjectId" value="">
<input id="itemTask" type="hidden" name="Task" value="">
<div class="badge badge-invalid tableTitle" style="width: 185px;text-align:left;cursor:auto;"><h6>Pending Authorizations</h6></div>
<table id="pendingAutorizations" class="table">
<thead>
<tr>
<th style="width: 180px;">Owner Name</th>
<th style="width: 180px;">Object Name</th>
<th>Location</th>
<th style="width: 195px;">Action</th>
</tr>
</thead>
<tbody></tbody>
</table>
<div class="badge badge-settled tableTitle" style="width: 185px;text-align:left;cursor:auto;"><h6>Authorized Objects</h6></div>
<table id="authorizedObjects" class="table">
<thead>
<tr>
<th style="width: 180px;">Owner Name</th>
<th style="width: 180px;">Object Name</th>
<th>Location</th>
<th style="width: 185px;">Action</th>
</tr>
</thead>
<tbody></tbody>
</table>
<div id="authorizationModal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="authorizationModalTitle" data-bs-backdrop="static">
<div class="modal-dialog modal-dialog-centered" role="document" style="max-width: 550px;">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="authorizationModalTitle">Authorizations Manager</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
<vc:icon symbol="close" />
</button>
</div>
<div id="authorizationModal-modal-body" class="modal-body"></div>
<div class="modal-footer">
<button id="authorizationModalBtn" type="button" class="btn" style="color: #fff;text-shadow: 0px 0px 3px #000;">Remove</button>
<button type="button" class="btn badge-settled cancelOrClose" data-bs-dismiss="modal" aria-label="Close">Cancel</button>
</div>
</div>
</div>
</div>
<script type="text/javascript" nonce="@nonce">
var osModel = JSON.parse("@JsonSerializer.Serialize(Model.Data.OrderByDescending(t => t.Timestamp))".replace(/&quot;/g,"\""));
var StoreId = "@Model.StoreID";
var defaultPaymentMethod = "@Model.StoreDefaultPaymetMethod";
var defaultCurrency = "@Model.StoreDefaultCurrency";
var IP = "@Model.ServerIP";
</script>
</form>
</div>
<div id="osScriptsPage" class="osPage" style="display: none;">
<ul id="scriptsNav">
<li id="tipJarScript" class="sNav-bg bg-yellow"><h6><i class="fa fa-heart"></i>Crypto Tip-Jar Script</h6></li>
<li id="vendorScript" class="sNav-bg"><h6><i class="fa fa-shopping-cart" ></i>Products Vendor Script</h6></li>
<li id="rentalBoxScript" class="sNav-bg"><h6><i class="fa fa-home" ></i>Rental Box Script</h6></li>
</ul>
<div id="tipJarScriptBox" class="row bg-green osScriptBox" style="padding: 10px 0;border-radius:10px;margin-bottom:20px;">
<div class="row">
<h5>Crypto Tip-Jar Script</h5>
<div class="alert alert-warning">
<span><b>User Level:</b> Parcel Owner or more.</span>
</div>
<span>The Tip-Jar script will turn any inWorld object placed in its content to a donation box allowing the object's owner to collect crypto donations and is the 3D equivalent of the Pay Button
that do more or less the same:
<ul style="margin-left: 20px;">
<li>Invite the donator to select a currency or the default payment method...</li>
<li>Invite the donator to chose a pre-setted or type a custom amount of the selected currency...</li>
<li>Request the invoice from BTCPay server and invite your donator to open the invoice page...</li>
<li>Monitor the invoice status and the payment progress and confirmation...</li>
<li>Handle the invoice success or expiration and auto-reset the Tip-Jar.</li>
<li>Protect your BTCPay store by limiting the usage to one user and one invoice per user at time.
Also the script disable the Tip-Jar for the other avatars while used and in the invoice processing time.</li>
</ul>
The script is ready to use as is and use your store settings as default script parameters. You can also
set the optional invoice parameters and override the default payment method using the scripting helper at right.<br>
<br>
This video guide will show you how to create a simple scripted object to use as inWorld Tip-Jar:<br>
<a href="https://www.youtube.com/watch?v=NTb8PPp0qco&t=175&vq=hd720" target="_blank"><i class="fa fa-youtube-play" style="color: #f00;font-size:16px;margin-right:5px;"></i><b>Youtube basic video guide.</b></a><br>
<br>
</span>
</div>
<div class="col-md-8">
<pre style="border: 1px solid #2e2e2e;"><code id="tipjarScriptCode" style="display:block;width: 540px;height:330px;">
</code></pre>
<button class="btn btn-outline-secondary pull-right bg-green" style="margin-right: 5px;" data-clipboard-target="#tipjarScriptCode">
<vc:icon symbol="copy"/>&nbsp;Copy Code
</button>
</div>
<div class="col-md-4">
<div class="form-group">
<label class="form-label" for="tjDefaultPaymentMethod">Payment Method&nbsp;&nbsp;<span class="labelWarning">(Supported Cryptos Only !)</span></label>
<input id="tjDefaultPaymentMethod" type="text" value="@Model.StoreDefaultPaymetMethod">
</div>
<div class="form-group">
<label class="form-label" for="notificationEmail">Notifications Email</label>
<input id="tjNotificationEmail" type="text">
</div>
<div class="form-group">
<label class="form-label" for="tjRedirectURL">Redirect URL</label>
<input id="tjRedirectURL" type="text">
</div>
<div class="form-group">
<label class="form-label" for="tjCheckoutQueryString">Checkout Query String</label>
<input id="tjCheckoutQueryString" type="text">
</div>
<div class="form-group">
<label class="form-label" for="chatNotification">Chat Notifications</label>
<select id="tjChatNotification" type="text">
<option value="TRUE">Yes</option>
<option value="FALSE">No</option>
</select>
</div>
</div>
</div>
<div id="vendorScriptBox" class="row bg-green osScriptBox" style="padding: 10px 0;border-radius:10px;margin-bottom:20px;display:none;">
<div class="row">
<h5>Single Product Vendor Script</h5>
<div class="alert alert-warning">
<span><b>User Level:</b> Parcel Owner or more.</span><br>
<span><b>Requiere:</b> The object to sell and setting a currency, the sell price and your product description.</span>
</div>
<span>Vendor Infos:
<ul style="margin-left: 20px;">
<li>Inform the buyer about the item or service in sale in the chat and the purchase blue box...</li>
<li>Invite the buyer to click a [Buy] button if she/he accept the offer...</li>
<li>Request the invoice from BTCPay server and invite your buyer to open the invoice page...</li>
<li>Monitor the invoice status and the payment progress and confirmation...</li>
<li>Handle the invoice success or expiration and deliver the product after a successful payment.</li>
<li>Protect your BTCPay store by limiting the usage to one user and one invoice per user at time.
Also the script disable the vendor for the other avatars while used and in the invoice processing time.</li>
</ul>
This video guide will show you how to create a simple vendor to sell your products inWorld:<br>
<a href="https://www.youtube.com/watch?v=NTb8PPp0qco&t=530&vq=hd720" target="_blank"><i class="fa fa-youtube-play" style="color: #f00;font-size:16px;margin-right:5px;"></i><b>Youtube basic video guide.</b></a><br>
<br>
</span>
</div>
<div class="col-md-12">
<pre style="border: 1px solid #2e2e2e;"><code id="vendorScriptCode" class="lsl" style="display:block;width: 540px;height:250px;">
</code></pre>
</div>
<div class="row">
<div class="col-md-4">
<div class="form-group">
<label class="form-label" for="vDisplayCurrency">Display Currency</label>
<select id="vDisplayCurrency" class="vDisplayCurrency" type="text" value="@Model.StoreDefaultCurrency"></select>
</div>
<div class="form-group">
<label class="form-label" for="vNotificationEmail">Notifications Email</label>
<input id="vNotificationEmail" type="text">
</div>
<div class="form-group">
<label class="form-label" for="vChatNotification">Chat Notifications</label>
<select id="vChatNotification" type="text">
<option value="TRUE">Yes</option>
<option value="FALSE">No</option>
</select>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label class="form-label" for="vItemPrice">Item Price In&nbsp;<span id="priceCurrency"></span>&nbsp;&nbsp;<span class="labelWarning">(Mandatory, Non-zero !)</span></label>
<input id="vItemPrice" type="number" value="0.000000" step="0.000001" min="0.000001">
</div>
<div class="form-group">
<label class="form-label" for="vRedirectURL">Redirect URL</label>
<input id="vRedirectURL" type="text">
</div>
<div class="form-group">
<label class="form-label" for="vShowHoverText">Show/Hide Hover Text</label>
<select id="vShowHoverText" type="text">
<option value="TRUE">Show</option>
<option value="FALSE">Hide</option>
</select>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label class="form-label" for="vDefaultPaymentMethod">Payment Method&nbsp;&nbsp;<span class="labelWarning">(Supported Cryptos Only !)</span></label>
<input id="vDefaultPaymentMethod" type="text" value="@Model.StoreDefaultPaymetMethod">
</div>
<div class="form-group">
<label class="form-label" for="vCheckoutQueryString">Checkout Query String</label>
<input id="vCheckoutQueryString" type="text">
</div>
<button class="btn btn-outline-secondary" style="position:relative;top:18px;width:100%;" data-clipboard-target="#vendorScriptCode">
<vc:icon symbol="copy"/>&nbsp;Copy Code
</button>
</div>
</div>
</div>
<div id="rentalBoxScriptBox" class="row bg-green osScriptBox" style="padding: 10px 0;border-radius:10px;margin-bottom:20px;display:none;">
<div class="row">
<h5>Crypto Rental Box Script</h5>
<div class="alert alert-warning">
<span style="color: #f00;"><b>User Level: Estate Owner Only !</b></span><br>
<span ><b>Important Note:</b>
<ul style="margin-bottom: 5px;padding-left:20px">
<li>This rental box was made to rent parcels or a region sub-divisions and must be inside the region where the parcel is and outside the parcel to rent.</li>
<li>The script use <a href="http://opensimulator.org/wiki/OsSetParcelDetails" target="_blank">osSetParcelDetails(...)</a> function to deliver and get back the parcel. The rental box will fail to get back the parcel if the script owner don't have the Estate Owner rights and permissions.</li>
<li>The rental box do not return the previous user items after getting back the parcel. The auto-return must not be set to zero in the land options or you will need a manual intervention to clean the parcel.</li>
</ul>
</span>
<span><b>Requiere:</b> A parcel to rent, setting the currency and the rental price (per month) and 2 information notecards named "Rental Infos And Guide" and "Security Settings" in the rental box content.</span>
</div>
<span>Rental Box Infos:
<ul style="margin-left: 20px;">
<li>Inform the interested about the rental price per month and invite her/him to get the infos notecard...</li>
<li>Invite the interested to select the rental period and confirm the calculated price per period...</li>
<li>Request the invoice from BTCPay server and invite the buyer to open the invoice page...</li>
<li>Monitor the invoice status and the payment progress and confirmation...</li>
<li>Handle the invoice success or expiration and deliver the parcel after a successful payment.</li>
<li>Allow the parcel owner to extend her/his rental period (paid).</li>
<li>Allow the rental box owner to add an extra time (for free).</li>
<li>Get back the parcel at the end of the rental contract and make it available for the next renter.</li>
<li>Protect your BTCPay store by limiting the usage to one user and one invoice per user at time.
Also the script allow only the new renter (if the parcel is not rented) and the current parcel owner to create invoices.</li>
</ul>
This video guide will show you how to sub-divide a parcel and setup your rental box:<br>
<a href="https://www.youtube.com/watch?v=NTb8PPp0qco&t=1009&vq=hd720" target="_blank"><i class="fa fa-youtube-play" style="color: #f00;font-size:16px;margin-right:5px;"></i><b>Youtube basic video guide.</b></a><br>
<br>
</span>
</div>
<div class="col-md-12">
<pre style="border: 1px solid #2e2e2e;"><code id="rentalScriptCode" style="display:block;width: 540px;height:250px;">
</code></pre>
</div>
<div class="row">
<div class="col-md-4">
<div class="form-group">
<label class="form-label" for="rbParcelPosition">Parcel Position X,Y,Z:</label>
<div id="rbParcelPosition">
<input id="rbParcelPosition-x" type="number" style="width: 30%;margin-right:8px;" min="0.0" max="256.0" value="0.0">
<input id="rbParcelPosition-y" type="number" style="width: 30%;margin-right:8px;" min="0.0" max="256.0" value="0.0">
<input id="rbParcelPosition-z" type="number" style="width: 30%;color:#000;" value="21.0" disabled>
</div>
</div>
<div class="form-group">
<label class="form-label" for="rbDefaultPaymentMethod">Payment Method&nbsp;&nbsp;<span class="labelWarning">(Supported Cryptos Only !)</span></label>
<input id="rbDefaultPaymentMethod" type="text" value="@Model.StoreDefaultPaymetMethod">
</div>
<div class="form-group">
<label class="form-label" for="rbCheckoutQueryString">Checkout Query String</label>
<input id="rbCheckoutQueryString" type="text">
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label class="form-label" for="rbDisplayCurrency">Display Currency</label>
<select id="rbDisplayCurrency" class="rbDisplayCurrency" type="text" value="@Model.StoreDefaultCurrency"></select>
</div>
<div class="form-group">
<label class="form-label" for="rbNotificationEmail">Notifications Email</label>
<input id="rbNotificationEmail" type="text">
</div>
<div class="form-group">
<label class="form-label" for="rbChatNotification">Chat Notifications</label>
<select id="rbChatNotification" type="text">
<option value="TRUE">Yes</option>
<option value="FALSE">No</option>
</select>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label class="form-label" for="rbRentalPrice">Rental Price In&nbsp;<span id="rbPriceCurrency"></span>&nbsp;&nbsp;<span class="labelWarning">(Mandatory, Non-zero !)</span></label>
<input id="rbRentalPrice" type="number" value="0.000000" step="0.000001" min="0.000001">
</div>
<div class="form-group">
<label class="form-label" for="rbRedirectURL">Redirect URL</label>
<input id="rbRedirectURL" type="text">
</div>
<button class="btn btn-outline-secondary" style="position:relative;top:20px;width:100%;" data-clipboard-target="#rentalScriptCode">
<vc:icon symbol="copy"/>&nbsp;Copy Code
</button>
</div>
</div>
</div>
</div>
<div id="osDocumentationPage" class="osPage" style="display: none;">
<h5>Opensimulator Plugin <span class="ospVersion">v0.1.1</span> Documentation</h5>
<hr>
<div class="panel-group">
<div class="panel panel-default">
<div class="panel-heading">
<h6 class="panel-title">
<a data-bs-toggle="collapse" class="doc-title" href="#collapse1" aria-expanded="true">About Opensimulator Plugin:</a>
</h6>
</div>
<div id="collapse1" class="panel-collapse collapse show">
<div class="panel-body">
<p>
This plugin targets an audiance that already have a bit of experience not only with the technical part of Opensimulator but also with the HyperGrid economy and the ways to conduct an inWorld business... In majority standalones or grids operators, 3D hosting providers, contents creators and merchants, many educative services providers and all kind of non-profit entities and organizations.
</p>
<p>
If this is not your case, please, give to yourself a bit of time to learn about the <a href="http://opensimulator.org/wiki/Main_Page" target="_blank">Opensimulator software</a> and the 3D Web of Virtual Worlds known as the <a href="https://www.hypergridbusiness.com/category/metaverse/" target="_blank">HyperGrid</a> (HG). You can also setup a local sandbox to practice the multiple edition arts including the scripting or <a href="https://opensimworld.com/dir" target="_blank">explore the HyperGrid</a> by creating an account in an HG enabled grid like <a href="https://osgrid.org/" target="_blank">osGrid</a> where you can meet <a href="http://opensimulator.org/wiki/Office_hours">the Opensimulator DEV team</a> and practice using their public sandbox.
</p>
<span>That said...</span>
<p>
The Opensimulator plugin add to BTCPay a permissioned public interface similar to the Pay Button but adapted for the Opensimulator usages and allow:
<br>- Only 3D objects from a virtual world to interact with the plugin.
<br>- Only the 3D objects that the store owner authorize in the "Authorizations Manager" section to create invoices in her/his store.
<br>- Only a maximum of 3 pending authorizations per store.
</p>
<p>
The scripts samples in the "LSL Scripts" section was made to interact with this plugin and handle the inWorld part like the objects registration and the invoices requests. Also handles the delivery of the purchased product / service after a successful payment (paid and confirmed) and the exceptions / failures.
<br>The following video is a proof of work and concept that show the scripts and the Opensimulator plugin in action:
<br><a href="https://www.youtube.com/watch?v=NTb8PPp0qco&vq=hd720" target="_blank"><i class="fa fa-youtube-play" style="color: #f00;font-size:16px;margin-right:5px;"></i><b>Youtube video Introduction - Proof of work and concept.</b></a><br>
</p>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h6 class="panel-title">
<a data-bs-toggle="collapse" class="doc-title" href="#collapse2" aria-expanded="false">Setting Your Opensimulator Sandbox:</a>
</h6>
</div>
<div id="collapse2" class="panel-collapse collapse">
<div class="panel-body">
<div class="alert alert-info" style="margin-left: 0;"><i>This part of the docu was add for the BTCPay Dev team audit and may be removed in the release version because the Opensimulator users already know all this...</i></div>
<p>
The best way to try and audit the plugin and the included scripts is setting up your own virtual world where you can have the full permissions and the Estate Owner rights. To do this, you need for a minimalist DEV and testing environement to:
</p>
<ul>
<li>Install "mono" if you have a linux OS or Mac</li>
<li>Download and unpack the last version of <a href="http://opensimulator.org/wiki/Main_Page" target="_blank">the Opensimulator binaries</a>.</li>
<li>Download and install the last version of Firestorm Viewer for Opensimulator ( <a href="https://www.firestormviewer.org/windows-for-open-simulator/" target="_blank">Windows</a> - <a href="https://www.firestormviewer.org/linux-for-open-simulator/" target="_blank">Linux</a> - <a href="https://www.firestormviewer.org/mac-for-open-simulator/" target="_blank">Mac</a> ).</li>
</ul>
<p>
<div class="alert alert-warning" style="margin-left: 0;"><i>NOTE: The following instructions are for testing only and is not the best way to setup Opensimulator!
<br>If you want to start a production virtual world, use the Diva distro version (Documentation is included as .txt files).
</i></div>
</p>
<p>
When done, navigate to ..\opensim-x.x.x.x\bin folder...<br>
If your BTCPay Server instance is not under a local or LAN IP (DEV mode), you have just to double click the OpenSim.exe in windows or open a console in the bin folder and type "mono OpenSim.exe" and hit Enter in linux.
</p>
<p>
If your BTCPay Server instance is under 127.0.0.1:14142 or a LAN IP, you have to add an exception to allow the LSL scripts to interact with the local/LAN IP and ports... To do, open the OpenSim.ini file in the bin folder, scroll down to the [Network] section and add the following line to the end of the section (Read the section comments for more infos). Change "127.0.0.1:14142"&nbsp;&nbsp;by&nbsp;&nbsp;"Your-LAN-IP:14142" if needed.
<pre><code>OutboundDisallowForUserScriptsExcept = 127.0.0.1:9000|127.0.0.1:14142</code></pre>
Save & close OpenSim.ini and double click the OpenSim.exe in windows or open a console in the bin folder and type "mono OpenSim.exe" and hit Enter in linux.
</p>
<p>
In the first Opensimulator startup, the OpenSim console will ask you to set an Estate and Region names, the first / last names and password of the admin user (important to set)... you can just hit enter for the optional settings and wait... the console will show you when your virtual world is ready to use.
<br>At the very end, when you see (Region root #) in the console, type the following command and hit Enter:
<pre><code>terrain fill 21</code></pre>
</p>
<p>
Now is time to run Firestorm and login into your local sandbox. So double click the Firestorm icon to start it, expand the "Grid" list in the bottom bar and select "the lost continent of hippo". If you don't have it in the grids list, click CTRL+P to open the settings window and click "Opensim" at left. In the top of the tab type in the "Add new grid" field:
<pre><code>127.0.0.1:9000</code></pre>
click "Apply" & "Ok". Now you will see "the lost continent of hippo" in the grids list, select it and type the first / last names that you have set in the OpenSim console separated by a space, type your password and click the Log in button. If you did it right, you are now driving the default avatar in your region and ready to follow the video guides and try the sample scripts in the "LSL Scripts" section.
</p>
<p>
<div class="alert alert-info" style="margin-left: 0;"><i>NOTE: If you want to set the menus like the video guides, click CTRL+P to open the settings window and type "pie menu" in the top search field and click "User Interface" at left. Unselect "Use Pie Menu" and click "Ok".</i>
</div>
</p>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h6 class="panel-title">
<a data-bs-toggle="collapse" class="doc-title" href="#collapse3" aria-expanded="false">Opensimulator Plugin And Scripts Workflow:</a>
</h6>
</div>
<div id="collapse3" class="panel-collapse collapse">
<div class="panel-body">
<p>
In Opensimulator, a <a href="https://wiki.secondlife.com/wiki/Getting_started_with_LSL" target="_blank">Linden Scripting Language (LSL) script</a> is the the source code of a .DLL that when compiled (on save), acts and works like a plugin attached to the object where the script is, extending it's capabilities
to allow all kind of interactions, animations and control.
</p>
<p>
Between the LSL features, there is the <a href="https://wiki.secondlife.com/wiki/Category:LSL_HTTP" target="_blank">HTTP I/O</a> that we use to interconnect an inWorld object with a BTCPay instance and the main role of the Opensimulator plugin is to secure a bit
this interactions with a simple permissions layer that only allow the invoices creation to the authorized 3D Objects / Avatars without exposing sensive data like the Greenfield API keys and protect your BTCPay instance / store in same time.
So, let's see how this works...
</p>
<p>
Because the Opensimulator plugin may have to handle 100s if not 1000s objects, is not a bad idea to divide the load by 2 public endpoints:
</p>
<span>One for the authorizations process (heavy load):</span>
<pre><code>BTCPayServerURL/opensim/authorization</code></pre>
<span>And the other for the invoicing part (limited load in the script level):</span>
<pre><code>BTCPayServerURL/opensim/invoices</code></pre>
<p>
The first thing that the script do on starting or when the object is clicked is asking the plugin about the object's status using the authorization() function with the "check" parameter.
</p>
<pre><code><span class="commented">/* Global variables needed in this code:
key avatar
*/</span>
<span class="commented">/* Request the object status when the script start...*/</span>
<a href="https://wiki.secondlife.com/wiki/State_entry" target="_blank" >state_entry</a>(){
<span style="color: #f00;">authorization("check")</span>;
}
<span class="commented">/* Request the object status when the object is clicked by the object's owner...*/</span>
<a href="https://wiki.secondlife.com/wiki/Touch_start" target="_blank" >touch_start</a>(integer a){
if(<a href="https://wiki.secondlife.com/wiki/LlDetectedKey" target="_blank" >llDetectedKey(0)</a> == <a href="https://wiki.secondlife.com/wiki/LlGetOwner" target="_blank" >llGetOwner()</a>){
avatar = <a href="https://wiki.secondlife.com/wiki/LlGetOwner" target="_blank" >llGetOwner()</a>;
<span style="color: #f00;">authorization("check")</span>;
}
else{
<a href="https://wiki.secondlife.com/wiki/LlInstantMessage" target="_blank" >llInstantMessage</a>(<a href="https://wiki.secondlife.com/wiki/LlDetectedKey" target="_blank" >llDetectedKey(0)</a>,"You are not the owner of this item!");
}
}
</code></pre>
<p>
The plugin returns a Json that drive what the object will show, allow and do in the next steps. The plugin use the <a href="https://wiki.secondlife.com/wiki/LlHTTPRequest" target="_blank" >llHTTPRequest</a> headers to get/set the object's data and verify if is authorized or no. The authorization() function looks like this:
</p>
<pre><code><span class="commented">/* Global variables needed in this code:
string BTCPayServerURL
string storeID
key authorizationRequest_id
*/</span>
<span style="color: #f00;">authorization(string action)</span>{
authorizationRequest_id = <a href="https://wiki.secondlife.com/wiki/LlHTTPRequest" target="_blank" >llHTTPRequest</a>(
BTCPayServerURL +"/opensim/authorization",
[
HTTP_METHOD,"POST",
HTTP_MIMETYPE,"application/x-www-form-urlencoded",
HTTP_BODY_MAXLENGTH,16384,
HTTP_VERIFY_CERT,FALSE, <span class="commented">/* FALSE for 127.0.0.1 else TRUE */</span>
HTTP_CUSTOM_HEADER,"x-opensim-store-id",storeID,
HTTP_CUSTOM_HEADER,"x-opensim-owner-home-url", <a href="http://opensimulator.org/wiki/OsGetAvatarHomeURI" target="_blank" >osGetAvatarHomeURI</a>(<a href="https://wiki.secondlife.com/wiki/LlGetOwner" target="_blank" >llGetOwner</a>()),
HTTP_CUSTOM_HEADER,"x-opensim-object-host-url", <a href="http://opensimulator.org/wiki/OsGetGridHomeURI" target="_blank" >osGetGridHomeURI</a>()
],
"action=" + action );
}</code></pre>
<p>
The result of the authorization() function is captured by <a href="https://wiki.secondlife.com/wiki/Http_response" target="_blank" >http_response</a> event handler.
</p>
<pre><code><span class="commented">/* Global variables needed in this code:
key avatar
key authorizationRequest_id
*/</span>
<span class="commented">/* Capture the plugin response to authorization("check") and perform actions...*/</span>
<a href="https://wiki.secondlife.com/wiki/Http_response" target="_blank" >http_response</a>(key id, integer status, list metaData, string Response){
if (status == 200 ){
if (id == authorizationRequest_id){
if(<a href="http://opensimulator.org/wiki/OsStringIndexOf" target="_blank" >osStringIndexOf</a>(Response,"ospError",0) == -1){ <span class="commented">/* if not an "ospError" return */</span>
string authStatus = <a href="https://wiki.secondlife.com/wiki/LlJsonGetValue" target="_blank" >llJsonGetValue</a>(Response,["status"]);
string authorized = <a href="https://wiki.secondlife.com/wiki/LlJsonGetValue" target="_blank" >llJsonGetValue</a>(Response,["authorized"]);
if(authStatus == "registred" && authorized == "True"){
<span class="commented">/* Here our object is authorized and you can start the invoicing process.
In the included scripts, we switch to the authorized state that handles the invoicing. */</span>
<a href="https://wiki.secondlife.com/wiki/State" target="_blank" >state</a> authorized;
}
else if(authStatus == "registred" && authorized == "False"){
<span class="commented">/* Here the object is already registred but not authorized yet...*/</span>
<a href="https://wiki.secondlife.com/wiki/LlSetText" target="_blank" >llSetText</a>("Out Of Service\nMissing Authorization",<1,0,0>,1.0);
if(avatar == <a href="https://wiki.secondlife.com/wiki/LlGetOwner" target="_blank" >llGetOwner()</a>){
<a href="https://wiki.secondlife.com/wiki/LlLoadURL" target="_blank" >llLoadURL</a>(<a href="https://wiki.secondlife.com/wiki/LlGetOwner" target="_blank" >llGetOwner()</a>,"\nThis object is registred and waiting your authorization.\n\nPlease open this page and authorize this object. When done, click this object again to enable it...", BTCPayServerURL + "/stores/" + storeID + "/plugins/opensim");
}
}
else if(authStatus == "success" && authorized == "False"){
<span class="commented">/* Here the object was successfully registred in the plugin side but not authorized yet...*/</span>
<a href="https://wiki.secondlife.com/wiki/LlSetText" target="_blank" >llSetText</a>("Out Of Service\nMissing Authorization",<1,0,0>,1.0);
if(avatar == <a href="https://wiki.secondlife.com/wiki/LlGetOwner" target="_blank" >llGetOwner()</a>){
<a href="https://wiki.secondlife.com/wiki/LlLoadURL" target="_blank" >llLoadURL</a>(<a href="https://wiki.secondlife.com/wiki/LlGetOwner" target="_blank" >llGetOwner()</a>,"\nThis object is registred and waiting your authorization.\n\nPlease open this page and authorize this object. When done, click this object again to enable it...", BTCPayServerURL + "/stores/" + storeID + "/plugins/opensim");
}
}
else if(authStatus == "unknown"){
<span class="commented">/* Here the object is new and unknown to the plugin...*/</span>
<a href="https://wiki.secondlife.com/wiki/LlSetText" target="_blank" >llSetText</a>("Out Of Service\nUnknown Object",<1,0,0>,1.0);
if(avatar == <a href="https://wiki.secondlife.com/wiki/LlGetOwner" target="_blank" >llGetOwner()</a>){
<a href="https://wiki.secondlife.com/wiki/LlDialog" target="_blank" >llDialog</a>(<a href="https://wiki.secondlife.com/wiki/LlGetOwner" target="_blank" >llGetOwner()</a>,"\nThis object is not linked to your BtcPay store yet!\n\nPlease, click [Register] to start the linking and authorization process...",["Register","Cancel"],channel);
}
}
else if(authStatus == "fail" && authorized == "False"){
<span class="commented">/* Here the registration in the plugin side failed...*/</span>
<a href="https://wiki.secondlife.com/wiki/LlSetText" target="_blank" >llSetText</a>("Out Of Service\nFatal Error",<1,0,0>,1.0);
if(avatar == <a href="https://wiki.secondlife.com/wiki/LlGetOwner" target="_blank" >llGetOwner()</a>){
<a href="https://wiki.secondlife.com/wiki/LlLoadURL" target="_blank" >llLoadURL</a>(<a href="https://wiki.secondlife.com/wiki/LlGetOwner" target="_blank" >llGetOwner()</a>,"\nThis object failed to register!\n\nPlease open this page and investigate the problem and try again...", BTCPayServerURL + "/stores/" + storeID + "/plugins/opensim");
}
}
}
else{
<span class="commented">/* Show the plugin errors if there is...*/</span>
<a href="https://wiki.secondlife.com/wiki/LlSetText" target="_blank" >llSetText</a>("Out Of Service\nFatal Error",<1,0,0>,1.0);
<a href="https://wiki.secondlife.com/wiki/LlOwnerSay" target="_blank" >llOwnerSay</a>("\nError: \n" + <a href="https://wiki.secondlife.com/wiki/LlJsonGetValue" target="_blank" >llJsonGetValue</a>(Response,["ospError"]));
}
}
}
else{
<span class="commented">/* Show errors in case of problems with your BTCPay server...*/</span>
<a href="https://wiki.secondlife.com/wiki/LlSetText" target="_blank" >llSetText</a>("Out Of Service\nFatal Error",<1,0,0>,1.0);
<a href="https://wiki.secondlife.com/wiki/LlOwnerSay" target="_blank" >llOwnerSay</a>("\nError: \nDetected a problem with your BTCPay instance!\nStatus: " + status);
}
}
</code></pre>
<p>
For the new objects, the plugin returns the following Json that set the object's hover text to <span style="color: #f00;">"Out Of Service - Unknown Object"</span> and makes the object only useable by it's owner.
Also, clicking the object opens a blue box ( <a href="https://wiki.secondlife.com/wiki/LlDialog" target="_blank" >llDialog</a> ) that invite the owner to register this object...
</p>
<pre><code>{"status":"unknown"}</code></pre>
<p>
When the object's owner click the [Register] button in the blue box ( <a href="https://wiki.secondlife.com/wiki/LlDialog" target="_blank" >llDialog</a> ), the script execute the authorization("register") function witn the "register" parameter under the <a href="https://wiki.secondlife.com/wiki/Listen" target="_blank" >listen</a> event handler:
</p>
<pre><code><span class="commented">/* Global variables needed in this code:
key avatar
*/</span>
<a href="https://wiki.secondlife.com/wiki/Listen" target="_blank" >listen</a>(integer channel, string name, key id, string Box){
if(avatar == <a href="https://wiki.secondlife.com/wiki/LlGetOwner" target="_blank" >llGetOwner()</a> && Box == "Register"){
<span style="color: #f00;">authorization("register")</span>;
}
}
</code></pre>
<p>
and the plugin return one of the 2 possible status:
</p>
<pre><code>{"status":"success","authorized":"False"}</code></pre>
Or
<pre><code>{"status":"fail","authorized":"False"}</code></pre>
<ul>
<li>
<b>"success":</b> means that the object was successfully saved in the plugin database and appear in the "Pending Authorizations" box in the "Authorizations Manager" section. Now the object's hover text show <span style="color: #f00;">"Out Of Service - Missing Authorization"</span> and the click open's a blue box ( <a href="https://wiki.secondlife.com/wiki/LlLoadURL" target="_blank" >llLoadURL</a> ) that invite the owner to open her/his BTCPay store > plugin to authorize the object...
</li>
<li>
<b>"fail":</b> mean something gose wrong in the registration part. The object' hover text will show <span style="color: #f00;">"Out Of Service - Fatal Error"</span> and the click open's a blue box ( <a href="https://wiki.secondlife.com/wiki/LlLoadURL" target="_blank" >llLoadURL</a> ) that invite the owner to open her/his BTCPay store > plugin to investigate and fix...
</li>
</ul>
<p>
After a successful registration, the click on the object fire the authorization("check") but the returned Json will be:
</p>
<pre><code>{"status":"registred","authorized":"False"}</code></pre>
<p>
Now, the object's owner have to login to her/his store > plugin, verify the object data and click the corresponding [Authorize] button. After that the object will appear in the "Authorized Objects" box and is allowed to request invoices from the owner's store.
<br>This manual intervention proof that the user is a human and she/he is the owner of both: the used store and the inWorld object.
<br><br>
After a successful authorization, the script restart or the object click fires the authorization("check") and the return will be:
</p>
<pre><code>{"status":"registred","authorized":"True"}</code></pre>
That it for the authorization part... <br><br>
<div class="alert alert-warning" style="margin-left: 0;"><i>Note: For security reasons, the plugin do not allow more than 3 pending authorizations requests per store and the owner have to remove or authorize the pending objects to be able to add more objects registrations.</i>
</div>
<p>For the invoicing part, the scripts do the majority of the job in term of verifications and the invoice request building and have its own function. But before all, is important to know how the script protect itself and your store.
</p>
For each use, the script:
<ul>
<li>Allow only one avatar at time (current user) to request an invoice and interact with the object while processing.</li>
<li>Allow only one invoice per avatar at time. If the current user click the object while processing, a blue box will appear inviting her/him to open the current invoice page.</li>
<li>Request from Opensimulator a single time use and destructible endpoint URL using <a href="https://wiki.secondlife.com/wiki/LlRequestURL" target="_blank" >llRequestURL()</a> to capture the BTCPay notifications.
The life and the validity time of the generated endpoint depends on the invoice status and your store settings. The current endpoint is destroyed by <a href="https://wiki.secondlife.com/wiki/LlReleaseURL" target="_blank" >llReleaseURL()</a> in the reset() function when the invoice status is "expired" or "confirmed" or when the invoice was marked ("invalid" or "complete"). </li>
<li>Generate a single time use random UUID and set it as "orderId" in the invoice parameters. The orderID play the role of a session ID and is destroyed by the reset() function.</li>
<li>Check if the IP of the incoming notifications sender is the same as the "allowedHttpInIP" in the global variables.</li>
<li>Check if the notification "url" start with the "BTCPayServerURL" that you have set in the global variables.</li>
<li>Check if the notification "invoiceId" and the "orderId" are the same as the inMemory invoiceID and orderID.</li>
<li>Reset the script / object and purge all the current invoice data at the end of the processing.</li>
<li>Handles errors and make the object "Out Of Service" if needed.</li>
</ul>
<p>
So, let's see this as LSL code by extending a bit the previous code...
<br> In this example we will chain (on click) the authorization() and the invoice request using the plugin return in case of an authorized object to call the requestInvoice() function.
</p>
<pre><code><span class="commented">/* Global variables needed in this code:
key authorizationRequest_id
key IPNEndpointRequest_id
key avatar
string invoiceID
string invoiceURL
string orderID
string txStatus
*/</span>
<a href="https://wiki.secondlife.com/wiki/Touch_start" target="_blank" >touch_start</a>(integer n){
if (avatar == NULL_KEY){
avatar = <a href="https://wiki.secondlife.com/wiki/LlDetectedKey" target="_blank" >llDetectedKey(0)</a>;
<span style="color:#f00;">authorization("check")</span>;
}
else if (avatar != <a href="https://wiki.secondlife.com/wiki/LlDetectedKey" target="_blank" >llDetectedKey(0)</a>){
<a href="https://wiki.secondlife.com/wiki/LlInstantMessage" target="_blank" >llInstantMessage</a>(<a href="https://wiki.secondlife.com/wiki/LlDetectedKey" target="_blank" >llDetectedKey(0)</a>, "This device is in use!/nPlease wait...");
}
else if (active && avatar == <a href="https://wiki.secondlife.com/wiki/LlDetectedKey" target="_blank" >llDetectedKey(0)</a> && txStatus != ""){
<a href="https://wiki.secondlife.com/wiki/LlInstantMessage" target="_blank" >llInstantMessage</a>(avatar, "\nTransaction in progress!\nYour invoice ID is:\n"+ invoiceID +"\\nInvoice page at:\\n"+ invoiceURL +"\nPlease wait...");
<a href="https://wiki.secondlife.com/wiki/LlLoadURL" target="_blank" >llLoadURL</a>(avatar, "Click to open the payment page.\nYour invoice ID is : "+ invoiceID +"\\nThank you for your payment!", invoiceURL);
}
}
<a href="https://wiki.secondlife.com/wiki/Http_response" target="_blank" >http_response</a>(key id, integer status, list metaData, string Response){
if (status == 200 ){
if (id == authorizationRequest_id){
if(<a href="http://opensimulator.org/wiki/OsStringIndexOf" target="_blank" >osStringIndexOf</a>(Response,"ospError",0) == -1){ <span class="commented">/* if not an Error */</span>
string authStatus = <a href="https://wiki.secondlife.com/wiki/LlJsonGetValue" target="_blank" >llJsonGetValue</a>(Response,["status"]);
string authorized = <a href="https://wiki.secondlife.com/wiki/LlJsonGetValue" target="_blank" >llJsonGetValue</a>(Response,["authorized"]);
if(authStatus == "registred" && authorized == "True"){
<span class="commented">/* We request our endpoint to capture the BTCPay notifications and is set later under http_request event */</span>
IPNEndpointRequest_id = <a href="https://wiki.secondlife.com/wiki/LlRequestURL" target="_blank" >llRequestURL()</a>;
<span class="commented">/* We cast a random key to string and set the invoice parameter "orderId". */</span>
orderID = (string)<a href="https://wiki.secondlife.com/wiki/LlGenerateKey" target="_blank" >llGenerateKey()</a>;
<span class="commented">/* If all the other invoice parameters are set, we fire the requestInvoice() function. */</span>
<span style="color: #f00;">requestInvoice()</span>;
txStatus = "new";
}
}
}
}
}
</code></pre>
<p>
The endpoint URL and the BTCPay notifications are captured by the <a href="https://wiki.secondlife.com/wiki/Http_request" target="_blank" >http_request</a> event handler.
For example we get the endpoint URL requested by the <a href="https://wiki.secondlife.com/wiki/LlRequestURL" target="_blank" >llRequestURL()</a> and set the invoice parameter "serverIpn" like this:
</p>
<pre><code><span class="commented">/* Global variables needed in this code:
key IPNEndpointRequest_id
string IPNEndpointURL
*/</span>
<a href="https://wiki.secondlife.com/wiki/Http_request" target="_blank" >http_request</a>(key id, string method, string body){
if (id == IPNEndpointRequest_id && method == URL_REQUEST_GRANTED){
IPNEndpointURL = body;
<a href="https://wiki.secondlife.com/wiki/LlHTTPResponse" target="_blank" >llHTTPResponse</a>(id, 200, "");
}
}
</code></pre>
<p>
The requestInvoice() function build and POST the invoice request to your BTCPay instance and must be called only after setting or populating the invoice parameters from the the script variables:
</p>
<pre><code><span class="commented">/* Global variables needed in this code:
string BTCPayServerURL
string storeID
string currency
string price
string defaultPaymentMethod
string orderID
string IPNEndpointURL
string checkoutDesc
string notificationEmail
string redirectURL
string checkoutQueryString
key requestInvoice_id
*/</span>
<span style="color: #f00;">requestInvoice()</span>{
string formData = "";
formData += "storeId="+ <a href="https://wiki.secondlife.com/wiki/LlEscapeURL" target="_blank" >llEscapeURL</a>(storeID);
formData += "&price="+ <a href="https://wiki.secondlife.com/wiki/LlEscapeURL" target="_blank" >llEscapeURL</a>(price);
formData += "&<span>currency</span>="+ <a href="https://wiki.secondlife.com/wiki/LlEscapeURL" target="_blank" >llEscapeURL</a>(currency);
formData += "&defaultPaymentMethod="+ <a href="https://wiki.secondlife.com/wiki/LlEscapeURL" target="_blank" >llEscapeURL</a>(defaultPaymentMethod);
formData += "&orderId="+ <a href="https://wiki.secondlife.com/wiki/LlEscapeURL" target="_blank" >llEscapeURL</a>(orderID);
formData += "&serverIpn="+ <a href="https://wiki.secondlife.com/wiki/LlEscapeURL" target="_blank" >llEscapeURL</a>(IPNEndpointURL);
if (checkoutDesc != ""){
formData += "&checkoutDesc="+ <a href="https://wiki.secondlife.com/wiki/LlEscapeURL" target="_blank" >llEscapeURL</a>(checkoutDesc);
}
if (redirectURL != ""){
formData += "&browserRedirect="+ <a href="https://wiki.secondlife.com/wiki/LlEscapeURL" target="_blank" >llEscapeURL</a>(redirectURL);
}
if (notificationEmail != ""){
formData += "&<span>notifyEmail</span>="+ <a href="https://wiki.secondlife.com/wiki/LlEscapeURL" target="_blank" >llEscapeURL</a>(notificationEmail);
}
if (checkoutQueryString != ""){
formData += "&checkoutQueryString="+ <a href="https://wiki.secondlife.com/wiki/LlEscapeURL" target="_blank" >llEscapeURL</a>(checkoutQueryString);
}
requestInvoice_id = <a href="https://wiki.secondlife.com/wiki/LlHTTPRequest" target="_blank" >llHTTPRequest</a>(
BTCPayServerURL +"/opensim/invoices",
[
HTTP_METHOD,"POST",
HTTP_MIMETYPE,"application/x-www-form-urlencoded",
HTTP_BODY_MAXLENGTH,16384,
HTTP_VERIFY_CERT,FALSE, <span class="commented">/* FALSE for 127.0.0.1 else TRUE */</span>
HTTP_CUSTOM_HEADER,"x-opensim-store-id",storeID,
HTTP_CUSTOM_HEADER,"x-opensim-owner-home-url", <a href="http://opensimulator.org/wiki/OsGetAvatarHomeURI" target="_blank" >osGetAvatarHomeURI</a>(<a href="https://wiki.secondlife.com/wiki/LlGetOwner" target="_blank" >llGetOwner</a>()),
HTTP_CUSTOM_HEADER,"x-opensim-object-host-url", <a href="http://opensimulator.org/wiki/OsGetGridHomeURI" target="_blank" >osGetGridHomeURI</a>()
],
formData );
}</code></pre>
<p>
In the plugin side, for each incoming invoice request, the plugin verify the following points:
</p>
<ul>
<li>If the store ID is valid...</li>
<li>If the price is not zero and the script's endpoint and the order ID are not empty...</li>
<li>If the request headers contain some <a href="https://wiki.secondlife.com/wiki/LlHTTPRequest" target="_blank" >llHTTPRequest</a> specific headers needed for the authorization verification...</li>
<li>If the the avatar and the object from their respective origins are authorized to create invoices in the store...</li>
<li>If the posData is under 500 characters...</li>
</ul>
<p>
If all is OK, the plugin use the form data to create the invoice and return the invoice ID and URL that are handled under <a href="https://wiki.secondlife.com/wiki/Http_response" target="_blank" >http_response</a>
and open a blue box ( <a href="https://wiki.secondlife.com/wiki/LlLoadURL" target="_blank" >llLoadURL</a> ) that invite the user to open the invoice page.</p>
<pre><code><span class="commented">/* Global variables needed in this code:
key avatar
key requestInvoice_id
string invoiceID
string invoiceURL
*/</span>
<a href="https://wiki.secondlife.com/wiki/Http_response" target="_blank" >http_response</a>(key id, integer status, list metaData, string Response){
if (status == 200 ){
if (id == requestInvoice_id){
invoiceID = <a href="https://wiki.secondlife.com/wiki/LlJsonGetValue" target="_blank" >llJsonGetValue</a>(Response, ["invoiceId"]);
invoiceURL = <a href="https://wiki.secondlife.com/wiki/LlJsonGetValue" target="_blank" >llJsonGetValue</a>(Response, ["invoiceUrl"]);
<a href="https://wiki.secondlife.com/wiki/LlLoadURL" target="_blank" >llLoadURL</a>(avatar, "Click to open the payment page.\nYour invoice ID is : "+ invoiceID, invoiceURL);
}
}
else {
llOwnerSay("\nStatus :"+ status +"\nError : \n"+ Response);
reset();
}
}
</code></pre>
<p>
At this point of the processing, the script is in wait of the user payment and the BTCPay notifications to perform some verifications and actions based on the notification data.
The notifications are captured by the <a href="https://wiki.secondlife.com/wiki/Http_request" target="_blank" >http_request</a> event handler:
</p>
<pre><code><span class="commented">/* Global variables needed in this code:
string BTCPayServerURL
string allowedHttpInIP
key avatar
string invoiceID
string orderID
*/</span>
<a href="https://wiki.secondlife.com/wiki/Http_request" target="_blank" >http_request</a>(key id, string method, string body){
if (<a href="https://wiki.secondlife.com/wiki/LlGetHTTPHeader" target="_blank" >llGetHTTPHeader</a>(id, "x-remote-ip") == allowedHttpInIP && <a href="http://opensimulator.org/wiki/OsStringIndexOf" target="_blank" >osStringIndexOf</a>(<a href="https://wiki.secondlife.com/wiki/LlJsonGetValue" target="_blank" >llJsonGetValue</a>(body,["url"]), BTCPayServerURL,0) > -1){
string invoiceOrderId = <a href="https://wiki.secondlife.com/wiki/LlJsonGetValue" target="_blank" >llJsonGetValue</a>(body, ["orderId"]);
string invoiceId = <a href="https://wiki.secondlife.com/wiki/LlJsonGetValue" target="_blank" >llJsonGetValue</a>(body, ["id"]);
string invoiceStatus = <a href="https://wiki.secondlife.com/wiki/LlJsonGetValue" target="_blank" >llJsonGetValue</a>(body, ["status"]);
if (method == "POST" && invoiceOrderId == orderID && invoiceId == invoiceID){
if(invoiceStatus == "expired" || invoiceStatus == "invalid"){
<span class="commented">/* The invoice expired or marked invalid. We inform the owner and the user... */</span>
<a href="https://wiki.secondlife.com/wiki/LlInstantMessage" target="_blank" >llInstantMessage</a>(avatar, "Operation Fail!\nInvoice ID: "+ invoiceID +" status is "+ invoiceStatus +".\nPlease try again or contact "+ llKey2Name(llGetOwner()) +" and provide your invoice ID: \n"+ invoiceID +"\nif you did a payment.");
if (notifications){
<a href="https://wiki.secondlife.com/wiki/LlOwnerSay" target="_blank" >llOwnerSay</a>("\nWarning: Invoice ID:\n"+ invoiceID +" Fail!\nStatus is "+ invoiceStatus);
}
<span class="commented">/* We return status 200 to BTCPay... */</span>
<a href="https://wiki.secondlife.com/wiki/LlHTTPResponse" target="_blank" >llHTTPResponse</a>(id, 200, "");
<span class="commented">/* And the script reset the object and make it available for the next user. */</span>
<span style="color: #f00;">reset()</span>;
}
else if (invoiceStatus == "paid"){
<span class="commented">/* The invoice was paid but not fully confirmed yet... We inform the user and the owner...*/</span>
<a href="https://wiki.secondlife.com/wiki/LlInstantMessage" target="_blank" >llInstantMessage</a>(avatar, "\nThank you for your...\nInvoice ID: "+ invoiceID +" is paid and in wait of the usual confirmations.");
if (notifications){
<a href="https://wiki.secondlife.com/wiki/LlOwnerSay" target="_blank" >llOwnerSay</a>("\n"+ osKey2Name(avatar) +" donated "+ price +" "+ currency +"!\nYou have recived: "+ llJsonGetValue(body, ["btcPrice"]) +" "+ defaultPaymentMethod +".\nInvoice ID: "+ invoiceID +" Paid (waiting confirmations).");
}
<span class="commented">/* We return status 200 to BTCPay... */</span>
<a href="https://wiki.secondlife.com/wiki/LlHTTPResponse" target="_blank" >llHTTPResponse</a>(id, 200, "");
}
else if (invoiceStatus == "confirmed" || invoiceStatus == "complete"){
<span class="commented">/* Now is the delivery time! The invoice was fully paid and confirmed. We inform the user and the owner and the object must do what is supposed to do after a successful payment. */</span>
<a href="https://wiki.secondlife.com/wiki/LlInstantMessage" target="_blank" >llInstantMessage</a>(avatar, "\nThank you for your...(purchase, donation...)\nInvoice ID: "+ invoiceID +" was fully paid and confirmed.");
if (notifications == TRUE){
<a href="https://wiki.secondlife.com/wiki/LlOwnerSay" target="_blank" >llOwnerSay</a>("\nInvoice ID: " + invoiceID +" Confirmed.");
}
<span class="commented">/* We return status 200 to BTCPay... */</span>
<a href="https://wiki.secondlife.com/wiki/LlHTTPResponse" target="_blank" >llHTTPResponse</a>(id, 200, "");
<span class="commented">/* The End :) The invoice was successfully handled and done. Now is time to reset the object and make it available for the next user. */</span>
<span style="color: #f00;">reset()</span>;
}
}
}
<span class="commented">/* Just return status 200 to any */</span>
<a href="https://wiki.secondlife.com/wiki/LlHTTPResponse" target="_blank" >llHTTPResponse</a>(id, 200, "");
}
</code></pre>
<p>And that it!<br>
The scripts in the "LSL Scripts" section was made with 2 states and each handle a part of the process. So, you have just to read the <b><i>default</i></b> state to get the authorization part and the <b><i>authorized</i></b> state to get the invoicing part.<br>You can also merge both states in one and adapt them to your own cases of use.
</p>
<p>Thanks to BTCPay, the adoption of cryptocurrencies in the HyperGrid become a question of choice and not a technical matter.
</p>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h6 class="panel-title">
<a data-bs-toggle="collapse" class="doc-title" href="#collapse4" aria-expanded="false">Version Notes & Changelog:</a>
</h6>
</div>
<div id="collapse4" class="panel-collapse collapse">
<div class="panel-body">
<span class="commented">v0.1.1 Pre-Release:</span>
<p>This is the very first version made as proof of concept and work of BTCPay as a solution to accept cryptocurrencies payments in the Opensimulator virtual Worlds without any need to an opensim region module or exposing sensive data and using just LSL scripts.</p>
<p>The Opensimulator plugin do not set or get any sensive data from / in the BTCPay database and have it's own context and SQLite database making it safe and easy to install and remove if needed. Also the plugin do not expose the BTCPay instance more than the "Pay Button" and the permissions / limitations layer makes it a bit safer. </p>
<p>This version needs an audit and the green light from BTCPay DEV team. Also need a stress test in real conditions using multiple Altcoins and 100s objects...</p>
</div>
</div>
</div>
</div>
</div>
<div id="osCreditsPage" class="osPage" style="display: none;">
<h6>Thanks Where Due:</h6>
<ul>
<li>Special thanks to the Opensimulator Dev team and project for creating and maintaining, for more than 15 years now, an amazing free and open source alternative to the Linden Lab's virtual world. I started to learn C# in 2011-12 to understand how Opensimulator works and how is possible to generate and serve something like a virtual world with just some data.</li>
<li>Special thanks to the BTCPay Server team for making easy the adoption of cryptocurrencies everywhere with this ingenious payments processing method and the multiple integrations solutions. Also, for forcing me to update my C# knowladge. I learned many new things about ASP.NET thanks to their works.</li>
<li>Special thanks to <a href="/holy-lol" target="_blank">Mr.Dorier</a> and the BTCPay team members who want to turn those scammers in borico and i hope by adding this case of use to BTCPay will make them more obsolete than they are.
<br>As is said: The karma... good or bad... soon or later... always payback.</li>
</ul>
<hr>
<h6>Used Libraries and SDKs:</h6>
<ul>
<li>Multiple Microsoft .NET products. </li>
<li><a href="https://github.com/btcpayserver/btcpayserver" target="_blank">BTCPay Server</a>.</li>
<li><a href="https://github.com/btcpayserver/btcpayserver-plugin-template" target="_blank">BTCPay Server Plugin Template</a>.</li>
<li><a href="https://getbootstrap.com/" target="_blank">Bootstrap v5.2.3</a>.</li>
<li><a href="https://jquery.com/" target="_blank">jQuery JavaScript Library v3.6.0</a>.</li>
<li><a href="http://opensimulator.org/wiki/Main_Page" target="_blank">Opensimulator v0.9.2.2</a>.</li>
<li><a href="https://wiki.secondlife.com/wiki/Getting_started_with_LSL" target="_blank">Linden Lab Scripting Language (LSL)</a> and <a href="https://wiki.secondlife.com/wiki/Category:LSL_Functions">functions</a>.</li>
<li><a href="http://opensimulator.org/wiki/OSSL" target="_blank">Opensimulator Scripting Language (OSSL)</a> and <a href="http://opensimulator.org/wiki/OSSL_Implemented">functions</a>.</li>
</ul>
<hr>
<h6>Edited using:</h6>
<ul>
<li><a href="https://code.visualstudio.com/">Visual Studio Code</a> (Plugin side).</li>
<li><a href="https://www.firestormviewer.org/windows-for-open-simulator/" target="_blank">Firestorm for Opensimulator Win-x64 v6-6-14-69596</a> (Internal scripts editor).</li>
<li><a href="http://opensimulator.org/wiki/Main_Page" target="_blank">Opensimulator v0.9.2.2</a> (YEngine compiler and scripts engine).</li>
</ul>
<hr>
<h6>About the author:</h6>
<p>
Adil El Farissi, 47 years, i am just a modest social entrepreneur and a polyvalent freelancer R&D from Tangier in Morocco.
<br>3D and virtual worlds edition and codding are ones of my multiple hobbies... So, i do some fun where i can as i can.
<br>Thank you & Enjoy <i class="fa fa-heart" style="color: #0f0;margin-left:5px;"></i>
</p>
</div>
@@ -0,0 +1 @@
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Adil El Farissi for BTCPay Server
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+8
View File
@@ -0,0 +1,8 @@
# OpenSimulator Plugin For BTCPay Server
OpenSimulator plugin for [BTCPay Server](https://github.com/btcpayserver).
Learn more in the BTCPay Server [Documentation](https://docs.btcpayserver.org/).
Learn more about [OpenSimulator](http://opensimulator.org/wiki/Main_Page).