diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..71d50ed3a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "overrides/Discord.Net.BuildOverrides"] + path = overrides/Discord.Net.BuildOverrides + url = https://github.com/discord-net/Discord.Net.BuildOverrides diff --git a/Discord.Net.sln b/Discord.Net.sln index fc68eb71c..544283b8b 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -34,7 +34,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_InteractionFramework", "sa EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_WebhookClient", "samples\WebhookClient\_WebhookClient.csproj", "{B61AAE66-15CC-40E4-873A-C23E697C3411}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IDN", "samples\idn\idn.csproj", "{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "idn", "samples\idn\idn.csproj", "{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{C7CF5621-7D36-433B-B337-5A2E3C101A71}" EndProject @@ -44,6 +44,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.BuildOverrides", "experiment\Discord.Net.BuildOverrides\Discord.Net.BuildOverrides.csproj", "{115F4921-B44D-4F69-996B-69796959C99D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -258,6 +260,18 @@ Global {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x64.Build.0 = Release|Any CPU {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x86.ActiveCfg = Release|Any CPU {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x86.Build.0 = Release|Any CPU + {115F4921-B44D-4F69-996B-69796959C99D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {115F4921-B44D-4F69-996B-69796959C99D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {115F4921-B44D-4F69-996B-69796959C99D}.Debug|x64.ActiveCfg = Debug|Any CPU + {115F4921-B44D-4F69-996B-69796959C99D}.Debug|x64.Build.0 = Debug|Any CPU + {115F4921-B44D-4F69-996B-69796959C99D}.Debug|x86.ActiveCfg = Debug|Any CPU + {115F4921-B44D-4F69-996B-69796959C99D}.Debug|x86.Build.0 = Debug|Any CPU + {115F4921-B44D-4F69-996B-69796959C99D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {115F4921-B44D-4F69-996B-69796959C99D}.Release|Any CPU.Build.0 = Release|Any CPU + {115F4921-B44D-4F69-996B-69796959C99D}.Release|x64.ActiveCfg = Release|Any CPU + {115F4921-B44D-4F69-996B-69796959C99D}.Release|x64.Build.0 = Release|Any CPU + {115F4921-B44D-4F69-996B-69796959C99D}.Release|x86.ActiveCfg = Release|Any CPU + {115F4921-B44D-4F69-996B-69796959C99D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -279,6 +293,7 @@ Global {A23E46D2-1610-4AE5-820F-422D34810887} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B} {B61AAE66-15CC-40E4-873A-C23E697C3411} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B} {4A03840B-9EBE-47E3-89AB-E0914DF21AFB} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B} + {115F4921-B44D-4F69-996B-69796959C99D} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D2404771-EEC8-45F2-9D71-F3373F6C1495} diff --git a/azure/deploy.yml b/azure/deploy.yml index 4742da3c8..d3460ad6c 100644 --- a/azure/deploy.yml +++ b/azure/deploy.yml @@ -8,6 +8,7 @@ steps: dotnet pack "src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "$(Build.ArtifactStagingDirectory)" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag) dotnet pack "src\Discord.Net.Analyzers\Discord.Net.Analyzers.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "$(Build.ArtifactStagingDirectory)" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag) dotnet pack "src\Discord.Net.Interactions\Discord.Net.Interactions.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "$(Build.ArtifactStagingDirectory)" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag) + dotnet pack "experiment\Discord.Net.BuildOverrides\Discord.Net.BuildOverrides.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "$(Build.ArtifactStagingDirectory)" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag) displayName: Pack projects - task: NuGetCommand@2 diff --git a/docs/faq/build_overrides/what-are-they.md b/docs/faq/build_overrides/what-are-they.md new file mode 100644 index 000000000..f76fd6ddb --- /dev/null +++ b/docs/faq/build_overrides/what-are-they.md @@ -0,0 +1,41 @@ +--- +uid: FAQ.BuildOverrides.WhatAreThey +title: Build Overrides, What are they? +--- + +# Build Overrides + +Build overrides are a way for library developers to override the default behavior of the library on the fly. Adding them to your code is really simple. + +## Installing the package + +The build override package can be installed on nuget [here](TODO) or by using the package manager + +``` +PM> Install-Package Discord.Net.BuildOverrides +``` + +## Adding an override + +```cs +public async Task MainAsync() +{ + // hook into the log function + BuildOverrides.Log += (buildOverride, message) => + { + Console.WriteLine($"{buildOverride.Name}: {message}"); + return Task.CompletedTask; + }; + + // add your overrides + await BuildOverrides.AddOverrideAsync("example-override-name"); +} + +``` + +Overrides are normally built for specific problems, for example if someone is having an issue and we think we might have a fix then we can create a build override for them to test out the fix. + +## Security and Transparency + +Overrides can only be created and updated by library developers, you should only apply an override if a library developer askes you to. +Code for the overrides server and the overrides themselves can be found [here](https://github.com/discord-net/Discord.Net.BuildOverrides). \ No newline at end of file diff --git a/docs/faq/toc.yml b/docs/faq/toc.yml index 97e327aba..b727f5117 100644 --- a/docs/faq/toc.yml +++ b/docs/faq/toc.yml @@ -22,3 +22,5 @@ topicUid: FAQ.TextCommands.General - name: Legacy or Upgrade topicUid: FAQ.Legacy +- name: Build Overrides + topicUid: FAQ.BuildOverrides.WhatAreThey diff --git a/experiment/Discord.Net.BuildOverrides/BuildOverrides.cs b/experiment/Discord.Net.BuildOverrides/BuildOverrides.cs new file mode 100644 index 000000000..fd15e5728 --- /dev/null +++ b/experiment/Discord.Net.BuildOverrides/BuildOverrides.cs @@ -0,0 +1,278 @@ +using Discord.Overrides; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.Loader; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents an override that can be loaded. + /// + public sealed class Override + { + /// + /// Gets the ID of the override. + /// + public Guid Id { get; internal set; } + + /// + /// Gets the name of the override. + /// + public string Name { get; internal set; } + + /// + /// Gets the description of the override. + /// + public string Description { get; internal set; } + + /// + /// Gets the date this override was created. + /// + public DateTimeOffset CreatedAt { get; internal set; } + + /// + /// Gets the date the override was last modified. + /// + public DateTimeOffset LastUpdated { get; internal set; } + + internal static Override FromJson(string json) + { + var result = new Override(); + + using(var textReader = new StringReader(json)) + using(var reader = new JsonTextReader(textReader)) + { + var obj = JObject.ReadFrom(reader); + result.Id = obj["id"].ToObject(); + result.Name = obj["name"].ToObject(); + result.Description = obj["description"].ToObject(); + result.CreatedAt = obj["created_at"].ToObject(); + result.LastUpdated = obj["last_updated"].ToObject(); + } + + return result; + } + } + + /// + /// Represents a loaded override instance. + /// + public sealed class LoadedOverride + { + /// + /// Gets the aseembly containing the overrides definition. + /// + public Assembly Assembly { get; internal set; } + + /// + /// Gets an instance of the override. + /// + public IOverride Instance { get; internal set; } + + /// + /// Gets the overrides type. + /// + public Type Type { get; internal set; } + } + + public sealed class BuildOverrides + { + /// + /// Fired when an override logs a message. + /// + public static event Func Log + { + add => _logEvents.Add(value); + remove => _logEvents.Remove(value); + + } + + /// + /// Gets a read-only dictionary containing the currently loaded overrides. + /// + public IReadOnlyDictionary> LoadedOverrides + => _loadedOverrides.Select(x => new KeyValuePair> (x.Key, x.Value)).ToDictionary(x => x.Key, x => x.Value); + + private static AssemblyLoadContext _overrideDomain; + private static List> _logEvents = new(); + private static ConcurrentDictionary> _loadedOverrides = new ConcurrentDictionary>(); + + private const string ApiUrl = "https://overrides.discordnet.dev"; + + static BuildOverrides() + { + _overrideDomain = new AssemblyLoadContext("Discord.Net.Overrides.Runtime"); + + _overrideDomain.Resolving += _overrideDomain_Resolving; + } + + /// + /// Gets details about a specific override. + /// + /// + /// Note: This method does not load an override, it simply retrives the info about it. + /// + /// The name of the override to get. + /// + /// A task representing the asynchronous get operation. The tasks result is an + /// if it exists; otherwise . + /// + public static async Task GetOverrideAsync(string name) + { + using (var client = new HttpClient()) + { + var result = await client.GetAsync($"{ApiUrl}/override/{name}"); + + if (result.IsSuccessStatusCode) + { + var content = await result.Content.ReadAsStringAsync(); + + return Override.FromJson(content); + } + else + return null; + } + } + + /// + /// Adds an override to the current Discord.Net instance. + /// + /// + /// The override initialization is non-blocking, any errors that occor within + /// the overrides initialization procedure will be sent in the event. + /// + /// The name of the override to add. + /// + /// A task representing the asynchronous add operaton. The tasks result is a boolean + /// determining if the add operation was successful. + /// + public static async Task AddOverrideAsync(string name) + { + var ovrride = await GetOverrideAsync(name); + + if (ovrride == null) + return false; + + return await AddOverrideAsync(ovrride); + } + + /// + /// Adds an override to the current Discord.Net instance. + /// + /// + /// The override initialization is non-blocking, any errors that occor within + /// the overrides initialization procedure will be sent in the event. + /// + /// The override to add. + /// + /// A task representing the asynchronous add operaton. The tasks result is a boolean + /// determining if the add operation was successful. + /// + public static async Task AddOverrideAsync(Override ovrride) + { + // download it + var ms = new MemoryStream(); + + using (var client = new HttpClient()) + { + var result = await client.GetAsync($"{ApiUrl}/override/download/{ovrride.Id}"); + + if (!result.IsSuccessStatusCode) + return false; + + await (await result.Content.ReadAsStreamAsync()).CopyToAsync(ms); + } + + ms.Position = 0; + + // load the assembly + //var test = Assembly.Load(ms.ToArray()); + var asm = _overrideDomain.LoadFromStream(ms); + + // find out IOverride + var overrides = asm.GetTypes().Where(x => x.GetInterfaces().Any(x => x == typeof(IOverride))); + + List loaded = new(); + + var context = new OverrideContext((m) => HandleLog(ovrride, m), ovrride); + + foreach (var ovr in overrides) + { + var inst = (IOverride)Activator.CreateInstance(ovr); + + inst.RegisterPackageLookupHandler((s) => + { + return GetDependencyAsync(ovrride.Id, s); + }); + + _ = Task.Run(async () => + { + try + { + await inst.InitializeAsync(context); + } + catch (Exception x) + { + HandleLog(ovrride, $"Failed to initialize build override: {x}"); + } + }); + + loaded.Add(new LoadedOverride() + { + Assembly = asm, + Instance = inst, + Type = ovr + }); + } + + return _loadedOverrides.AddOrUpdate(ovrride, loaded, (_, __) => loaded) != null; + } + + internal static void HandleLog(Override ovr, string msg) + { + _ = Task.Run(async () => + { + foreach (var item in _logEvents) + { + await item.Invoke(ovr, msg).ConfigureAwait(false); + } + }); + } + + private static Assembly _overrideDomain_Resolving(AssemblyLoadContext arg1, AssemblyName arg2) + { + // resolve the override id + var v = _loadedOverrides.FirstOrDefault(x => x.Value.Any(x => x.Assembly.FullName == arg1.Assemblies.FirstOrDefault().FullName)); + + return GetDependencyAsync(v.Key.Id, $"{arg2}").GetAwaiter().GetResult(); + } + + private static async Task GetDependencyAsync(Guid id, string name) + { + using(var client = new HttpClient()) + { + var result = await client.PostAsync($"{ApiUrl}/override/{id}/dependency", new StringContent($"{{ \"info\": \"{name}\"}}", Encoding.UTF8, "application/json")); + + if (!result.IsSuccessStatusCode) + throw new Exception("Failed to get dependency"); + + using(var ms = new MemoryStream()) + { + var innerStream = await result.Content.ReadAsStreamAsync(); + await innerStream.CopyToAsync(ms); + ms.Position = 0; + return _overrideDomain.LoadFromStream(ms); + } + } + } + } +} diff --git a/experiment/Discord.Net.BuildOverrides/Discord.Net.BuildOverrides.csproj b/experiment/Discord.Net.BuildOverrides/Discord.Net.BuildOverrides.csproj new file mode 100644 index 000000000..25b1c40b0 --- /dev/null +++ b/experiment/Discord.Net.BuildOverrides/Discord.Net.BuildOverrides.csproj @@ -0,0 +1,20 @@ + + + + 9.0 + Discord.Net.BuildOverrides + Discord.BuildOverrides + A Discord.Net extension adding a way to add build overrides for testing. + net6.0;net5.0; + net6.0;net5.0; + + + + + + + + + + + diff --git a/experiment/Discord.Net.BuildOverrides/IOverride.cs b/experiment/Discord.Net.BuildOverrides/IOverride.cs new file mode 100644 index 000000000..17327ae2c --- /dev/null +++ b/experiment/Discord.Net.BuildOverrides/IOverride.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.Overrides +{ + /// + /// Represents a generic build override for Discord.Net + /// + public interface IOverride + { + /// + /// Initializes the override. + /// + /// + /// This method is called by the class + /// and should not be called externally from it. + /// + /// Context used by an override to initialize. + /// + /// A task representing the asynchronous initialization operation. + /// + Task InitializeAsync(OverrideContext context); + + /// + /// Registers a callback to load a dependency for this override. + /// + /// The callback to load an external dependency. + void RegisterPackageLookupHandler(Func> func); + } +} diff --git a/experiment/Discord.Net.BuildOverrides/OverrideContext.cs b/experiment/Discord.Net.BuildOverrides/OverrideContext.cs new file mode 100644 index 000000000..1e88be74a --- /dev/null +++ b/experiment/Discord.Net.BuildOverrides/OverrideContext.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.Overrides +{ + /// + /// Represents context thats passed to an override in the initialization step. + /// + public sealed class OverrideContext + { + /// + /// A callback used to log messages. + /// + public Action Log { get; private set; } + + /// + /// The info about the override. + /// + public Override Info { get; private set; } + + internal OverrideContext(Action log, Override info) + { + Log = log; + Info = info; + } + } +} diff --git a/overrides/Discord.Net.BuildOverrides b/overrides/Discord.Net.BuildOverrides new file mode 160000 index 000000000..9b2be5597 --- /dev/null +++ b/overrides/Discord.Net.BuildOverrides @@ -0,0 +1 @@ +Subproject commit 9b2be5597468329090015fa1b2775815b20be440