Ver código fonte

Created initial Xero integration;

Kenric Nugteren 1 mês atrás
pai
commit
7949732b42

+ 23 - 0
InABox.Poster.Xero/IXeroPoster.cs

@@ -0,0 +1,23 @@
+using InABox.Core;
+using InABox.Scripting;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace InABox.Poster.Xero;
+
+[Caption("Xero")]
+public interface IXeroPoster<TPostable, TSettings> : IPoster<TPostable, TSettings>
+    where TPostable : Entity, IPostable, IRemotable, IPersistent, new()
+    where TSettings : PosterSettings
+{
+    ScriptDocument? Script { set; }
+
+    XeroConnectionData ConnectionData { get; set; }
+
+    bool BeforePost(IDataModel<TPostable> model);
+
+    IPostResult<TPostable> Process(IDataModel<TPostable> model);
+}

+ 16 - 0
InABox.Poster.Xero/InABox.Poster.Xero.csproj

@@ -0,0 +1,16 @@
+<Project Sdk="Microsoft.NET.Sdk">
+	<PropertyGroup>
+		<TargetFramework>net8.0-windows</TargetFramework>
+		<ImplicitUsings>enable</ImplicitUsings>
+		<Nullable>enable</Nullable>
+		<UseWPF>true</UseWPF>
+	</PropertyGroup>
+	<ItemGroup>
+	  <PackageReference Include="Microsoft.Web.WebView2" Version="1.0.2592.51" />
+	  <PackageReference Include="Xero.NetStandard.OAuth2" Version="12.4.0" />
+	  <PackageReference Include="Xero.NetStandard.OAuth2Client" Version="1.6.0" />
+	</ItemGroup>
+	<ItemGroup>
+	  <ProjectReference Include="..\InABox.Poster.Shared\InABox.Poster.Shared.csproj" />
+	</ItemGroup>
+</Project>

+ 195 - 0
InABox.Poster.Xero/XeroPosterEngine.cs

@@ -0,0 +1,195 @@
+using InABox.Core;
+using InABox.Core.Postable;
+using InABox.Poster.Shared;
+using Microsoft.Web.WebView2.Wpf;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using System.Windows;
+using Xero.NetStandard.OAuth2.Api;
+using Xero.NetStandard.OAuth2.Client;
+using Xero.NetStandard.OAuth2.Config;
+using Xero.NetStandard.OAuth2.Token;
+
+namespace InABox.Poster.Xero;
+
+public class XeroConnectionData(XeroConfiguration configuration, IXeroToken token, XeroClient client, Guid tenantID)
+{
+    public XeroConfiguration Configuration { get; set; } = configuration;
+
+    public IXeroToken Token { get; set; } = token;
+
+    public XeroClient Client { get; set; } = client;
+
+    public Guid TenantID { get; set; } = tenantID;
+}
+
+public static partial class XeroPosterEngine
+{
+    internal static readonly string CLIENT_ID = "E83E844138534229BBB0DC9291D02C94";
+    internal static readonly string CALLBACK_URI = "http://localhost:8888/callback";
+
+    private static XeroConnectionData? _connectionData;
+
+    public static XeroConnectionData? GetConnectionDataOrNull()
+    {
+        return _connectionData;
+    }
+
+    public static async Task<XeroConnectionData> GetConnectionData()
+    {
+        if(_connectionData is XeroConnectionData data)
+        {
+            return _connectionData;
+        }
+
+        var state = CreateState();
+
+        var xconfig = new XeroConfiguration();
+        xconfig.ClientId = CLIENT_ID;
+        xconfig.CallbackUri = new Uri(CALLBACK_URI);
+        xconfig.Scope = "accounting.attachments";
+        xconfig.State = state;
+
+        var client = new XeroClient(xconfig);
+
+        // generate a random codeVerifier
+        var validChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-._~";          
+        var random = new Random();
+        int charsLength = random.Next(43, 128);
+        char[] randomChars = new char[charsLength];
+        for (int i = 0; i < charsLength; i++)  
+        {
+            randomChars[i] = validChars[random.Next(0, validChars.Length)];  
+        }  
+        string codeVerifier = new string(randomChars);
+
+        var link = client.BuildLoginUriPkce(codeVerifier);
+        if (!GetCode(link, state, out var code))
+        {
+            throw new PostCancelledException();
+        }
+        // Check State
+
+        _connectionData = Task.Run<XeroConnectionData>(async () =>
+        {
+            var token = await client.RequestAccessTokenPkceAsync(code, codeVerifier).ConfigureAwait(false);
+
+            var tenants = await client.GetConnectionsAsync(token);
+
+            if(tenants.Count == 0)
+            {
+                Logger.Send(LogType.Error, "", "Xero Error: No tenant found.");
+                throw new PostFailedMessageException("Post failed when connecting to server.");
+            }
+
+            return new(xconfig, token, client, tenants[0].TenantId);
+        }).Result;
+
+        return _connectionData;
+    }
+
+    private static string CreateState()
+    {
+        // generate a random codeVerifier
+        var validChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";          
+        var random = new Random();
+        int charsLength = random.Next(43, 128);
+        char[] randomChars = new char[charsLength];
+        for (int i = 0; i < charsLength; i++)  
+        {
+            randomChars[i] = validChars[random.Next(0, validChars.Length)];  
+        }  
+        return new string(randomChars);
+    }
+
+    private static bool GetCode(string link, string state, [NotNullWhen(true)] out string? code)
+    {
+        var url = link;
+
+        var window = new Window
+        {
+            Width = 800,
+            Height = 600,
+            Title = "Sign in to Xero"
+        };
+
+        string? resultCode = null;
+        string? resultState = null;
+
+        var view = new WebView2
+        {
+        };
+        view.NavigationStarting += (o, e) =>
+        {
+            var codeMatch = CodeRegex().Match(e.Uri);
+            if (!codeMatch.Success) return;
+
+            var stateMatch = StateRegex().Match(e.Uri);
+            if (!stateMatch.Success) return;
+
+            resultCode = codeMatch.Groups[1].Value;
+            resultState = stateMatch.Groups[1].Value;
+            window.Dispatcher.BeginInvoke(() =>
+            {
+                if (window.DialogResult is null)
+                {
+                    window.DialogResult = true;
+                    window.Close();
+                }
+            });
+        };
+        view.Source = new Uri(url);
+        window.Content = view;
+
+        var result = window.ShowDialog() == true;
+
+        if(resultState != state)
+        {
+            Logger.Send(LogType.Error, "", "State did not match for connecting to Xero");
+            throw new PostException("Error connecting to server.");
+        }
+        code = resultCode;
+
+        return result;
+    }
+
+    [GeneratedRegex("state=([A-Za-z]+)")]
+    private static partial Regex StateRegex();
+
+    [GeneratedRegex("code=([A-Za-z0-9\\-._~]+)")]
+    private static partial Regex CodeRegex();
+}
+
+public class XeroPosterEngine<TPostable, TSettings> : BasePosterEngine<TPostable, IXeroPoster<TPostable, TSettings>, TSettings>
+    where TPostable : Entity, IPostable, IRemotable, IPersistent, new()
+    where TSettings : XeroPosterSettings, new()
+{
+
+    protected override IXeroPoster<TPostable, TSettings> CreatePoster()
+    {
+        var poster = base.CreatePoster();
+        poster.Script = GetScriptDocument();
+        return poster;
+    }
+
+    public override bool BeforePost(IDataModel<TPostable> model)
+    {
+        return Poster.BeforePost(model);
+    }
+
+    protected override IPostResult<TPostable> DoProcess(IDataModel<TPostable> model)
+    {
+        Poster.ConnectionData = XeroPosterEngine.GetConnectionData().Result;
+        return Poster.Process(model);
+    }
+
+
+    public override void AfterPost(IDataModel<TPostable> model, IPostResult<TPostable> result)
+    {
+    }
+}

+ 15 - 0
InABox.Poster.Xero/XeroPosterSettings.cs

@@ -0,0 +1,15 @@
+using InABox.Core;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace InABox.Poster.Xero;
+
+public abstract class XeroPosterSettings : PosterSettings
+{
+    protected abstract string DefaultScript();
+
+    public override string DefaultScript(Type TPostable) => DefaultScript();
+}