From b83109aed4c6f89afe298d260a4af02f4cfa9ce0 Mon Sep 17 00:00:00 2001 From: Toastie Date: Thu, 13 Jun 2024 17:47:39 +1200 Subject: [PATCH] Added experiment files --- .../Discord.Net.Analyzers/AssemblyInfo.cs | 3 + .../ConfigureAwaitAnalyzer.cs | 55 ++ .../Discord.Net.Analyzers.csproj | 18 + .../BuildOverrides.cs | 278 ++++++++++ .../Discord.Net.BuildOverrides.csproj | 20 +- .../Discord.Net.BuildOverrides/IOverride.cs | 34 ++ .../OverrideContext.cs | 30 ++ .../Discord.Net.BuildOverrides/Program.cs | 2 - .../ApplicationBuilderExtensions.cs | 20 + experiment/Discord.Net.Relay/AssemblyInfo.cs | 3 + .../Discord.Net.Relay.csproj | 18 + .../Discord.Net.Relay/RelayConnection.cs | 79 +++ experiment/Discord.Net.Relay/RelayServer.cs | 103 ++++ .../API/Rpc/AuthenticateParams.cs | 11 + .../API/Rpc/AuthenticateResponse.cs | 18 + .../API/Rpc/AuthorizeParams.cs | 16 + .../API/Rpc/AuthorizeResponse.cs | 11 + experiment/Discord.Net.Rpc/API/Rpc/Channel.cs | 34 ++ .../API/Rpc/ChannelSubscriptionParams.cs | 11 + .../Discord.Net.Rpc/API/Rpc/ChannelSummary.cs | 14 + .../Discord.Net.Rpc/API/Rpc/ErrorEvent.cs | 13 + .../API/Rpc/ExtendedVoiceState.cs | 21 + .../API/Rpc/GetChannelParams.cs | 11 + .../API/Rpc/GetChannelsParams.cs | 11 + .../API/Rpc/GetChannelsResponse.cs | 12 + .../Discord.Net.Rpc/API/Rpc/GetGuildParams.cs | 11 + .../API/Rpc/GetGuildsParams.cs | 8 + .../API/Rpc/GetGuildsResponse.cs | 11 + experiment/Discord.Net.Rpc/API/Rpc/Guild.cs | 18 + .../Discord.Net.Rpc/API/Rpc/GuildMember.cs | 15 + .../API/Rpc/GuildStatusEvent.cs | 13 + .../API/Rpc/GuildSubscriptionParams.cs | 11 + .../Discord.Net.Rpc/API/Rpc/GuildSummary.cs | 12 + experiment/Discord.Net.Rpc/API/Rpc/Message.cs | 17 + .../Discord.Net.Rpc/API/Rpc/MessageEvent.cs | 12 + experiment/Discord.Net.Rpc/API/Rpc/Pan.cs | 12 + .../Discord.Net.Rpc/API/Rpc/ReadyEvent.cs | 13 + .../Discord.Net.Rpc/API/Rpc/RpcConfig.cs | 15 + .../API/Rpc/SelectChannelParams.cs | 13 + .../API/Rpc/SetLocalVolumeParams.cs | 11 + .../API/Rpc/SetLocalVolumeResponse.cs | 13 + .../Discord.Net.Rpc/API/Rpc/SpeakingEvent.cs | 11 + .../API/Rpc/SubscriptionResponse.cs | 11 + .../API/Rpc/UserVoiceSettings.cs | 18 + .../Discord.Net.Rpc/API/Rpc/VoiceDevice.cs | 12 + .../API/Rpc/VoiceDeviceSettings.cs | 14 + .../Discord.Net.Rpc/API/Rpc/VoiceMode.cs | 18 + .../Discord.Net.Rpc/API/Rpc/VoiceSettings.cs | 26 + .../Discord.Net.Rpc/API/Rpc/VoiceShortcut.cs | 15 + experiment/Discord.Net.Rpc/API/RpcFrame.cs | 20 + experiment/Discord.Net.Rpc/AssemblyInfo.cs | 3 + .../Commands/RpcCommandContext.cs | 29 ++ .../Discord.Net.Rpc/Discord.Net.Rpc.csproj | 24 + .../Discord.Net.Rpc/DiscordRpcApiClient.cs | 397 +++++++++++++++ .../DiscordRpcClient.Events.cs | 112 ++++ .../Discord.Net.Rpc/DiscordRpcClient.cs | 478 ++++++++++++++++++ .../Discord.Net.Rpc/DiscordRpcConfig.cs | 33 ++ .../Entities/Channels/IRpcAudioChannel.cs | 9 + .../Entities/Channels/IRpcMessageChannel.cs | 9 + .../Entities/Channels/IRpcPrivateChannel.cs | 6 + .../Entities/Channels/RpcCategoryChannel.cs | 36 ++ .../Entities/Channels/RpcChannel.cs | 43 ++ .../Entities/Channels/RpcChannelSummary.cs | 32 ++ .../Entities/Channels/RpcDMChannel.cs | 126 +++++ .../Entities/Channels/RpcGroupChannel.cs | 129 +++++ .../Entities/Channels/RpcGuildChannel.cs | 109 ++++ .../Entities/Channels/RpcTextChannel.cs | 134 +++++ .../Entities/Channels/RpcVoiceChannel.cs | 48 ++ .../Entities/Guilds/RpcGuild.cs | 36 ++ .../Entities/Guilds/RpcGuildStatus.cs | 30 ++ .../Entities/Guilds/RpcGuildSummary.cs | 30 ++ .../Entities/Messages/RpcMessage.cs | 75 +++ .../Entities/Messages/RpcSystemMessage.cs | 33 ++ .../Entities/Messages/RpcUserMessage.cs | 128 +++++ .../Discord.Net.Rpc/Entities/RpcEntity.cs | 17 + .../Entities/UserVoiceProperties.cs | 18 + .../Discord.Net.Rpc/Entities/Users/Pan.cs | 25 + .../Entities/Users/RpcGuildUser.cs | 29 ++ .../Discord.Net.Rpc/Entities/Users/RpcUser.cs | 65 +++ .../Entities/Users/RpcVoiceState.cs | 79 +++ .../Entities/Users/RpcWebhookUser.cs | 25 + .../Discord.Net.Rpc/Entities/VoiceDevice.cs | 25 + .../Entities/VoiceDeviceProperties.cs | 9 + .../Entities/VoiceModeProperties.cs | 11 + .../Entities/VoiceProperties.cs | 14 + .../Discord.Net.Rpc/Entities/VoiceSettings.cs | 76 +++ .../Discord.Net.Rpc/Entities/VoiceShortcut.cs | 27 + .../Entities/VoiceShortcutType.cs | 10 + .../Extensions/EntityExtensions.cs | 31 ++ experiment/Discord.Net.Rpc/RpcChannelEvent.cs | 14 + experiment/Discord.Net.Rpc/RpcGlobalEvent.cs | 9 + experiment/Discord.Net.Rpc/RpcGuildEvent.cs | 7 + 92 files changed, 3770 insertions(+), 6 deletions(-) create mode 100644 experiment/Discord.Net.Analyzers/AssemblyInfo.cs create mode 100644 experiment/Discord.Net.Analyzers/ConfigureAwaitAnalyzer.cs create mode 100644 experiment/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj create mode 100644 experiment/Discord.Net.BuildOverrides/BuildOverrides.cs create mode 100644 experiment/Discord.Net.BuildOverrides/IOverride.cs create mode 100644 experiment/Discord.Net.BuildOverrides/OverrideContext.cs delete mode 100644 experiment/Discord.Net.BuildOverrides/Program.cs create mode 100644 experiment/Discord.Net.Relay/ApplicationBuilderExtensions.cs create mode 100644 experiment/Discord.Net.Relay/AssemblyInfo.cs create mode 100644 experiment/Discord.Net.Relay/Discord.Net.Relay.csproj create mode 100644 experiment/Discord.Net.Relay/RelayConnection.cs create mode 100644 experiment/Discord.Net.Relay/RelayServer.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/AuthenticateParams.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/AuthenticateResponse.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/AuthorizeParams.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/AuthorizeResponse.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/Channel.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/ChannelSubscriptionParams.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/ChannelSummary.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/ErrorEvent.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/ExtendedVoiceState.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/GetChannelParams.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/GetChannelsParams.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/GetChannelsResponse.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/GetGuildParams.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/GetGuildsParams.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/GetGuildsResponse.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/Guild.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/GuildMember.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/GuildStatusEvent.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/GuildSubscriptionParams.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/GuildSummary.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/Message.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/MessageEvent.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/Pan.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/ReadyEvent.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/RpcConfig.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/SelectChannelParams.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/SetLocalVolumeParams.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/SetLocalVolumeResponse.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/SpeakingEvent.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/SubscriptionResponse.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/UserVoiceSettings.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/VoiceDevice.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/VoiceDeviceSettings.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/VoiceMode.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/VoiceSettings.cs create mode 100644 experiment/Discord.Net.Rpc/API/Rpc/VoiceShortcut.cs create mode 100644 experiment/Discord.Net.Rpc/API/RpcFrame.cs create mode 100644 experiment/Discord.Net.Rpc/AssemblyInfo.cs create mode 100644 experiment/Discord.Net.Rpc/Commands/RpcCommandContext.cs create mode 100644 experiment/Discord.Net.Rpc/Discord.Net.Rpc.csproj create mode 100644 experiment/Discord.Net.Rpc/DiscordRpcApiClient.cs create mode 100644 experiment/Discord.Net.Rpc/DiscordRpcClient.Events.cs create mode 100644 experiment/Discord.Net.Rpc/DiscordRpcClient.cs create mode 100644 experiment/Discord.Net.Rpc/DiscordRpcConfig.cs create mode 100644 experiment/Discord.Net.Rpc/Entities/Channels/IRpcAudioChannel.cs create mode 100644 experiment/Discord.Net.Rpc/Entities/Channels/IRpcMessageChannel.cs create mode 100644 experiment/Discord.Net.Rpc/Entities/Channels/IRpcPrivateChannel.cs create mode 100644 experiment/Discord.Net.Rpc/Entities/Channels/RpcCategoryChannel.cs create mode 100644 experiment/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs create mode 100644 experiment/Discord.Net.Rpc/Entities/Channels/RpcChannelSummary.cs create mode 100644 experiment/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs create mode 100644 experiment/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs create mode 100644 experiment/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs create mode 100644 experiment/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs create mode 100644 experiment/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs create mode 100644 experiment/Discord.Net.Rpc/Entities/Guilds/RpcGuild.cs create mode 100644 experiment/Discord.Net.Rpc/Entities/Guilds/RpcGuildStatus.cs create mode 100644 experiment/Discord.Net.Rpc/Entities/Guilds/RpcGuildSummary.cs create mode 100644 experiment/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs create mode 100644 experiment/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs create mode 100644 experiment/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs create mode 100644 experiment/Discord.Net.Rpc/Entities/RpcEntity.cs create mode 100644 experiment/Discord.Net.Rpc/Entities/UserVoiceProperties.cs create mode 100644 experiment/Discord.Net.Rpc/Entities/Users/Pan.cs create mode 100644 experiment/Discord.Net.Rpc/Entities/Users/RpcGuildUser.cs create mode 100644 experiment/Discord.Net.Rpc/Entities/Users/RpcUser.cs create mode 100644 experiment/Discord.Net.Rpc/Entities/Users/RpcVoiceState.cs create mode 100644 experiment/Discord.Net.Rpc/Entities/Users/RpcWebhookUser.cs create mode 100644 experiment/Discord.Net.Rpc/Entities/VoiceDevice.cs create mode 100644 experiment/Discord.Net.Rpc/Entities/VoiceDeviceProperties.cs create mode 100644 experiment/Discord.Net.Rpc/Entities/VoiceModeProperties.cs create mode 100644 experiment/Discord.Net.Rpc/Entities/VoiceProperties.cs create mode 100644 experiment/Discord.Net.Rpc/Entities/VoiceSettings.cs create mode 100644 experiment/Discord.Net.Rpc/Entities/VoiceShortcut.cs create mode 100644 experiment/Discord.Net.Rpc/Entities/VoiceShortcutType.cs create mode 100644 experiment/Discord.Net.Rpc/Extensions/EntityExtensions.cs create mode 100644 experiment/Discord.Net.Rpc/RpcChannelEvent.cs create mode 100644 experiment/Discord.Net.Rpc/RpcGlobalEvent.cs create mode 100644 experiment/Discord.Net.Rpc/RpcGuildEvent.cs diff --git a/experiment/Discord.Net.Analyzers/AssemblyInfo.cs b/experiment/Discord.Net.Analyzers/AssemblyInfo.cs new file mode 100644 index 0000000..5e9efa5 --- /dev/null +++ b/experiment/Discord.Net.Analyzers/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Discord.Net.Tests")] \ No newline at end of file diff --git a/experiment/Discord.Net.Analyzers/ConfigureAwaitAnalyzer.cs b/experiment/Discord.Net.Analyzers/ConfigureAwaitAnalyzer.cs new file mode 100644 index 0000000..97382f3 --- /dev/null +++ b/experiment/Discord.Net.Analyzers/ConfigureAwaitAnalyzer.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace RegexAnalyzer +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class ConfigureAwaitAnalyzer : DiagnosticAnalyzer + { + public const string DiagnosticId = "ConfigureAwait"; + internal const string Title = "ConfigureAwait was not specified"; + internal const string MessageFormat = "ConfigureAwait error {0}"; + internal const string Description = "ConfigureAwait(false) should be used."; + internal const string Category = "Usage"; + internal static DiagnosticDescriptor Rule = + new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, + Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.InvocationExpression); + } + + private void AnalyzeNode(SyntaxNodeAnalysisContext context) + { + /*var invocationExpr = (InvocationExpressionSyntax)context.Node; + var memberAccessExpr = invocationExpr.Expression as MemberAccessExpressionSyntax; + if (memberAccessExpr?.Name.ToString() != "Match") return; + var memberSymbol = context.SemanticModel.GetSymbolInfo(memberAccessExpr).Symbol as IMethodSymbol; + if (!memberSymbol?.ToString().StartsWith("System.Text.RegularExpressions.Regex.Match") ?? true) return; + var argumentList = invocationExpr.ArgumentList as ArgumentListSyntax; + if ((argumentList?.Arguments.Count ?? 0) < 2) return; + var regexLiteral = argumentList.Arguments[1].Expression as LiteralExpressionSyntax; + if (regexLiteral == null) return; + var regexOpt = context.SemanticModel.GetConstantValue(regexLiteral); + if (!regexOpt.HasValue) return; + var regex = regexOpt.Value as string; + if (regex == null) return; + try + { + System.Text.RegularExpressions.Regex.Match("", regex); + } + catch (ArgumentException e) + { + var diagnostic = Diagnostic.Create(Rule, regexLiteral.GetLocation(), e.Message); + context.ReportDiagnostic(diagnostic); + }*/ + } + } +} \ No newline at end of file diff --git a/experiment/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj b/experiment/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj new file mode 100644 index 0000000..8654169 --- /dev/null +++ b/experiment/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj @@ -0,0 +1,18 @@ + + + + Discord.Net.Analyzers + Discord.Analyzers + A Discord.Net extension adding compile-time analysis. + netstandard1.3 + $(PackageTargetFallback);portable-net45+win81 + + + + + + + all + + + \ No newline at end of file diff --git a/experiment/Discord.Net.BuildOverrides/BuildOverrides.cs b/experiment/Discord.Net.BuildOverrides/BuildOverrides.cs new file mode 100644 index 0000000..9146898 --- /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 assembly 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 retrieves 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}/overrides/{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 occur within + /// the overrides initialization procedure will be sent in the event. + /// + /// The name of the override to add. + /// + /// A task representing the asynchronous add operation. 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 occur within + /// the overrides initialization procedure will be sent in the event. + /// + /// The override to add. + /// + /// A task representing the asynchronous add operation. 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}/overrides/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.First().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}/overrides/{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 index 74abf5c..8667a22 100644 --- a/experiment/Discord.Net.BuildOverrides/Discord.Net.BuildOverrides.csproj +++ b/experiment/Discord.Net.BuildOverrides/Discord.Net.BuildOverrides.csproj @@ -1,10 +1,22 @@ - Exe - net6.0 - enable - enable + 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; + false + false + + + + + + + + diff --git a/experiment/Discord.Net.BuildOverrides/IOverride.cs b/experiment/Discord.Net.BuildOverrides/IOverride.cs new file mode 100644 index 0000000..17327ae --- /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 0000000..0d683f9 --- /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 that's 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/experiment/Discord.Net.BuildOverrides/Program.cs b/experiment/Discord.Net.BuildOverrides/Program.cs deleted file mode 100644 index 3751555..0000000 --- a/experiment/Discord.Net.BuildOverrides/Program.cs +++ /dev/null @@ -1,2 +0,0 @@ -// See https://aka.ms/new-console-template for more information -Console.WriteLine("Hello, World!"); diff --git a/experiment/Discord.Net.Relay/ApplicationBuilderExtensions.cs b/experiment/Discord.Net.Relay/ApplicationBuilderExtensions.cs new file mode 100644 index 0000000..2a1e759 --- /dev/null +++ b/experiment/Discord.Net.Relay/ApplicationBuilderExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Builder; +using System; + +namespace Discord.Relay +{ + public static class ApplicationBuilderExtensions + { + public static void UseDiscordRelay(this IApplicationBuilder app, Action configAction = null) + { + var server = new RelayServer(configAction); + server.StartAsync(); + app.Use(async (context, next) => + { + if (context.WebSockets.IsWebSocketRequest) + await server.AcceptAsync(context); + await next(); + }); + } + } +} diff --git a/experiment/Discord.Net.Relay/AssemblyInfo.cs b/experiment/Discord.Net.Relay/AssemblyInfo.cs new file mode 100644 index 0000000..5e9efa5 --- /dev/null +++ b/experiment/Discord.Net.Relay/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Discord.Net.Tests")] \ No newline at end of file diff --git a/experiment/Discord.Net.Relay/Discord.Net.Relay.csproj b/experiment/Discord.Net.Relay/Discord.Net.Relay.csproj new file mode 100644 index 0000000..2e91016 --- /dev/null +++ b/experiment/Discord.Net.Relay/Discord.Net.Relay.csproj @@ -0,0 +1,18 @@ + + + + Discord.Net.Relay + Discord.Relay + A core Discord.Net library containing the Relay server. + netstandard1.3 + + + + + + + + + + + \ No newline at end of file diff --git a/experiment/Discord.Net.Relay/RelayConnection.cs b/experiment/Discord.Net.Relay/RelayConnection.cs new file mode 100644 index 0000000..ffce74f --- /dev/null +++ b/experiment/Discord.Net.Relay/RelayConnection.cs @@ -0,0 +1,79 @@ +using Discord.API; +using Discord.API.Gateway; +using Discord.Logging; +using System; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; +using WebSocketClient = System.Net.WebSockets.WebSocket; + +namespace Discord.Relay +{ + public class RelayConnection + { + private readonly RelayServer _server; + private readonly WebSocketClient _socket; + private readonly CancellationTokenSource _cancelToken; + private readonly byte[] _inBuffer, _outBuffer; + private readonly Logger _logger; + + internal RelayConnection(RelayServer server, WebSocketClient socket, int id) + { + _server = server; + _socket = socket; + _cancelToken = new CancellationTokenSource(); + _inBuffer = new byte[4000]; + _outBuffer = new byte[4000]; + _logger = server.LogManager.CreateLogger($"Client #{id}"); + } + + internal async Task RunAsync() + { + await _logger.InfoAsync($"Connected"); + var token = _cancelToken.Token; + try + { + var segment = new ArraySegment(_inBuffer); + + //Send HELLO + await SendAsync(GatewayOpCode.Hello, new HelloEvent { HeartbeatInterval = 15000 }).ConfigureAwait(false); + + while (_socket.State == WebSocketState.Open) + { + var result = await _socket.ReceiveAsync(segment, token).ConfigureAwait(false); + if (result.MessageType == WebSocketMessageType.Close) + await _logger.WarningAsync($"Received Close {result.CloseStatus} ({result.CloseStatusDescription ?? "No Reason"})").ConfigureAwait(false); + else + await _logger.InfoAsync($"Received {result.Count} bytes"); + } + } + catch (OperationCanceledException) + { + try { await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None).ConfigureAwait(false); } + catch { } + } + catch (Exception ex) + { + try { await _socket.CloseAsync(WebSocketCloseStatus.InternalServerError, ex.Message, CancellationToken.None).ConfigureAwait(false); } + catch { } + } + finally + { + await _logger.InfoAsync($"Disconnected"); + } + } + + internal void Stop() + { + _cancelToken.Cancel(); + } + + private async Task SendAsync(GatewayOpCode opCode, object payload) + { + var frame = new SocketFrame { Operation = (int)opCode, Payload = payload }; + var bytes = _server.Serialize(frame, _outBuffer); + var segment = new ArraySegment(_outBuffer, 0, bytes); + await _socket.SendAsync(segment, WebSocketMessageType.Text, true, CancellationToken.None).ConfigureAwait(false); + } + } +} diff --git a/experiment/Discord.Net.Relay/RelayServer.cs b/experiment/Discord.Net.Relay/RelayServer.cs new file mode 100644 index 0000000..4082191 --- /dev/null +++ b/experiment/Discord.Net.Relay/RelayServer.cs @@ -0,0 +1,103 @@ +using Discord.API; +using Discord.Logging; +using Discord.Net.Rest; +using Discord.Net.WebSockets; +using Discord.Rest; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using WebSocketClient = System.Net.WebSockets.WebSocket; + +namespace Discord.Relay +{ + public class RelayServer + { + public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } + internal readonly AsyncEvent> _logEvent = new AsyncEvent>(); + + private readonly HashSet _connections; + private readonly SemaphoreSlim _lock; + private readonly JsonSerializer _serializer; + private readonly DiscordSocketApiClient _discord; + private int _nextId; + + internal LogManager LogManager { get; } + + internal RelayServer(Action configAction) + { + _connections = new HashSet(); + _lock = new SemaphoreSlim(1, 1); + _serializer = new JsonSerializer(); + _discord = new DiscordSocketApiClient( + DefaultRestClientProvider.Instance, + DefaultWebSocketProvider.Instance, + DiscordRestConfig.UserAgent); + configAction?.Invoke(this); + + LogManager = new LogManager(LogSeverity.Debug); + LogManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); + } + + internal async Task AcceptAsync(HttpContext context) + { + WebSocketClient socket; + try + { + socket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false); + } + catch { return; } + + var _ = Task.Run(async () => + { + var conn = new RelayConnection(this, socket, Interlocked.Increment(ref _nextId)); + await AddConnection(conn).ConfigureAwait(false); + try + { + await conn.RunAsync().ConfigureAwait(false); + } + finally { await RemoveConnection(conn).ConfigureAwait(false); } + }); + } + + internal void StartAsync() + { + Task.Run(async () => + { + await _discord.ConnectAsync().ConfigureAwait(false); + }); + } + + internal async Task AddConnection(RelayConnection conn) + { + await _lock.WaitAsync().ConfigureAwait(false); + try + { + _connections.Add(conn); + } + finally { _lock.Release(); } + } + internal async Task RemoveConnection(RelayConnection conn) + { + await _lock.WaitAsync().ConfigureAwait(false); + try + { + _connections.Remove(conn); + } + finally { _lock.Release(); } + } + + internal int Serialize(object obj, byte[] buffer) + { + using (var stream = new MemoryStream(buffer)) + using (var writer = new StreamWriter(stream)) + { + _serializer.Serialize(writer, obj); + return (int)stream.Position; + } + } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/AuthenticateParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/AuthenticateParams.cs new file mode 100644 index 0000000..f4b69a3 --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/AuthenticateParams.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + internal class AuthenticateParams + { + [JsonProperty("access_token")] + public string AccessToken { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/AuthenticateResponse.cs b/experiment/Discord.Net.Rpc/API/Rpc/AuthenticateResponse.cs new file mode 100644 index 0000000..6c6cba9 --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/AuthenticateResponse.cs @@ -0,0 +1,18 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; +using System; + +namespace Discord.API.Rpc +{ + internal class AuthenticateResponse + { + [JsonProperty("application")] + public Application Application { get; set; } + [JsonProperty("expires")] + public DateTimeOffset Expires { get; set; } + [JsonProperty("user")] + public User User { get; set; } + [JsonProperty("scopes")] + public string[] Scopes { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/AuthorizeParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/AuthorizeParams.cs new file mode 100644 index 0000000..91678d9 --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/AuthorizeParams.cs @@ -0,0 +1,16 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Discord.API.Rpc +{ + internal class AuthorizeParams + { + [JsonProperty("client_id")] + public string ClientId { get; set; } + [JsonProperty("scopes")] + public IReadOnlyCollection Scopes { get; set; } + [JsonProperty("rpc_token")] + public Optional RpcToken { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/AuthorizeResponse.cs b/experiment/Discord.Net.Rpc/API/Rpc/AuthorizeResponse.cs new file mode 100644 index 0000000..42a9138 --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/AuthorizeResponse.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + internal class AuthorizeResponse + { + [JsonProperty("code")] + public string Code { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/Channel.cs b/experiment/Discord.Net.Rpc/API/Rpc/Channel.cs new file mode 100644 index 0000000..0fc7ac0 --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/Channel.cs @@ -0,0 +1,34 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + internal class Channel + { + //Shared + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("type")] + public ChannelType Type { get; set; } + + //GuildChannel + [JsonProperty("guild_id")] + public Optional GuildId { get; set; } + [JsonProperty("name")] + public Optional Name { get; set; } + [JsonProperty("position")] + public Optional Position { get; set; } + + //IMessageChannel + [JsonProperty("messages")] + public Message[] Messages { get; set; } + + //VoiceChannel + [JsonProperty("bitrate")] + public Optional Bitrate { get; set; } + [JsonProperty("user_limit")] + public Optional UserLimit { get; set; } + [JsonProperty("voice_states")] + public ExtendedVoiceState[] VoiceStates { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/ChannelSubscriptionParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/ChannelSubscriptionParams.cs new file mode 100644 index 0000000..7443a6f --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/ChannelSubscriptionParams.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + internal class ChannelSubscriptionParams + { + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/ChannelSummary.cs b/experiment/Discord.Net.Rpc/API/Rpc/ChannelSummary.cs new file mode 100644 index 0000000..43e59c1 --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/ChannelSummary.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + internal class ChannelSummary + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("type")] + public ChannelType Type { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/ErrorEvent.cs b/experiment/Discord.Net.Rpc/API/Rpc/ErrorEvent.cs new file mode 100644 index 0000000..c592751 --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/ErrorEvent.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + internal class ErrorEvent + { + [JsonProperty("code")] + public int Code { get; set; } + [JsonProperty("message")] + public string Message { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/ExtendedVoiceState.cs b/experiment/Discord.Net.Rpc/API/Rpc/ExtendedVoiceState.cs new file mode 100644 index 0000000..6722d3a --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/ExtendedVoiceState.cs @@ -0,0 +1,21 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + internal class ExtendedVoiceState + { + [JsonProperty("user")] + public User User { get; set; } + [JsonProperty("voice_state")] + public Optional VoiceState { get; set; } + [JsonProperty("nick")] + public Optional Nickname { get; set; } + [JsonProperty("volume")] + public Optional Volume { get; set; } + [JsonProperty("mute")] + public Optional Mute { get; set; } + [JsonProperty("pan")] + public Optional Pan { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/GetChannelParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/GetChannelParams.cs new file mode 100644 index 0000000..4c0e186 --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/GetChannelParams.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + internal class GetChannelParams + { + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/GetChannelsParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/GetChannelsParams.cs new file mode 100644 index 0000000..61e4886 --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/GetChannelsParams.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + internal class GetChannelsParams + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/GetChannelsResponse.cs b/experiment/Discord.Net.Rpc/API/Rpc/GetChannelsResponse.cs new file mode 100644 index 0000000..004da31 --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/GetChannelsResponse.cs @@ -0,0 +1,12 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Discord.API.Rpc +{ + internal class GetChannelsResponse + { + [JsonProperty("channels")] + public IReadOnlyCollection Channels { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/GetGuildParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/GetGuildParams.cs new file mode 100644 index 0000000..54d5018 --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/GetGuildParams.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + internal class GetGuildParams + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/GetGuildsParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/GetGuildsParams.cs new file mode 100644 index 0000000..b4350ea --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/GetGuildsParams.cs @@ -0,0 +1,8 @@ +#pragma warning disable CS1591 + +namespace Discord.API.Rpc +{ + internal class GetGuildsParams + { + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/GetGuildsResponse.cs b/experiment/Discord.Net.Rpc/API/Rpc/GetGuildsResponse.cs new file mode 100644 index 0000000..4d57ae4 --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/GetGuildsResponse.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + internal class GetGuildsResponse + { + [JsonProperty("guilds")] + public GuildSummary[] Guilds { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/Guild.cs b/experiment/Discord.Net.Rpc/API/Rpc/Guild.cs new file mode 100644 index 0000000..fde5ef2 --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/Guild.cs @@ -0,0 +1,18 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Discord.API.Rpc +{ + internal class Guild + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("icon_url")] + public string IconUrl { get; set; } + [JsonProperty("members")] + public IEnumerable Members { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/GuildMember.cs b/experiment/Discord.Net.Rpc/API/Rpc/GuildMember.cs new file mode 100644 index 0000000..be8fba9 --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/GuildMember.cs @@ -0,0 +1,15 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + internal class GuildMember + { + [JsonProperty("user")] + public User User { get; set; } + [JsonProperty("status")] + public UserStatus Status { get; set; } + /*[JsonProperty("activity")] + public object Activity { get; set; }*/ + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/GuildStatusEvent.cs b/experiment/Discord.Net.Rpc/API/Rpc/GuildStatusEvent.cs new file mode 100644 index 0000000..3cfbf34 --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/GuildStatusEvent.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + internal class GuildStatusEvent + { + [JsonProperty("guild")] + public Guild Guild { get; set; } + [JsonProperty("online")] + public int Online { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/GuildSubscriptionParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/GuildSubscriptionParams.cs new file mode 100644 index 0000000..a34c710 --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/GuildSubscriptionParams.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + internal class GuildSubscriptionParams + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/GuildSummary.cs b/experiment/Discord.Net.Rpc/API/Rpc/GuildSummary.cs new file mode 100644 index 0000000..09928e1 --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/GuildSummary.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + internal class GuildSummary + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/Message.cs b/experiment/Discord.Net.Rpc/API/Rpc/Message.cs new file mode 100644 index 0000000..6cbd364 --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/Message.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + internal class Message : Discord.API.Message + { + [JsonProperty("blocked")] + public Optional IsBlocked { get; } + [JsonProperty("content_parsed")] + public Optional ContentParsed { get; } + [JsonProperty("author_color")] + public Optional AuthorColor { get; } //#Hex + + [JsonProperty("mentions")] + public new Optional UserMentions { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/MessageEvent.cs b/experiment/Discord.Net.Rpc/API/Rpc/MessageEvent.cs new file mode 100644 index 0000000..4d656d5 --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/MessageEvent.cs @@ -0,0 +1,12 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; +namespace Discord.API.Rpc +{ + internal class MessageEvent + { + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + [JsonProperty("message")] + public Message Message { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/Pan.cs b/experiment/Discord.Net.Rpc/API/Rpc/Pan.cs new file mode 100644 index 0000000..dc9cbef --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/Pan.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + internal class Pan + { + [JsonProperty("left")] + public float Left { get; set; } + [JsonProperty("right")] + public float Right { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/ReadyEvent.cs b/experiment/Discord.Net.Rpc/API/Rpc/ReadyEvent.cs new file mode 100644 index 0000000..8de6940 --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/ReadyEvent.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + internal class ReadyEvent + { + [JsonProperty("v")] + public int Version { get; set; } + [JsonProperty("config")] + public RpcConfig Config { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/RpcConfig.cs b/experiment/Discord.Net.Rpc/API/Rpc/RpcConfig.cs new file mode 100644 index 0000000..4a8928a --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/RpcConfig.cs @@ -0,0 +1,15 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + internal class RpcConfig + { + [JsonProperty("cdn_host")] + public string CdnHost { get; set; } + [JsonProperty("api_endpoint")] + public string ApiEndpoint { get; set; } + [JsonProperty("environment")] + public string Environment { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/SelectChannelParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/SelectChannelParams.cs new file mode 100644 index 0000000..6fc9314 --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/SelectChannelParams.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + internal class SelectChannelParams + { + [JsonProperty("channel_id")] + public ulong? ChannelId { get; set; } + [JsonProperty("force")] + public Optional Force { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/SetLocalVolumeParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/SetLocalVolumeParams.cs new file mode 100644 index 0000000..345ad90 --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/SetLocalVolumeParams.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + internal class SetLocalVolumeParams + { + [JsonProperty("volume")] + public int Volume { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/SetLocalVolumeResponse.cs b/experiment/Discord.Net.Rpc/API/Rpc/SetLocalVolumeResponse.cs new file mode 100644 index 0000000..33927b7 --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/SetLocalVolumeResponse.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + internal class SetLocalVolumeResponse + { + [JsonProperty("user_id")] + public ulong UserId { get; set; } + [JsonProperty("volume")] + public int Volume { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/SpeakingEvent.cs b/experiment/Discord.Net.Rpc/API/Rpc/SpeakingEvent.cs new file mode 100644 index 0000000..913d7d7 --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/SpeakingEvent.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + internal class SpeakingEvent + { + [JsonProperty("user_id")] + public ulong UserId { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/SubscriptionResponse.cs b/experiment/Discord.Net.Rpc/API/Rpc/SubscriptionResponse.cs new file mode 100644 index 0000000..76adc8d --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/SubscriptionResponse.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + internal class SubscriptionResponse + { + [JsonProperty("evt")] + public string Event { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/UserVoiceSettings.cs b/experiment/Discord.Net.Rpc/API/Rpc/UserVoiceSettings.cs new file mode 100644 index 0000000..ff338a4 --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/UserVoiceSettings.cs @@ -0,0 +1,18 @@ +#pragma warning disable CS1591 + +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + internal class UserVoiceSettings + { + [JsonProperty("userId")] + internal ulong UserId { get; set; } + [JsonProperty("pan")] + public Optional Pan { get; set; } + [JsonProperty("volume")] + public Optional Volume { get; set; } + [JsonProperty("mute")] + public Optional Mute { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/VoiceDevice.cs b/experiment/Discord.Net.Rpc/API/Rpc/VoiceDevice.cs new file mode 100644 index 0000000..52bdef8 --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/VoiceDevice.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + internal class VoiceDevice + { + [JsonProperty("id")] + public string Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/VoiceDeviceSettings.cs b/experiment/Discord.Net.Rpc/API/Rpc/VoiceDeviceSettings.cs new file mode 100644 index 0000000..c06eb2c --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/VoiceDeviceSettings.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + internal class VoiceDeviceSettings + { + [JsonProperty("device_id")] + public Optional DeviceId { get; set; } + [JsonProperty("volume")] + public Optional Volume { get; set; } + [JsonProperty("available_devices")] + public Optional AvailableDevices { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/VoiceMode.cs b/experiment/Discord.Net.Rpc/API/Rpc/VoiceMode.cs new file mode 100644 index 0000000..35e9d45 --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/VoiceMode.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + internal class VoiceMode + { + [JsonProperty("type")] + public Optional Type { get; set; } + [JsonProperty("auto_threshold")] + public Optional AutoThreshold { get; set; } + [JsonProperty("threshold")] + public Optional Threshold { get; set; } + [JsonProperty("shortcut")] + public Optional Shortcut { get; set; } + [JsonProperty("delay")] + public Optional Delay { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/VoiceSettings.cs b/experiment/Discord.Net.Rpc/API/Rpc/VoiceSettings.cs new file mode 100644 index 0000000..11fb3b6 --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/VoiceSettings.cs @@ -0,0 +1,26 @@ +#pragma warning disable CS1591 + +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + internal class VoiceSettings + { + [JsonProperty("input")] + public VoiceDeviceSettings Input { get; set; } + [JsonProperty("output")] + public VoiceDeviceSettings Output { get; set; } + [JsonProperty("mode")] + public VoiceMode Mode { get; set; } + [JsonProperty("automatic_gain_control")] + public Optional AutomaticGainControl { get; set; } + [JsonProperty("echo_cancellation")] + public Optional EchoCancellation { get; set; } + [JsonProperty("noise_suppression")] + public Optional NoiseSuppression { get; set; } + [JsonProperty("qos")] + public Optional QualityOfService { get; set; } + [JsonProperty("silence_warning")] + public Optional SilenceWarning { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/Rpc/VoiceShortcut.cs b/experiment/Discord.Net.Rpc/API/Rpc/VoiceShortcut.cs new file mode 100644 index 0000000..65e2580 --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/Rpc/VoiceShortcut.cs @@ -0,0 +1,15 @@ +using Discord.Rpc; +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + internal class VoiceShortcut + { + [JsonProperty("type")] + public Optional Type { get; set; } + [JsonProperty("code")] + public Optional Code { get; set; } + [JsonProperty("name")] + public Optional Name { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/API/RpcFrame.cs b/experiment/Discord.Net.Rpc/API/RpcFrame.cs new file mode 100644 index 0000000..523378b --- /dev/null +++ b/experiment/Discord.Net.Rpc/API/RpcFrame.cs @@ -0,0 +1,20 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; +using System; + +namespace Discord.API.Rpc +{ + internal class RpcFrame + { + [JsonProperty("cmd")] + public string Cmd { get; set; } + [JsonProperty("nonce")] + public Optional Nonce { get; set; } + [JsonProperty("evt")] + public Optional Event { get; set; } + [JsonProperty("data")] + public Optional Data { get; set; } + [JsonProperty("args")] + public object Args { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/AssemblyInfo.cs b/experiment/Discord.Net.Rpc/AssemblyInfo.cs new file mode 100644 index 0000000..c6b5997 --- /dev/null +++ b/experiment/Discord.Net.Rpc/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Discord.Net.Tests")] \ No newline at end of file diff --git a/experiment/Discord.Net.Rpc/Commands/RpcCommandContext.cs b/experiment/Discord.Net.Rpc/Commands/RpcCommandContext.cs new file mode 100644 index 0000000..80dfddb --- /dev/null +++ b/experiment/Discord.Net.Rpc/Commands/RpcCommandContext.cs @@ -0,0 +1,29 @@ +using Discord.Rpc; + +namespace Discord.Commands +{ + public class RpcCommandContext : ICommandContext + { + public DiscordRpcClient Client { get; } + public IMessageChannel Channel { get; } + public RpcUser User { get; } + public RpcUserMessage Message { get; } + + public bool IsPrivate => Channel is IPrivateChannel; + + public RpcCommandContext(DiscordRpcClient client, RpcUserMessage msg) + { + Client = client; + Channel = msg.Channel; + User = msg.Author; + Message = msg; + } + + //ICommandContext + IDiscordClient ICommandContext.Client => Client; + IGuild ICommandContext.Guild => null; + IMessageChannel ICommandContext.Channel => Channel; + IUser ICommandContext.User => User; + IUserMessage ICommandContext.Message => Message; + } +} diff --git a/experiment/Discord.Net.Rpc/Discord.Net.Rpc.csproj b/experiment/Discord.Net.Rpc/Discord.Net.Rpc.csproj new file mode 100644 index 0000000..5572d69 --- /dev/null +++ b/experiment/Discord.Net.Rpc/Discord.Net.Rpc.csproj @@ -0,0 +1,24 @@ + + + + Discord.Net.Rpc + Discord.Rpc + A core Discord.Net library containing the RPC client and models. + net45;netstandard1.1;netstandard1.3 + + + + Net\DefaultWebSocketClient.cs + + + ConnectionManager.cs + + + + + + + + + + \ No newline at end of file diff --git a/experiment/Discord.Net.Rpc/DiscordRpcApiClient.cs b/experiment/Discord.Net.Rpc/DiscordRpcApiClient.cs new file mode 100644 index 0000000..300784f --- /dev/null +++ b/experiment/Discord.Net.Rpc/DiscordRpcApiClient.cs @@ -0,0 +1,397 @@ +#pragma warning disable CS1591 +using Discord.API.Rpc; +using Discord.Net.Queue; +using Discord.Net.Rest; +using Discord.Net.WebSockets; +using Discord.Rpc; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class DiscordRpcApiClient : DiscordRestApiClient, IDisposable + { + private abstract class RpcRequest + { + public abstract Task SetResultAsync(JToken data, JsonSerializer serializer); + public abstract Task SetExceptionAsync(JToken data, JsonSerializer serializer); + } + private class RpcRequest : RpcRequest + { + public TaskCompletionSource Promise { get; set; } + + public RpcRequest(RequestOptions options) + { + Promise = new TaskCompletionSource(); + Task.Run(async () => + { + await Task.Delay(options?.Timeout ?? 15000).ConfigureAwait(false); + Promise.TrySetCanceled(); //Doesn't need to be async, we're already in a separate task + }); + } + public override Task SetResultAsync(JToken data, JsonSerializer serializer) + { + return Promise.TrySetResultAsync(data.ToObject(serializer)); + } + public override Task SetExceptionAsync(JToken data, JsonSerializer serializer) + { + var error = data.ToObject(serializer); + return Promise.TrySetExceptionAsync(new RpcException(error.Code, error.Message)); + } + } + + private object _eventLock = new object(); + + public event Func SentRpcMessage { add { _sentRpcMessageEvent.Add(value); } remove { _sentRpcMessageEvent.Remove(value); } } + private readonly AsyncEvent> _sentRpcMessageEvent = new AsyncEvent>(); + + public event Func, Optional, Task> ReceivedRpcEvent { add { _receivedRpcEvent.Add(value); } remove { _receivedRpcEvent.Remove(value); } } + private readonly AsyncEvent, Optional, Task>> _receivedRpcEvent = new AsyncEvent, Optional, Task>>(); + public event Func Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } + private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); + + private readonly ConcurrentDictionary _requests; + private readonly IWebSocketClient _webSocketClient; + private readonly SemaphoreSlim _connectionLock; + private readonly string _clientId; + private CancellationTokenSource _stateCancelToken; + private string _origin; + + public ConnectionState ConnectionState { get; private set; } + + public DiscordRpcApiClient(string clientId, string userAgent, string origin, RestClientProvider restClientProvider, WebSocketProvider webSocketProvider, + RetryMode defaultRetryMode = RetryMode.AlwaysRetry, JsonSerializer serializer = null) + : base(restClientProvider, userAgent, defaultRetryMode, serializer) + { + _connectionLock = new SemaphoreSlim(1, 1); + _clientId = clientId; + _origin = origin; + + _requests = new ConcurrentDictionary(); + + _webSocketClient = webSocketProvider(); + //_webSocketClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .Net 4.6+) + _webSocketClient.SetHeader("origin", _origin); + _webSocketClient.BinaryMessage += async (data, index, count) => + { + using (var compressed = new MemoryStream(data, index + 2, count - 2)) + using (var decompressed = new MemoryStream()) + { + using (var zlib = new DeflateStream(compressed, CompressionMode.Decompress)) + zlib.CopyTo(decompressed); + decompressed.Position = 0; + using (var reader = new StreamReader(decompressed)) + using (var jsonReader = new JsonTextReader(reader)) + { + var msg = _serializer.Deserialize(jsonReader); + await _receivedRpcEvent.InvokeAsync(msg.Cmd, msg.Event, msg.Data).ConfigureAwait(false); + if (msg.Nonce.IsSpecified && msg.Nonce.Value.HasValue) + ProcessMessage(msg); + } + } + }; + _webSocketClient.TextMessage += async text => + { + using (var reader = new StringReader(text)) + using (var jsonReader = new JsonTextReader(reader)) + { + var msg = _serializer.Deserialize(jsonReader); + await _receivedRpcEvent.InvokeAsync(msg.Cmd, msg.Event, msg.Data).ConfigureAwait(false); + if (msg.Nonce.IsSpecified && msg.Nonce.Value.HasValue) + ProcessMessage(msg); + } + }; + _webSocketClient.Closed += async ex => + { + await DisconnectAsync().ConfigureAwait(false); + await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); + }; + } + internal override void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + _stateCancelToken?.Dispose(); + (_webSocketClient as IDisposable)?.Dispose(); + } + _isDisposed = true; + } + } + + public async Task ConnectAsync() + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await ConnectInternalAsync().ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + internal override async Task ConnectInternalAsync() + { + /*if (LoginState != LoginState.LoggedIn) + throw new InvalidOperationException("Client is not logged in.");*/ + + ConnectionState = ConnectionState.Connecting; + try + { + _stateCancelToken = new CancellationTokenSource(); + if (_webSocketClient != null) + _webSocketClient.SetCancelToken(_stateCancelToken.Token); + + bool success = false; + int port; + string uuid = Guid.NewGuid().ToString(); + + for ( port = DiscordRpcConfig.PortRangeStart; port <= DiscordRpcConfig.PortRangeEnd; port++) + { + try + { + string url = $"wss://{uuid}.discordapp.io:{port}/?v={DiscordRpcConfig.RpcAPIVersion}&client_id={_clientId}"; + await _webSocketClient.ConnectAsync(url).ConfigureAwait(false); + success = true; + break; + } + catch (Exception) + { + } + } + + if (!success) + throw new Exception("Unable to connect to the RPC server."); + + SetBaseUrl($"https://{uuid}.discordapp.io:{port}/"); + ConnectionState = ConnectionState.Connected; + } + catch (Exception) + { + await DisconnectInternalAsync().ConfigureAwait(false); + throw; + } + } + + public async Task DisconnectAsync() + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await DisconnectInternalAsync().ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + internal override async Task DisconnectInternalAsync() + { + if (_webSocketClient == null) + throw new NotSupportedException("This client is not configured with WebSocket support."); + + if (ConnectionState == ConnectionState.Disconnected) return; + ConnectionState = ConnectionState.Disconnecting; + + try { _stateCancelToken?.Cancel(false); } + catch { } + + await _webSocketClient.DisconnectAsync().ConfigureAwait(false); + + ConnectionState = ConnectionState.Disconnected; + } + + //Core + public async Task SendRpcAsync(string cmd, object payload, Optional evt = default(Optional), RequestOptions options = null) + where TResponse : class + { + return await SendRpcAsyncInternal(cmd, payload, evt, options).ConfigureAwait(false); + } + private async Task SendRpcAsyncInternal(string cmd, object payload, Optional evt, RequestOptions options) + where TResponse : class + { + byte[] bytes = null; + var guid = Guid.NewGuid(); + payload = new API.Rpc.RpcFrame { Cmd = cmd, Event = evt, Args = payload, Nonce = guid }; + if (payload != null) + { + var json = SerializeJson(payload); + bytes = Encoding.UTF8.GetBytes(json); + } + + var requestTracker = new RpcRequest(options); + _requests[guid] = requestTracker; + + await RequestQueue.SendAsync(new WebSocketRequest(_webSocketClient, null, bytes, true, options)).ConfigureAwait(false); + await _sentRpcMessageEvent.InvokeAsync(cmd).ConfigureAwait(false); + return await requestTracker.Promise.Task.ConfigureAwait(false); + } + + //Rpc + public async Task SendAuthenticateAsync(RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + var msg = new AuthenticateParams + { + AccessToken = AuthToken + }; + options.IgnoreState = true; + return await SendRpcAsync("AUTHENTICATE", msg, options: options).ConfigureAwait(false); + } + public async Task SendAuthorizeAsync(IReadOnlyCollection scopes, string rpcToken = null, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + var msg = new AuthorizeParams + { + ClientId = _clientId, + Scopes = scopes, + RpcToken = rpcToken != null ? rpcToken : Optional.Create() + }; + if (options.Timeout == null) + options.Timeout = 60000; //This requires manual input on the user's end, lets give them more time + options.IgnoreState = true; + return await SendRpcAsync("AUTHORIZE", msg, options: options).ConfigureAwait(false); + } + + public async Task SendGetGuildsAsync(RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + return await SendRpcAsync("GET_GUILDS", null, options: options).ConfigureAwait(false); + } + public async Task SendGetGuildAsync(ulong guildId, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + var msg = new GetGuildParams + { + GuildId = guildId + }; + return await SendRpcAsync("GET_GUILD", msg, options: options).ConfigureAwait(false); + } + public async Task SendGetChannelsAsync(ulong guildId, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + var msg = new GetChannelsParams + { + GuildId = guildId + }; + return await SendRpcAsync("GET_CHANNELS", msg, options: options).ConfigureAwait(false); + } + public async Task SendGetChannelAsync(ulong channelId, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + var msg = new GetChannelParams + { + ChannelId = channelId + }; + return await SendRpcAsync("GET_CHANNEL", msg, options: options).ConfigureAwait(false); + } + + public async Task SendSelectTextChannelAsync(ulong channelId, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + var msg = new SelectChannelParams + { + ChannelId = channelId + }; + return await SendRpcAsync("SELECT_TEXT_CHANNEL", msg, options: options).ConfigureAwait(false); + } + public async Task SendSelectVoiceChannelAsync(ulong channelId, bool force = false, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + var msg = new SelectChannelParams + { + ChannelId = channelId, + Force = force + }; + return await SendRpcAsync("SELECT_VOICE_CHANNEL", msg, options: options).ConfigureAwait(false); + } + + public async Task SendGlobalSubscribeAsync(string evt, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + return await SendRpcAsync("SUBSCRIBE", null, evt: evt, options: options).ConfigureAwait(false); + } + public async Task SendGlobalUnsubscribeAsync(string evt, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + return await SendRpcAsync("UNSUBSCRIBE", null, evt: evt, options: options).ConfigureAwait(false); + } + + public async Task SendGuildSubscribeAsync(string evt, ulong guildId, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + var msg = new GuildSubscriptionParams + { + GuildId = guildId + }; + return await SendRpcAsync("SUBSCRIBE", msg, evt: evt, options: options).ConfigureAwait(false); + } + public async Task SendGuildUnsubscribeAsync(string evt, ulong guildId, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + var msg = new GuildSubscriptionParams + { + GuildId = guildId + }; + return await SendRpcAsync("UNSUBSCRIBE", msg, evt: evt, options: options).ConfigureAwait(false); + } + + public async Task SendChannelSubscribeAsync(string evt, ulong channelId, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + var msg = new ChannelSubscriptionParams + { + ChannelId = channelId + }; + return await SendRpcAsync("SUBSCRIBE", msg, evt: evt, options: options).ConfigureAwait(false); + } + public async Task SendChannelUnsubscribeAsync(string evt, ulong channelId, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + var msg = new ChannelSubscriptionParams + { + ChannelId = channelId + }; + return await SendRpcAsync("UNSUBSCRIBE", msg, evt: evt, options: options).ConfigureAwait(false); + } + + public async Task GetVoiceSettingsAsync(RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + return await SendRpcAsync("GET_VOICE_SETTINGS", null, options: options).ConfigureAwait(false); + } + public async Task SetVoiceSettingsAsync(API.Rpc.VoiceSettings settings, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + await SendRpcAsync("SET_VOICE_SETTINGS", settings, options: options).ConfigureAwait(false); + } + public async Task SetUserVoiceSettingsAsync(ulong userId, API.Rpc.UserVoiceSettings settings, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + settings.UserId = userId; + await SendRpcAsync("SET_USER_VOICE_SETTINGS", settings, options: options).ConfigureAwait(false); + } + + private bool ProcessMessage(API.Rpc.RpcFrame msg) + { + if (_requests.TryGetValue(msg.Nonce.Value.Value, out RpcRequest requestTracker)) + { + if (msg.Event.GetValueOrDefault("") == "ERROR") + { + var _ = requestTracker.SetExceptionAsync(msg.Data.GetValueOrDefault() as JToken, _serializer); + } + else + { + var _ = requestTracker.SetResultAsync(msg.Data.GetValueOrDefault() as JToken, _serializer); + } + return true; + } + else + return false; + } + } +} diff --git a/experiment/Discord.Net.Rpc/DiscordRpcClient.Events.cs b/experiment/Discord.Net.Rpc/DiscordRpcClient.Events.cs new file mode 100644 index 0000000..2a9ae21 --- /dev/null +++ b/experiment/Discord.Net.Rpc/DiscordRpcClient.Events.cs @@ -0,0 +1,112 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Rpc +{ + public partial class DiscordRpcClient + { + //General + public event Func Connected + { + add { _connectedEvent.Add(value); } + remove { _connectedEvent.Remove(value); } + } + private readonly AsyncEvent> _connectedEvent = new AsyncEvent>(); + public event Func Disconnected + { + add { _disconnectedEvent.Add(value); } + remove { _disconnectedEvent.Remove(value); } + } + private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); + public event Func Ready + { + add { _readyEvent.Add(value); } + remove { _readyEvent.Remove(value); } + } + private readonly AsyncEvent> _readyEvent = new AsyncEvent>(); + + //Channel + public event Func ChannelCreated + { + add { _channelCreatedEvent.Add(value); } + remove { _channelCreatedEvent.Remove(value); } + } + private readonly AsyncEvent> _channelCreatedEvent = new AsyncEvent>(); + + //Guild + public event Func GuildCreated + { + add { _guildCreatedEvent.Add(value); } + remove { _guildCreatedEvent.Remove(value); } + } + private readonly AsyncEvent> _guildCreatedEvent = new AsyncEvent>(); + public event Func GuildStatusUpdated + { + add { _guildStatusUpdatedEvent.Add(value); } + remove { _guildStatusUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _guildStatusUpdatedEvent = new AsyncEvent>(); + + //Voice + public event Func VoiceStateCreated + { + add { _voiceStateCreatedEvent.Add(value); } + remove { _voiceStateCreatedEvent.Remove(value); } + } + private readonly AsyncEvent> _voiceStateCreatedEvent = new AsyncEvent>(); + + public event Func VoiceStateUpdated + { + add { _voiceStateUpdatedEvent.Add(value); } + remove { _voiceStateUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _voiceStateUpdatedEvent = new AsyncEvent>(); + + public event Func VoiceStateDeleted + { + add { _voiceStateDeletedEvent.Add(value); } + remove { _voiceStateDeletedEvent.Remove(value); } + } + private readonly AsyncEvent> _voiceStateDeletedEvent = new AsyncEvent>(); + + public event Func SpeakingStarted + { + add { _speakingStartedEvent.Add(value); } + remove { _speakingStartedEvent.Remove(value); } + } + private readonly AsyncEvent> _speakingStartedEvent = new AsyncEvent>(); + public event Func SpeakingStopped + { + add { _speakingStoppedEvent.Add(value); } + remove { _speakingStoppedEvent.Remove(value); } + } + private readonly AsyncEvent> _speakingStoppedEvent = new AsyncEvent>(); + + public event Func VoiceSettingsUpdated + { + add { _voiceSettingsUpdated.Add(value); } + remove { _voiceSettingsUpdated.Remove(value); } + } + private readonly AsyncEvent> _voiceSettingsUpdated = new AsyncEvent>(); + + //Messages + public event Func MessageReceived + { + add { _messageReceivedEvent.Add(value); } + remove { _messageReceivedEvent.Remove(value); } + } + private readonly AsyncEvent> _messageReceivedEvent = new AsyncEvent>(); + public event Func MessageUpdated + { + add { _messageUpdatedEvent.Add(value); } + remove { _messageUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _messageUpdatedEvent = new AsyncEvent>(); + public event Func MessageDeleted + { + add { _messageDeletedEvent.Add(value); } + remove { _messageDeletedEvent.Remove(value); } + } + private readonly AsyncEvent> _messageDeletedEvent = new AsyncEvent>(); + } +} diff --git a/experiment/Discord.Net.Rpc/DiscordRpcClient.cs b/experiment/Discord.Net.Rpc/DiscordRpcClient.cs new file mode 100644 index 0000000..9c77fc9 --- /dev/null +++ b/experiment/Discord.Net.Rpc/DiscordRpcClient.cs @@ -0,0 +1,478 @@ +using Discord.API.Rpc; +using Discord.Logging; +using Discord.Net.Converters; +using Discord.Rest; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using System.Threading; + +namespace Discord.Rpc +{ + public partial class DiscordRpcClient : BaseDiscordClient, IDiscordClient + { + private readonly JsonSerializer _serializer; + private readonly ConnectionManager _connection; + private readonly Logger _rpcLogger; + private readonly SemaphoreSlim _stateLock, _authorizeLock; + + public ConnectionState ConnectionState { get; private set; } + public IReadOnlyCollection Scopes { get; private set; } + public DateTimeOffset TokenExpiresAt { get; private set; } + + internal new API.DiscordRpcApiClient ApiClient => base.ApiClient as API.DiscordRpcApiClient; + public new RestSelfUser CurrentUser { get { return base.CurrentUser as RestSelfUser; } private set { base.CurrentUser = value; } } + public RestApplication ApplicationInfo { get; private set; } + + /// Creates a new RPC discord client. + public DiscordRpcClient(string clientId, string origin) + : this(clientId, origin, new DiscordRpcConfig()) { } + /// Creates a new RPC discord client. + public DiscordRpcClient(string clientId, string origin, DiscordRpcConfig config) + : base(config, CreateApiClient(clientId, origin, config)) + { + _stateLock = new SemaphoreSlim(1, 1); + _authorizeLock = new SemaphoreSlim(1, 1); + _rpcLogger = LogManager.CreateLogger("RPC"); + _connection = new ConnectionManager(_stateLock, _rpcLogger, config.ConnectionTimeout, + OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x); + _connection.Connected += () => _connectedEvent.InvokeAsync(); + _connection.Disconnected += (ex, recon) => _disconnectedEvent.InvokeAsync(ex); + + _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; + _serializer.Error += (s, e) => + { + _rpcLogger.WarningAsync(e.ErrorContext.Error).GetAwaiter().GetResult(); + e.ErrorContext.Handled = true; + }; + + ApiClient.SentRpcMessage += async opCode => await _rpcLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false); + ApiClient.ReceivedRpcEvent += ProcessMessageAsync; + } + + private static API.DiscordRpcApiClient CreateApiClient(string clientId, string origin, DiscordRpcConfig config) + => new API.DiscordRpcApiClient(clientId, DiscordRestConfig.UserAgent, origin, config.RestClientProvider, config.WebSocketProvider); + internal override void Dispose(bool disposing) + { + if (disposing) + { + StopAsync().GetAwaiter().GetResult(); + ApiClient.Dispose(); + } + } + + public Task StartAsync() => _connection.StartAsync(); + public Task StopAsync() => _connection.StopAsync(); + + private async Task OnConnectingAsync() + { + await _rpcLogger.DebugAsync("Connecting ApiClient").ConfigureAwait(false); + await ApiClient.ConnectAsync().ConfigureAwait(false); + + await _connection.WaitAsync().ConfigureAwait(false); + } + private async Task OnDisconnectingAsync(Exception ex) + { + await _rpcLogger.DebugAsync("Disconnecting ApiClient").ConfigureAwait(false); + await ApiClient.DisconnectAsync().ConfigureAwait(false); + } + + public async Task AuthorizeAsync(string[] scopes, string rpcToken = null, RequestOptions options = null) + { + await _authorizeLock.WaitAsync().ConfigureAwait(false); + try + { + await _connection.StartAsync().ConfigureAwait(false); + await _connection.WaitAsync().ConfigureAwait(false); + var result = await ApiClient.SendAuthorizeAsync(scopes, rpcToken, options).ConfigureAwait(false); + await _connection.StopAsync().ConfigureAwait(false); + return result.Code; + } + finally + { + _authorizeLock.Release(); + } + } + + public async Task SubscribeGlobal(RpcGlobalEvent evnt, RequestOptions options = null) + { + await ApiClient.SendGlobalSubscribeAsync(GetEventName(evnt), options).ConfigureAwait(false); + } + public async Task UnsubscribeGlobal(RpcGlobalEvent evnt, RequestOptions options = null) + { + await ApiClient.SendGlobalUnsubscribeAsync(GetEventName(evnt), options).ConfigureAwait(false); + } + public async Task SubscribeGuild(ulong guildId, RpcChannelEvent evnt, RequestOptions options = null) + { + await ApiClient.SendGuildSubscribeAsync(GetEventName(evnt), guildId, options).ConfigureAwait(false); + } + public async Task UnsubscribeGuild(ulong guildId, RpcChannelEvent evnt, RequestOptions options = null) + { + await ApiClient.SendGuildUnsubscribeAsync(GetEventName(evnt), guildId, options).ConfigureAwait(false); + } + public async Task SubscribeChannel(ulong channelId, RpcChannelEvent evnt, RequestOptions options = null) + { + await ApiClient.SendChannelSubscribeAsync(GetEventName(evnt), channelId).ConfigureAwait(false); + } + public async Task UnsubscribeChannel(ulong channelId, RpcChannelEvent evnt, RequestOptions options = null) + { + await ApiClient.SendChannelUnsubscribeAsync(GetEventName(evnt), channelId).ConfigureAwait(false); + } + + public async Task GetRpcGuildAsync(ulong id, RequestOptions options = null) + { + var model = await ApiClient.SendGetGuildAsync(id, options).ConfigureAwait(false); + return RpcGuild.Create(this, model); + } + public async Task> GetRpcGuildsAsync(RequestOptions options = null) + { + var models = await ApiClient.SendGetGuildsAsync(options).ConfigureAwait(false); + return models.Guilds.Select(x => RpcGuildSummary.Create(x)).ToImmutableArray(); + } + public async Task GetRpcChannelAsync(ulong id, RequestOptions options = null) + { + var model = await ApiClient.SendGetChannelAsync(id, options).ConfigureAwait(false); + return RpcChannel.Create(this, model); + } + public async Task> GetRpcChannelsAsync(ulong guildId, RequestOptions options = null) + { + var models = await ApiClient.SendGetChannelsAsync(guildId, options).ConfigureAwait(false); + return models.Channels.Select(x => RpcChannelSummary.Create(x)).ToImmutableArray(); + } + + public async Task SelectTextChannelAsync(IChannel channel, RequestOptions options = null) + { + var model = await ApiClient.SendSelectTextChannelAsync(channel.Id, options).ConfigureAwait(false); + return RpcChannel.Create(this, model) as IMessageChannel; + } + public async Task SelectTextChannelAsync(RpcChannelSummary channel, RequestOptions options = null) + { + var model = await ApiClient.SendSelectTextChannelAsync(channel.Id, options).ConfigureAwait(false); + return RpcChannel.Create(this, model) as IMessageChannel; + } + public async Task SelectTextChannelAsync(ulong channelId, RequestOptions options = null) + { + var model = await ApiClient.SendSelectTextChannelAsync(channelId, options).ConfigureAwait(false); + return RpcChannel.Create(this, model) as IMessageChannel; + } + + public async Task SelectVoiceChannelAsync(IChannel channel, bool force = false, RequestOptions options = null) + { + var model = await ApiClient.SendSelectVoiceChannelAsync(channel.Id, force, options).ConfigureAwait(false); + return RpcChannel.Create(this, model) as IRpcAudioChannel; + } + public async Task SelectVoiceChannelAsync(RpcChannelSummary channel, bool force = false, RequestOptions options = null) + { + var model = await ApiClient.SendSelectVoiceChannelAsync(channel.Id, force, options).ConfigureAwait(false); + return RpcChannel.Create(this, model) as IRpcAudioChannel; + } + public async Task SelectVoiceChannelAsync(ulong channelId, bool force = false, RequestOptions options = null) + { + var model = await ApiClient.SendSelectVoiceChannelAsync(channelId, force, options).ConfigureAwait(false); + return RpcChannel.Create(this, model) as IRpcAudioChannel; + } + + public async Task GetVoiceSettingsAsync(RequestOptions options = null) + { + var model = await ApiClient.GetVoiceSettingsAsync(options).ConfigureAwait(false); + return VoiceSettings.Create(model); + } + public async Task SetVoiceSettingsAsync(Action func, RequestOptions options = null) + { + if (func == null) throw new NullReferenceException(nameof(func)); + + var settings = new VoiceProperties(); + settings.Input = new VoiceDeviceProperties(); + settings.Output = new VoiceDeviceProperties(); + settings.Mode = new VoiceModeProperties(); + func(settings); + + var model = new API.Rpc.VoiceSettings + { + AutomaticGainControl = settings.AutomaticGainControl, + EchoCancellation = settings.EchoCancellation, + NoiseSuppression = settings.NoiseSuppression, + QualityOfService = settings.QualityOfService, + SilenceWarning = settings.SilenceWarning + }; + model.Input = new API.Rpc.VoiceDeviceSettings + { + DeviceId = settings.Input.DeviceId, + Volume = settings.Input.Volume + }; + model.Output = new API.Rpc.VoiceDeviceSettings + { + DeviceId = settings.Output.DeviceId, + Volume = settings.Output.Volume + }; + model.Mode = new API.Rpc.VoiceMode + { + AutoThreshold = settings.Mode.AutoThreshold, + Delay = settings.Mode.Delay, + Threshold = settings.Mode.Threshold, + Type = settings.Mode.Type + }; + + if (settings.Input.AvailableDevices.IsSpecified) + model.Input.AvailableDevices = settings.Input.AvailableDevices.Value.Select(x => x.ToModel()).ToArray(); + if (settings.Output.AvailableDevices.IsSpecified) + model.Output.AvailableDevices = settings.Output.AvailableDevices.Value.Select(x => x.ToModel()).ToArray(); + if (settings.Mode.Shortcut.IsSpecified) + model.Mode.Shortcut = settings.Mode.Shortcut.Value.Select(x => x.ToModel()).ToArray(); + + await ApiClient.SetVoiceSettingsAsync(model, options).ConfigureAwait(false); + } + public async Task SetUserVoiceSettingsAsync(ulong userId, Action func, RequestOptions options = null) + { + if (func == null) throw new NullReferenceException(nameof(func)); + + var settings = new UserVoiceProperties(); + func(settings); + + var model = new API.Rpc.UserVoiceSettings + { + Mute = settings.Mute, + UserId = settings.UserId, + Volume = settings.Volume + }; + if (settings.Pan.IsSpecified) + model.Pan = settings.Pan.Value.ToModel(); + await ApiClient.SetUserVoiceSettingsAsync(userId, model, options).ConfigureAwait(false); + } + + private static string GetEventName(RpcGlobalEvent rpcEvent) + { + switch (rpcEvent) + { + case RpcGlobalEvent.ChannelCreated: return "CHANNEL_CREATE"; + case RpcGlobalEvent.GuildCreated: return "GUILD_CREATE"; + case RpcGlobalEvent.VoiceSettingsUpdated: return "VOICE_SETTINGS_UPDATE"; + default: + throw new InvalidOperationException($"Unknown RPC Global Event: {rpcEvent}"); + } + } + private static string GetEventName(RpcGuildEvent rpcEvent) + { + switch (rpcEvent) + { + case RpcGuildEvent.GuildStatus: return "GUILD_STATUS"; + default: + throw new InvalidOperationException($"Unknown RPC Guild Event: {rpcEvent}"); + } + } + private static string GetEventName(RpcChannelEvent rpcEvent) + { + switch (rpcEvent) + { + case RpcChannelEvent.MessageCreate: return "MESSAGE_CREATE"; + case RpcChannelEvent.MessageUpdate: return "MESSAGE_UPDATE"; + case RpcChannelEvent.MessageDelete: return "MESSAGE_DELETE"; + case RpcChannelEvent.SpeakingStart: return "SPEAKING_START"; + case RpcChannelEvent.SpeakingStop: return "SPEAKING_STOP"; + case RpcChannelEvent.VoiceStateCreate: return "VOICE_STATE_CREATE"; + case RpcChannelEvent.VoiceStateUpdate: return "VOICE_STATE_UPDATE"; + case RpcChannelEvent.VoiceStateDelete: return "VOICE_STATE_DELETE"; + default: + throw new InvalidOperationException($"Unknown RPC Channel Event: {rpcEvent}"); + } + } + + private async Task ProcessMessageAsync(string cmd, Optional evnt, Optional payload) + { + try + { + switch (cmd) + { + case "DISPATCH": + switch (evnt.Value) + { + //Connection + case "READY": + { + await _rpcLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false); + var data = (payload.Value as JToken).ToObject(_serializer); + + RequestOptions options = new RequestOptions + { + //CancellationToken = _cancelToken //TODO: Implement + }; + + if (ApiClient.LoginState == LoginState.LoggedIn) + { + var _ = Task.Run(async () => + { + try + { + var response = await ApiClient.SendAuthenticateAsync(options).ConfigureAwait(false); + CurrentUser = RestSelfUser.Create(this, response.User); + ApiClient.CurrentUserId = CurrentUser.Id; + ApplicationInfo = RestApplication.Create(this, response.Application); + Scopes = response.Scopes; + TokenExpiresAt = response.Expires; + + var __ = _connection.CompleteAsync(); + await _rpcLogger.InfoAsync("Ready").ConfigureAwait(false); + } + catch (Exception ex) + { + await _rpcLogger.ErrorAsync($"Error handling {cmd}{(evnt.IsSpecified ? $" ({evnt})" : "")}", ex).ConfigureAwait(false); + return; + } + }); + } + else + { + var _ = _connection.CompleteAsync(); + await _rpcLogger.InfoAsync("Ready").ConfigureAwait(false); + } + } + break; + + //Channels + case "CHANNEL_CREATE": + { + await _rpcLogger.DebugAsync("Received Dispatch (CHANNEL_CREATE)").ConfigureAwait(false); + var data = (payload.Value as JToken).ToObject(_serializer); + var channel = RpcChannelSummary.Create(data); + + await _channelCreatedEvent.InvokeAsync(channel).ConfigureAwait(false); + } + break; + + //Guilds + case "GUILD_CREATE": + { + await _rpcLogger.DebugAsync("Received Dispatch (GUILD_CREATE)").ConfigureAwait(false); + var data = (payload.Value as JToken).ToObject(_serializer); + var guild = RpcGuildSummary.Create(data); + + await _guildCreatedEvent.InvokeAsync(guild).ConfigureAwait(false); + } + break; + case "GUILD_STATUS": + { + await _rpcLogger.DebugAsync("Received Dispatch (GUILD_STATUS)").ConfigureAwait(false); + var data = (payload.Value as JToken).ToObject(_serializer); + var guildStatus = RpcGuildStatus.Create(data); + + await _guildStatusUpdatedEvent.InvokeAsync(guildStatus).ConfigureAwait(false); + } + break; + + //Voice + case "VOICE_STATE_CREATE": + { + await _rpcLogger.DebugAsync("Received Dispatch (VOICE_STATE_CREATE)").ConfigureAwait(false); + var data = (payload.Value as JToken).ToObject(_serializer); + var voiceState = RpcVoiceState.Create(this, data); + + await _voiceStateCreatedEvent.InvokeAsync(voiceState).ConfigureAwait(false); + } + break; + case "VOICE_STATE_UPDATE": + { + await _rpcLogger.DebugAsync("Received Dispatch (VOICE_STATE_UPDATE)").ConfigureAwait(false); + var data = (payload.Value as JToken).ToObject(_serializer); + var voiceState = RpcVoiceState.Create(this, data); + + await _voiceStateUpdatedEvent.InvokeAsync(voiceState).ConfigureAwait(false); + } + break; + case "VOICE_STATE_DELETE": + { + await _rpcLogger.DebugAsync("Received Dispatch (VOICE_STATE_DELETE)").ConfigureAwait(false); + var data = (payload.Value as JToken).ToObject(_serializer); + var voiceState = RpcVoiceState.Create(this, data); + + await _voiceStateDeletedEvent.InvokeAsync(voiceState).ConfigureAwait(false); + } + break; + + case "SPEAKING_START": + { + await _rpcLogger.DebugAsync("Received Dispatch (SPEAKING_START)").ConfigureAwait(false); + var data = (payload.Value as JToken).ToObject(_serializer); + + await _speakingStartedEvent.InvokeAsync(data.UserId).ConfigureAwait(false); + } + break; + case "SPEAKING_STOP": + { + await _rpcLogger.DebugAsync("Received Dispatch (SPEAKING_STOP)").ConfigureAwait(false); + var data = (payload.Value as JToken).ToObject(_serializer); + + await _speakingStoppedEvent.InvokeAsync(data.UserId).ConfigureAwait(false); + } + break; + case "VOICE_SETTINGS_UPDATE": + { + await _rpcLogger.DebugAsync("Received Dispatch (VOICE_SETTINGS_UPDATE)").ConfigureAwait(false); + var data = (payload.Value as JToken).ToObject(_serializer); + var settings = VoiceSettings.Create(data); + + await _voiceSettingsUpdated.InvokeAsync(settings).ConfigureAwait(false); + } + break; + + //Messages + case "MESSAGE_CREATE": + { + await _rpcLogger.DebugAsync("Received Dispatch (MESSAGE_CREATE)").ConfigureAwait(false); + var data = (payload.Value as JToken).ToObject(_serializer); + var msg = RpcMessage.Create(this, data.ChannelId, data.Message); + + await _messageReceivedEvent.InvokeAsync(msg).ConfigureAwait(false); + } + break; + case "MESSAGE_UPDATE": + { + await _rpcLogger.DebugAsync("Received Dispatch (MESSAGE_UPDATE)").ConfigureAwait(false); + var data = (payload.Value as JToken).ToObject(_serializer); + var msg = RpcMessage.Create(this, data.ChannelId, data.Message); + + await _messageUpdatedEvent.InvokeAsync(msg).ConfigureAwait(false); + } + break; + case "MESSAGE_DELETE": + { + await _rpcLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE)").ConfigureAwait(false); + var data = (payload.Value as JToken).ToObject(_serializer); + + await _messageDeletedEvent.InvokeAsync(data.ChannelId, data.Message.Id).ConfigureAwait(false); + } + break; + + //Others + default: + await _rpcLogger.WarningAsync($"Unknown Dispatch ({evnt})").ConfigureAwait(false); + return; + } + break; + + /*default: //Other opcodes are used for command responses + await _rpcLogger.WarningAsync($"Unknown OpCode ({cmd})").ConfigureAwait(false); + return;*/ + } + } + catch (Exception ex) + { + await _rpcLogger.ErrorAsync($"Error handling {cmd}{(evnt.IsSpecified ? $" ({evnt})" : "")}", ex).ConfigureAwait(false); + return; + } + } + + //IDiscordClient + ConnectionState IDiscordClient.ConnectionState => _connection.State; + + Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) => Task.FromResult(ApplicationInfo); + + async Task IDiscordClient.StartAsync() + => await StartAsync().ConfigureAwait(false); + async Task IDiscordClient.StopAsync() + => await StopAsync().ConfigureAwait(false); + } +} diff --git a/experiment/Discord.Net.Rpc/DiscordRpcConfig.cs b/experiment/Discord.Net.Rpc/DiscordRpcConfig.cs new file mode 100644 index 0000000..90df8d1 --- /dev/null +++ b/experiment/Discord.Net.Rpc/DiscordRpcConfig.cs @@ -0,0 +1,33 @@ +using Discord.Net.WebSockets; +using Discord.Rest; +using System; + +namespace Discord.Rpc +{ + public class DiscordRpcConfig : DiscordRestConfig + { + public const int RpcAPIVersion = 1; + + public const int PortRangeStart = 6463; + public const int PortRangeEnd = 6472; + + /// Gets or sets the time, in milliseconds, to wait for a connection to complete before aborting. + public int ConnectionTimeout { get; set; } = 30000; + + /// Gets or sets the provider used to generate new WebSocket connections. + public WebSocketProvider WebSocketProvider { get; set; } + + public DiscordRpcConfig() + { +#if FILESYSTEM + WebSocketProvider = () => new DefaultWebSocketClient(); +#else + WebSocketProvider = () => + { + throw new InvalidOperationException("The default WebSocket provider is not supported on this platform.\n" + + "You must specify a WebSocketProvider or target a runtime supporting .NET Standard 1.3, such as .NET Framework 4.6+."); + }; +#endif + } + } +} diff --git a/experiment/Discord.Net.Rpc/Entities/Channels/IRpcAudioChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/IRpcAudioChannel.cs new file mode 100644 index 0000000..4fa0110 --- /dev/null +++ b/experiment/Discord.Net.Rpc/Entities/Channels/IRpcAudioChannel.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Discord.Rpc +{ + public interface IRpcAudioChannel : IAudioChannel + { + IReadOnlyCollection VoiceStates { get; } + } +} diff --git a/experiment/Discord.Net.Rpc/Entities/Channels/IRpcMessageChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/IRpcMessageChannel.cs new file mode 100644 index 0000000..8e69c1b --- /dev/null +++ b/experiment/Discord.Net.Rpc/Entities/Channels/IRpcMessageChannel.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Discord.Rpc +{ + public interface IRpcMessageChannel : IMessageChannel + { + IReadOnlyCollection CachedMessages { get; } + } +} diff --git a/experiment/Discord.Net.Rpc/Entities/Channels/IRpcPrivateChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/IRpcPrivateChannel.cs new file mode 100644 index 0000000..ae43c86 --- /dev/null +++ b/experiment/Discord.Net.Rpc/Entities/Channels/IRpcPrivateChannel.cs @@ -0,0 +1,6 @@ +namespace Discord.Rpc +{ + public interface IRpcPrivateChannel + { + } +} diff --git a/experiment/Discord.Net.Rpc/Entities/Channels/RpcCategoryChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcCategoryChannel.cs new file mode 100644 index 0000000..cac766f --- /dev/null +++ b/experiment/Discord.Net.Rpc/Entities/Channels/RpcCategoryChannel.cs @@ -0,0 +1,36 @@ +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Rpc.Channel; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RpcCategoryChannel : RpcGuildChannel, ICategoryChannel + { + public IReadOnlyCollection CachedMessages { get; private set; } + + public string Mention => MentionUtils.MentionChannel(Id); + + internal RpcCategoryChannel(DiscordRpcClient discord, ulong id, ulong guildId) + : base(discord, id, guildId) + { + } + internal new static RpcCategoryChannel Create(DiscordRpcClient discord, Model model) + { + var entity = new RpcCategoryChannel(discord, model.Id, model.GuildId.Value); + entity.Update(model); + return entity; + } + internal override void Update(Model model) + { + base.Update(model); + CachedMessages = model.Messages.Select(x => RpcMessage.Create(Discord, Id, x)).ToImmutableArray(); + } + } +} diff --git a/experiment/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs new file mode 100644 index 0000000..0c22a03 --- /dev/null +++ b/experiment/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs @@ -0,0 +1,43 @@ +using Discord.Rest; +using System; + +using Model = Discord.API.Rpc.Channel; + +namespace Discord.Rpc +{ + public class RpcChannel : RpcEntity + { + public string Name { get; private set; } + + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + + internal RpcChannel(DiscordRpcClient discord, ulong id) + : base(discord, id) + { + } + internal static RpcChannel Create(DiscordRpcClient discord, Model model) + { + if (model.GuildId.IsSpecified) + return RpcGuildChannel.Create(discord, model); + else + return CreatePrivate(discord, model); + } + internal static RpcChannel CreatePrivate(DiscordRpcClient discord, Model model) + { + switch (model.Type) + { + case ChannelType.DM: + return RpcDMChannel.Create(discord, model); + case ChannelType.Group: + return RpcGroupChannel.Create(discord, model); + default: + throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); + } + } + internal virtual void Update(Model model) + { + if (model.Name.IsSpecified) + Name = model.Name.Value; + } + } +} diff --git a/experiment/Discord.Net.Rpc/Entities/Channels/RpcChannelSummary.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcChannelSummary.cs new file mode 100644 index 0000000..c35437e --- /dev/null +++ b/experiment/Discord.Net.Rpc/Entities/Channels/RpcChannelSummary.cs @@ -0,0 +1,32 @@ +using System.Diagnostics; +using Model = Discord.API.Rpc.ChannelSummary; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RpcChannelSummary + { + public ulong Id { get; } + public string Name { get; private set; } + public ChannelType Type { get; private set; } + + internal RpcChannelSummary(ulong id) + { + Id = id; + } + internal static RpcChannelSummary Create(Model model) + { + var entity = new RpcChannelSummary(model.Id); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { + Name = model.Name; + Type = model.Type; + } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id}, {Type})"; + } +} diff --git a/experiment/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs new file mode 100644 index 0000000..1954116 --- /dev/null +++ b/experiment/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs @@ -0,0 +1,126 @@ +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Rpc.Channel; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RpcDMChannel : RpcChannel, IRpcMessageChannel, IRpcPrivateChannel, IDMChannel + { + public IReadOnlyCollection CachedMessages { get; private set; } + + internal RpcDMChannel(DiscordRpcClient discord, ulong id) + : base(discord, id) + { + } + internal static new RpcDMChannel Create(DiscordRpcClient discord, Model model) + { + var entity = new RpcDMChannel(discord, model.Id); + entity.Update(model); + return entity; + } + internal override void Update(Model model) + { + base.Update(model); + CachedMessages = model.Messages.Select(x => RpcMessage.Create(Discord, Id, x)).ToImmutableArray(); + } + + public Task CloseAsync(RequestOptions options = null) + => ChannelHelper.DeleteAsync(this, Discord, options); + + //TODO: Use RPC cache + public Task GetMessageAsync(ulong id, RequestOptions options = null) + => ChannelHelper.GetMessageAsync(this, Discord, id, options); + public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, options); + public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, options); + public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, options); + public Task> GetPinnedMessagesAsync(RequestOptions options = null) + => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); + + public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); +#if FILESYSTEM + public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); +#endif + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); + + public Task TriggerTypingAsync(RequestOptions options = null) + => ChannelHelper.TriggerTypingAsync(this, Discord, options); + public IDisposable EnterTypingState(RequestOptions options = null) + => ChannelHelper.EnterTypingState(this, Discord, options); + + public override string ToString() => Id.ToString(); + private string DebuggerDisplay => $"({Id}, DM)"; + + //IDMChannel + IUser IDMChannel.Recipient { get { throw new NotSupportedException(); } } + + //IPrivateChannel + IReadOnlyCollection IPrivateChannel.Recipients { get { throw new NotSupportedException(); } } + + //IMessageChannel + async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetMessageAsync(id, options).ConfigureAwait(false); + else + return null; + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(limit, options); + else + return AsyncEnumerable.Empty>(); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(fromMessageId, dir, limit, options); + else + return AsyncEnumerable.Empty>(); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(fromMessage, dir, limit, options); + else + return AsyncEnumerable.Empty>(); + } + async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) + => await GetPinnedMessagesAsync(options).ConfigureAwait(false); + +#if FILESYSTEM + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) + => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); +#endif + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) + => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) + => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + IDisposable IMessageChannel.EnterTypingState(RequestOptions options) + => EnterTypingState(options); + + //IChannel + string IChannel.Name { get { throw new NotSupportedException(); } } + + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + throw new NotSupportedException(); + } + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + { + throw new NotSupportedException(); + } + } +} diff --git a/experiment/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs new file mode 100644 index 0000000..9d484c2 --- /dev/null +++ b/experiment/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs @@ -0,0 +1,129 @@ +using Discord.Audio; +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Rpc.Channel; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RpcGroupChannel : RpcChannel, IRpcMessageChannel, IRpcAudioChannel, IRpcPrivateChannel, IGroupChannel + { + public IReadOnlyCollection CachedMessages { get; private set; } + public IReadOnlyCollection VoiceStates { get; private set; } + + internal RpcGroupChannel(DiscordRpcClient discord, ulong id) + : base(discord, id) + { + } + internal new static RpcGroupChannel Create(DiscordRpcClient discord, Model model) + { + var entity = new RpcGroupChannel(discord, model.Id); + entity.Update(model); + return entity; + } + internal override void Update(Model model) + { + base.Update(model); + CachedMessages = model.Messages.Select(x => RpcMessage.Create(Discord, Id, x)).ToImmutableArray(); + VoiceStates = model.VoiceStates.Select(x => RpcVoiceState.Create(Discord, x)).ToImmutableArray(); + } + + public Task LeaveAsync(RequestOptions options = null) + => ChannelHelper.DeleteAsync(this, Discord, options); + + //TODO: Use RPC cache + public Task GetMessageAsync(ulong id, RequestOptions options = null) + => ChannelHelper.GetMessageAsync(this, Discord, id, options); + public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, options); + public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, options); + public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, options); + public Task> GetPinnedMessagesAsync(RequestOptions options = null) + => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); + + public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); +#if FILESYSTEM + public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); +#endif + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); + + public Task TriggerTypingAsync(RequestOptions options = null) + => ChannelHelper.TriggerTypingAsync(this, Discord, options); + public IDisposable EnterTypingState(RequestOptions options = null) + => ChannelHelper.EnterTypingState(this, Discord, options); + + public override string ToString() => Id.ToString(); + private string DebuggerDisplay => $"({Id}, Group)"; + + //IPrivateChannel + IReadOnlyCollection IPrivateChannel.Recipients { get { throw new NotSupportedException(); } } + + //IMessageChannel + async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetMessageAsync(id, options).ConfigureAwait(false); + else + return null; + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(limit, options); + else + return AsyncEnumerable.Empty>(); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(fromMessageId, dir, limit, options); + else + return AsyncEnumerable.Empty>(); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(fromMessage, dir, limit, options); + else + return AsyncEnumerable.Empty>(); + } + async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) + => await GetPinnedMessagesAsync(options).ConfigureAwait(false); + +#if FILESYSTEM + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) + => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); +#endif + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) + => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) + => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + IDisposable IMessageChannel.EnterTypingState(RequestOptions options) + => EnterTypingState(options); + + //IAudioChannel + Task IAudioChannel.ConnectAsync(Action configAction) { throw new NotSupportedException(); } + + //IChannel + string IChannel.Name { get { throw new NotSupportedException(); } } + + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + throw new NotSupportedException(); + } + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + { + throw new NotSupportedException(); + } + } +} diff --git a/experiment/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs new file mode 100644 index 0000000..576a048 --- /dev/null +++ b/experiment/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Model = Discord.API.Rpc.Channel; +using Discord.Rest; + +namespace Discord.Rpc +{ + public class RpcGuildChannel : RpcChannel, IGuildChannel + { + public ulong GuildId { get; } + public int Position { get; private set; } + public ulong? CategoryId { get; private set; } + + internal RpcGuildChannel(DiscordRpcClient discord, ulong id, ulong guildId) + : base(discord, id) + { + GuildId = guildId; + } + internal new static RpcGuildChannel Create(DiscordRpcClient discord, Model model) + { + switch (model.Type) + { + case ChannelType.Text: + return RpcTextChannel.Create(discord, model); + case ChannelType.Voice: + return RpcVoiceChannel.Create(discord, model); + default: + throw new InvalidOperationException("Unknown guild channel type"); + } + } + internal override void Update(Model model) + { + base.Update(model); + if (model.Position.IsSpecified) + Position = model.Position.Value; + } + + public Task ModifyAsync(Action func, RequestOptions options = null) + => ChannelHelper.ModifyAsync(this, Discord, func, options); + public Task DeleteAsync(RequestOptions options = null) + => ChannelHelper.DeleteAsync(this, Discord, options); + + public Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions perms, RequestOptions options = null) + => ChannelHelper.AddPermissionOverwriteAsync(this, Discord, user, perms, options); + public Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions perms, RequestOptions options = null) + => ChannelHelper.AddPermissionOverwriteAsync(this, Discord, role, perms, options); + public Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null) + => ChannelHelper.RemovePermissionOverwriteAsync(this, Discord, user, options); + public Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) + => ChannelHelper.RemovePermissionOverwriteAsync(this, Discord, role, options); + + public async Task> GetInvitesAsync(RequestOptions options = null) + => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); + public async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); + + public override string ToString() => Name; + + //IGuildChannel + public Task GetCategoryAsync() + { + //Always fails + throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); + } + + IGuild IGuildChannel.Guild + { + get + { + //Always fails + throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); + } + } + + async Task> IGuildChannel.GetInvitesAsync(RequestOptions options) + => await GetInvitesAsync(options).ConfigureAwait(false); + async Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) + => await CreateInviteAsync(maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); + + IReadOnlyCollection IGuildChannel.PermissionOverwrites { get { throw new NotSupportedException(); } } + OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IUser user) + { + throw new NotSupportedException(); + } + OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IRole role) + { + throw new NotSupportedException(); + } + IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + { + throw new NotSupportedException(); + } + Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + throw new NotSupportedException(); + } + + //IChannel + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + { + throw new NotSupportedException(); + } + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + throw new NotSupportedException(); + } + } +} diff --git a/experiment/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs new file mode 100644 index 0000000..8c49f06 --- /dev/null +++ b/experiment/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs @@ -0,0 +1,134 @@ +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Rpc.Channel; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RpcTextChannel : RpcGuildChannel, IRpcMessageChannel, ITextChannel + { + public IReadOnlyCollection CachedMessages { get; private set; } + + public string Mention => MentionUtils.MentionChannel(Id); + // TODO: Check if RPC includes the 'nsfw' field on Channel models + public bool IsNsfw => ChannelHelper.IsNsfw(this); + + internal RpcTextChannel(DiscordRpcClient discord, ulong id, ulong guildId) + : base(discord, id, guildId) + { + } + internal new static RpcVoiceChannel Create(DiscordRpcClient discord, Model model) + { + var entity = new RpcVoiceChannel(discord, model.Id, model.GuildId.Value); + entity.Update(model); + return entity; + } + internal override void Update(Model model) + { + base.Update(model); + CachedMessages = model.Messages.Select(x => RpcMessage.Create(Discord, Id, x)).ToImmutableArray(); + } + + public Task ModifyAsync(Action func, RequestOptions options = null) + => ChannelHelper.ModifyAsync(this, Discord, func, options); + + //TODO: Use RPC cache + public Task GetMessageAsync(ulong id, RequestOptions options = null) + => ChannelHelper.GetMessageAsync(this, Discord, id, options); + public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, options); + public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, options); + public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, options); + public Task> GetPinnedMessagesAsync(RequestOptions options = null) + => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); + + public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); +#if FILESYSTEM + public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); +#endif + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); + + public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); + + public Task TriggerTypingAsync(RequestOptions options = null) + => ChannelHelper.TriggerTypingAsync(this, Discord, options); + public IDisposable EnterTypingState(RequestOptions options = null) + => ChannelHelper.EnterTypingState(this, Discord, options); + + //Webhooks + public Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) + => ChannelHelper.CreateWebhookAsync(this, Discord, name, avatar, options); + public Task GetWebhookAsync(ulong id, RequestOptions options = null) + => ChannelHelper.GetWebhookAsync(this, Discord, id, options); + public Task> GetWebhooksAsync(RequestOptions options = null) + => ChannelHelper.GetWebhooksAsync(this, Discord, options); + + private string DebuggerDisplay => $"{Name} ({Id}, Text)"; + + //ITextChannel + string ITextChannel.Topic { get { throw new NotSupportedException(); } } + async Task ITextChannel.CreateWebhookAsync(string name, Stream avatar, RequestOptions options) + => await CreateWebhookAsync(name, avatar, options); + async Task ITextChannel.GetWebhookAsync(ulong id, RequestOptions options) + => await GetWebhookAsync(id, options); + async Task> ITextChannel.GetWebhooksAsync(RequestOptions options) + => await GetWebhooksAsync(options); + + //IMessageChannel + async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetMessageAsync(id, options).ConfigureAwait(false); + else + return null; + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(limit, options); + else + return AsyncEnumerable.Empty>(); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(fromMessageId, dir, limit, options); + else + return AsyncEnumerable.Empty>(); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(fromMessage, dir, limit, options); + else + return AsyncEnumerable.Empty>(); + } + async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) + => await GetPinnedMessagesAsync(options).ConfigureAwait(false); + +#if FILESYSTEM + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) + => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); +#endif + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) + => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) + => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + IDisposable IMessageChannel.EnterTypingState(RequestOptions options) + => EnterTypingState(options); + } +} diff --git a/experiment/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs new file mode 100644 index 0000000..067da67 --- /dev/null +++ b/experiment/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs @@ -0,0 +1,48 @@ +using Discord.Audio; +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Rpc.Channel; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RpcVoiceChannel : RpcGuildChannel, IRpcAudioChannel, IVoiceChannel + { + public int Bitrate { get; private set; } + public int? UserLimit { get; private set; } + public IReadOnlyCollection VoiceStates { get; private set; } + + internal RpcVoiceChannel(DiscordRpcClient discord, ulong id, ulong guildId) + : base(discord, id, guildId) + { + } + internal new static RpcVoiceChannel Create(DiscordRpcClient discord, Model model) + { + var entity = new RpcVoiceChannel(discord, model.Id, model.GuildId.Value); + entity.Update(model); + return entity; + } + internal override void Update(Model model) + { + base.Update(model); + if (model.UserLimit.IsSpecified) + UserLimit = model.UserLimit.Value != 0 ? model.UserLimit.Value : (int?)null; + if (model.Bitrate.IsSpecified) + Bitrate = model.Bitrate.Value; + VoiceStates = model.VoiceStates.Select(x => RpcVoiceState.Create(Discord, x)).ToImmutableArray(); + } + + public Task ModifyAsync(Action func, RequestOptions options = null) + => ChannelHelper.ModifyAsync(this, Discord, func, options); + + private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; + + //IAudioChannel + Task IAudioChannel.ConnectAsync(Action configAction) { throw new NotSupportedException(); } + } +} diff --git a/experiment/Discord.Net.Rpc/Entities/Guilds/RpcGuild.cs b/experiment/Discord.Net.Rpc/Entities/Guilds/RpcGuild.cs new file mode 100644 index 0000000..7352d9e --- /dev/null +++ b/experiment/Discord.Net.Rpc/Entities/Guilds/RpcGuild.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using Model = Discord.API.Rpc.Guild; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RpcGuild : RpcEntity + { + public string Name { get; private set; } + public string IconUrl { get; private set; } + public IReadOnlyCollection Users { get; private set; } + + internal RpcGuild(DiscordRpcClient discord, ulong id) + : base(discord, id) + { + } + internal static RpcGuild Create(DiscordRpcClient discord, Model model) + { + var entity = new RpcGuild(discord, model.Id); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { + Name = model.Name; + IconUrl = model.IconUrl; + Users = model.Members.Select(x => RpcGuildUser.Create(Discord, x)).ToImmutableArray(); + } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id})"; + } +} diff --git a/experiment/Discord.Net.Rpc/Entities/Guilds/RpcGuildStatus.cs b/experiment/Discord.Net.Rpc/Entities/Guilds/RpcGuildStatus.cs new file mode 100644 index 0000000..f443d7a --- /dev/null +++ b/experiment/Discord.Net.Rpc/Entities/Guilds/RpcGuildStatus.cs @@ -0,0 +1,30 @@ +using System.Diagnostics; +using Model = Discord.API.Rpc.GuildStatusEvent; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RpcGuildStatus + { + public RpcGuildSummary Guild { get; } + public int Online { get; private set; } + + internal RpcGuildStatus(ulong guildId) + { + Guild = new RpcGuildSummary(guildId); + } + internal static RpcGuildStatus Create(Model model) + { + var entity = new RpcGuildStatus(model.Guild.Id); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { + Online = model.Online; + } + + public override string ToString() => Guild.Name; + private string DebuggerDisplay => $"{Guild.Name} ({Guild.Id}, {Online} Online)"; + } +} diff --git a/experiment/Discord.Net.Rpc/Entities/Guilds/RpcGuildSummary.cs b/experiment/Discord.Net.Rpc/Entities/Guilds/RpcGuildSummary.cs new file mode 100644 index 0000000..4f9bff2 --- /dev/null +++ b/experiment/Discord.Net.Rpc/Entities/Guilds/RpcGuildSummary.cs @@ -0,0 +1,30 @@ +using System.Diagnostics; +using Model = Discord.API.Rpc.GuildSummary; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RpcGuildSummary + { + public ulong Id { get; } + public string Name { get; private set; } + + internal RpcGuildSummary(ulong id) + { + Id = id; + } + internal static RpcGuildSummary Create(Model model) + { + var entity = new RpcGuildSummary(model.Id); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { + Name = model.Name; + } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id})"; + } +} diff --git a/experiment/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs b/experiment/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs new file mode 100644 index 0000000..a2f7b45 --- /dev/null +++ b/experiment/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs @@ -0,0 +1,75 @@ +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading.Tasks; +using Model = Discord.API.Rpc.Message; + +namespace Discord.Rpc +{ + public abstract class RpcMessage : RpcEntity, IMessage + { + private long _timestampTicks; + + public IMessageChannel Channel { get; } + public RpcUser Author { get; } + public MessageSource Source { get; } + + public string Content { get; private set; } + public Color AuthorColor { get; private set; } + + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + public virtual bool IsTTS => false; + public virtual bool IsPinned => false; + public virtual bool IsBlocked => false; + public virtual DateTimeOffset? EditedTimestamp => null; + public virtual IReadOnlyCollection Attachments => ImmutableArray.Create(); + public virtual IReadOnlyCollection Embeds => ImmutableArray.Create(); + public virtual IReadOnlyCollection MentionedChannelIds => ImmutableArray.Create(); + public virtual IReadOnlyCollection MentionedRoleIds => ImmutableArray.Create(); + public virtual IReadOnlyCollection MentionedUserIds => ImmutableArray.Create(); + public virtual IReadOnlyCollection Tags => ImmutableArray.Create(); + public virtual ulong? WebhookId => null; + public bool IsWebhook => WebhookId != null; + + public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); + + internal RpcMessage(DiscordRpcClient discord, ulong id, RestVirtualMessageChannel channel, RpcUser author, MessageSource source) + : base(discord, id) + { + Channel = channel; + Author = author; + Source = source; + } + internal static RpcMessage Create(DiscordRpcClient discord, ulong channelId, Model model) + { + //model.ChannelId is always 0, needs to be passed from the event + if (model.Type == MessageType.Default) + return RpcUserMessage.Create(discord, channelId, model); + else + return RpcSystemMessage.Create(discord, channelId, model); + } + internal virtual void Update(Model model) + { + if (model.Timestamp.IsSpecified) + _timestampTicks = model.Timestamp.Value.UtcTicks; + + if (model.Content.IsSpecified) + Content = model.Content.Value; + if (model.AuthorColor.IsSpecified) + AuthorColor = new Color(Convert.ToUInt32(model.AuthorColor.Value.Substring(1), 16)); + } + + public Task DeleteAsync(RequestOptions options = null) + => MessageHelper.DeleteAsync(this, Discord, options); + + public override string ToString() => Content; + + //IMessage + IMessageChannel IMessage.Channel => Channel; + MessageType IMessage.Type => MessageType.Default; + IUser IMessage.Author => Author; + IReadOnlyCollection IMessage.Attachments => Attachments; + IReadOnlyCollection IMessage.Embeds => Embeds; + } +} diff --git a/experiment/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs b/experiment/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs new file mode 100644 index 0000000..39c6026 --- /dev/null +++ b/experiment/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs @@ -0,0 +1,33 @@ +using Discord.Rest; +using System.Diagnostics; +using Model = Discord.API.Rpc.Message; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RpcSystemMessage : RpcMessage, ISystemMessage + { + public MessageType Type { get; private set; } + + internal RpcSystemMessage(DiscordRpcClient discord, ulong id, RestVirtualMessageChannel channel, RpcUser author) + : base(discord, id, channel, author, MessageSource.System) + { + } + internal new static RpcSystemMessage Create(DiscordRpcClient discord, ulong channelId, Model model) + { + var entity = new RpcSystemMessage(discord, model.Id, + RestVirtualMessageChannel.Create(discord, channelId), + RpcUser.Create(discord, model.Author.Value, model.WebhookId.ToNullable())); + entity.Update(model); + return entity; + } + internal override void Update(Model model) + { + base.Update(model); + + Type = model.Type; + } + + private string DebuggerDisplay => $"{Author}: {Content} ({Id}, {Type})"; + } +} diff --git a/experiment/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs b/experiment/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs new file mode 100644 index 0000000..bc17516 --- /dev/null +++ b/experiment/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs @@ -0,0 +1,128 @@ +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.Rpc.Message; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RpcUserMessage : RpcMessage, IUserMessage + { + private bool _isMentioningEveryone, _isTTS, _isPinned, _isBlocked; + private long? _editedTimestampTicks; + private ulong? _webhookId; + private ImmutableArray _attachments; + private ImmutableArray _embeds; + private ImmutableArray _tags; + + public override bool IsTTS => _isTTS; + public override bool IsPinned => _isPinned; + public override bool IsBlocked => _isBlocked; + public override ulong? WebhookId => _webhookId; + public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks); + public override IReadOnlyCollection Attachments => _attachments; + public override IReadOnlyCollection Embeds => _embeds; + public override IReadOnlyCollection MentionedChannelIds => MessageHelper.FilterTagsByKey(TagType.ChannelMention, _tags); + public override IReadOnlyCollection MentionedRoleIds => MessageHelper.FilterTagsByKey(TagType.RoleMention, _tags); + public override IReadOnlyCollection MentionedUserIds => MessageHelper.FilterTagsByKey(TagType.UserMention, _tags); + public override IReadOnlyCollection Tags => _tags; + public IReadOnlyDictionary Reactions => ImmutableDictionary.Create(); + + internal RpcUserMessage(DiscordRpcClient discord, ulong id, RestVirtualMessageChannel channel, RpcUser author, MessageSource source) + : base(discord, id, channel, author, source) + { + } + internal new static RpcUserMessage Create(DiscordRpcClient discord, ulong channelId, Model model) + { + var entity = new RpcUserMessage(discord, model.Id, + RestVirtualMessageChannel.Create(discord, channelId), + RpcUser.Create(discord, model.Author.Value, model.WebhookId.ToNullable()), + MessageHelper.GetSource(model)); + entity.Update(model); + return entity; + } + + internal override void Update(Model model) + { + base.Update(model); + + if (model.IsTextToSpeech.IsSpecified) + _isTTS = model.IsTextToSpeech.Value; + if (model.Pinned.IsSpecified) + _isPinned = model.Pinned.Value; + if (model.EditedTimestamp.IsSpecified) + _editedTimestampTicks = model.EditedTimestamp.Value?.UtcTicks; + if (model.MentionEveryone.IsSpecified) + _isMentioningEveryone = model.MentionEveryone.Value; + if (model.WebhookId.IsSpecified) + _webhookId = model.WebhookId.Value; + + if (model.IsBlocked.IsSpecified) + _isBlocked = model.IsBlocked.Value; + + if (model.Attachments.IsSpecified) + { + var value = model.Attachments.Value; + if (value.Length > 0) + { + var attachments = ImmutableArray.CreateBuilder(value.Length); + for (int i = 0; i < value.Length; i++) + attachments.Add(Attachment.Create(value[i])); + _attachments = attachments.ToImmutable(); + } + else + _attachments = ImmutableArray.Create(); + } + + if (model.Embeds.IsSpecified) + { + var value = model.Embeds.Value; + if (value.Length > 0) + { + var embeds = ImmutableArray.CreateBuilder(value.Length); + for (int i = 0; i < value.Length; i++) + embeds.Add(value[i].ToEntity()); + _embeds = embeds.ToImmutable(); + } + else + _embeds = ImmutableArray.Create(); + } + + if (model.Content.IsSpecified) + { + var text = model.Content.Value; + _tags = MessageHelper.ParseTags(text, null, null, ImmutableArray.Create()); + model.Content = text; + } + } + + public Task ModifyAsync(Action func, RequestOptions options) + => MessageHelper.ModifyAsync(this, Discord, func, options); + + public Task AddReactionAsync(IEmote emote, RequestOptions options = null) + => MessageHelper.AddReactionAsync(this, emote, Discord, options); + public Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null) + => MessageHelper.RemoveReactionAsync(this, user, emote, Discord, options); + public Task RemoveAllReactionsAsync(RequestOptions options = null) + => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); + public Task> GetReactionUsersAsync(IEmote emote, int limit = 100, ulong? afterUserId = null, RequestOptions options = null) + => MessageHelper.GetReactionUsersAsync(this, emote, x => { x.Limit = limit; x.AfterUserId = afterUserId ?? Optional.Create(); }, Discord, options); + + public Task PinAsync(RequestOptions options) + => MessageHelper.PinAsync(this, Discord, options); + public Task UnpinAsync(RequestOptions options) + => MessageHelper.UnpinAsync(this, Discord, options); + + public string Resolve(int startIndex, TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, + TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) + => MentionUtils.Resolve(this, startIndex, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); + public string Resolve(TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, + TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) + => MentionUtils.Resolve(this, 0, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); + + private string DebuggerDisplay => $"{Author}: {Content} ({Id}{(Attachments.Count > 0 ? $", {Attachments.Count} Attachments" : "")})"; + } +} diff --git a/experiment/Discord.Net.Rpc/Entities/RpcEntity.cs b/experiment/Discord.Net.Rpc/Entities/RpcEntity.cs new file mode 100644 index 0000000..3827175 --- /dev/null +++ b/experiment/Discord.Net.Rpc/Entities/RpcEntity.cs @@ -0,0 +1,17 @@ +using System; + +namespace Discord.Rpc +{ + public abstract class RpcEntity : IEntity + where T : IEquatable + { + internal DiscordRpcClient Discord { get; } + public T Id { get; } + + internal RpcEntity(DiscordRpcClient discord, T id) + { + Discord = discord; + Id = id; + } + } +} diff --git a/experiment/Discord.Net.Rpc/Entities/UserVoiceProperties.cs b/experiment/Discord.Net.Rpc/Entities/UserVoiceProperties.cs new file mode 100644 index 0000000..830ba16 --- /dev/null +++ b/experiment/Discord.Net.Rpc/Entities/UserVoiceProperties.cs @@ -0,0 +1,18 @@ +#pragma warning disable CS1591 + +using Newtonsoft.Json; + +namespace Discord.Rpc +{ + public class UserVoiceProperties + { + [JsonProperty("userId")] + internal ulong UserId { get; set; } + [JsonProperty("pan")] + public Optional Pan { get; set; } + [JsonProperty("volume")] + public Optional Volume { get; set; } + [JsonProperty("mute")] + public Optional Mute { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/Entities/Users/Pan.cs b/experiment/Discord.Net.Rpc/Entities/Users/Pan.cs new file mode 100644 index 0000000..2db6cdb --- /dev/null +++ b/experiment/Discord.Net.Rpc/Entities/Users/Pan.cs @@ -0,0 +1,25 @@ +using System.Diagnostics; +using Model = Discord.API.Rpc.Pan; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public struct Pan + { + public float Left { get; } + public float Right { get; } + + public Pan(float left, float right) + { + Left = left; + Right = right; + } + internal static Pan Create(Model model) + { + return new Pan(model.Left, model.Right); + } + + public override string ToString() => $"Left = {Left}, Right = {Right}"; + private string DebuggerDisplay => $"Left = {Left}, Right = {Right}"; + } +} diff --git a/experiment/Discord.Net.Rpc/Entities/Users/RpcGuildUser.cs b/experiment/Discord.Net.Rpc/Entities/Users/RpcGuildUser.cs new file mode 100644 index 0000000..f4ca637 --- /dev/null +++ b/experiment/Discord.Net.Rpc/Entities/Users/RpcGuildUser.cs @@ -0,0 +1,29 @@ +using Model = Discord.API.Rpc.GuildMember; + +namespace Discord.Rpc +{ + public class RpcGuildUser : RpcUser + { + private UserStatus _status; + + public override UserStatus Status => _status; + //public object Acitivity { get; private set; } + + internal RpcGuildUser(DiscordRpcClient discord, ulong id) + : base(discord, id) + { + } + internal static RpcGuildUser Create(DiscordRpcClient discord, Model model) + { + var entity = new RpcGuildUser(discord, model.User.Id); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { + base.Update(model.User); + _status = model.Status; + //Activity = model.Activity; + } + } +} diff --git a/experiment/Discord.Net.Rpc/Entities/Users/RpcUser.cs b/experiment/Discord.Net.Rpc/Entities/Users/RpcUser.cs new file mode 100644 index 0000000..f55c83b --- /dev/null +++ b/experiment/Discord.Net.Rpc/Entities/Users/RpcUser.cs @@ -0,0 +1,65 @@ +using Discord.Rest; +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.User; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RpcUser : RpcEntity, IUser + { + public bool IsBot { get; private set; } + public string Username { get; private set; } + public ushort DiscriminatorValue { get; private set; } + public string AvatarId { get; private set; } + + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + public string Discriminator => DiscriminatorValue.ToString("D4"); + public string Mention => MentionUtils.MentionUser(Id); + public virtual bool IsWebhook => false; + public virtual IActivity Activity => null; + public virtual UserStatus Status => UserStatus.Offline; + + internal RpcUser(DiscordRpcClient discord, ulong id) + : base(discord, id) + { + } + internal static RpcUser Create(DiscordRpcClient discord, Model model) + => Create(discord, model, null); + internal static RpcUser Create(DiscordRpcClient discord, Model model, ulong? webhookId) + { + RpcUser entity; + if (webhookId.HasValue) + entity = new RpcWebhookUser(discord, model.Id, webhookId.Value); + else + entity = new RpcUser(discord, model.Id); + entity.Update(model); + return entity; + } + internal virtual void Update(Model model) + { + if (model.Avatar.IsSpecified) + AvatarId = model.Avatar.Value; + if (model.Discriminator.IsSpecified) + DiscriminatorValue = ushort.Parse(model.Discriminator.Value); + if (model.Bot.IsSpecified) + IsBot = model.Bot.Value; + if (model.Username.IsSpecified) + Username = model.Username.Value; + } + + public Task GetOrCreateDMChannelAsync(RequestOptions options = null) + => UserHelper.CreateDMChannelAsync(this, Discord, options); + + public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + + public override string ToString() => $"{Username}#{Discriminator}"; + private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; + + //IUser + async Task IUser.GetOrCreateDMChannelAsync(RequestOptions options) + => await GetOrCreateDMChannelAsync(options); + } +} diff --git a/experiment/Discord.Net.Rpc/Entities/Users/RpcVoiceState.cs b/experiment/Discord.Net.Rpc/Entities/Users/RpcVoiceState.cs new file mode 100644 index 0000000..66b4186 --- /dev/null +++ b/experiment/Discord.Net.Rpc/Entities/Users/RpcVoiceState.cs @@ -0,0 +1,79 @@ +using System; +using System.Diagnostics; +using Model = Discord.API.Rpc.ExtendedVoiceState; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RpcVoiceState : IVoiceState + { + [Flags] + private enum Flags : byte + { + Normal = 0x00, + Suppressed = 0x01, + Muted = 0x02, + Deafened = 0x04, + SelfMuted = 0x08, + SelfDeafened = 0x10, + } + + private Flags _voiceStates; + + public RpcUser User { get; } + public string Nickname { get; private set; } + public int Volume { get; private set; } + public bool IsMuted2 { get; private set; } + public Pan Pan { get; private set; } + + public bool IsMuted => (_voiceStates & Flags.Muted) != 0; + public bool IsDeafened => (_voiceStates & Flags.Deafened) != 0; + public bool IsSuppressed => (_voiceStates & Flags.Suppressed) != 0; + public bool IsSelfMuted => (_voiceStates & Flags.SelfMuted) != 0; + public bool IsSelfDeafened => (_voiceStates & Flags.SelfDeafened) != 0; + + internal RpcVoiceState(DiscordRpcClient discord, ulong userId) + { + User = new RpcUser(discord, userId); + } + internal static RpcVoiceState Create(DiscordRpcClient discord, Model model) + { + var entity = new RpcVoiceState(discord, model.User.Id); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { + if (model.VoiceState.IsSpecified) + { + Flags voiceStates = Flags.Normal; + if (model.VoiceState.Value.Mute) + voiceStates |= Flags.Muted; + if (model.VoiceState.Value.Deaf) + voiceStates |= Flags.Deafened; + if (model.VoiceState.Value.SelfMute) + voiceStates |= Flags.SelfMuted; + if (model.VoiceState.Value.SelfDeaf) + voiceStates |= Flags.SelfDeafened; + if (model.VoiceState.Value.Suppress) + voiceStates |= Flags.Suppressed; + _voiceStates = voiceStates; + } + User.Update(model.User); + if (model.Nickname.IsSpecified) + Nickname = model.Nickname.Value; + if (model.Volume.IsSpecified) + Volume = model.Volume.Value; + if (model.Mute.IsSpecified) + IsMuted2 = model.Mute.Value; + if (model.Pan.IsSpecified) + Pan = Pan.Create(model.Pan.Value); + } + + public override string ToString() => User.ToString(); + private string DebuggerDisplay => $"{User} ({_voiceStates})"; + + string IVoiceState.VoiceSessionId { get { throw new NotSupportedException(); } } + IVoiceChannel IVoiceState.VoiceChannel { get { throw new NotSupportedException(); } } + } +} diff --git a/experiment/Discord.Net.Rpc/Entities/Users/RpcWebhookUser.cs b/experiment/Discord.Net.Rpc/Entities/Users/RpcWebhookUser.cs new file mode 100644 index 0000000..9ea4312 --- /dev/null +++ b/experiment/Discord.Net.Rpc/Entities/Users/RpcWebhookUser.cs @@ -0,0 +1,25 @@ +using System.Diagnostics; +using Model = Discord.API.User; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RpcWebhookUser : RpcUser + { + public ulong WebhookId { get; } + + public override bool IsWebhook => true; + + internal RpcWebhookUser(DiscordRpcClient discord, ulong id, ulong webhookId) + : base(discord, id) + { + WebhookId = webhookId; + } + internal static RpcWebhookUser Create(DiscordRpcClient discord, Model model, ulong webhookId) + { + var entity = new RpcWebhookUser(discord, model.Id, webhookId); + entity.Update(model); + return entity; + } + } +} diff --git a/experiment/Discord.Net.Rpc/Entities/VoiceDevice.cs b/experiment/Discord.Net.Rpc/Entities/VoiceDevice.cs new file mode 100644 index 0000000..18f929f --- /dev/null +++ b/experiment/Discord.Net.Rpc/Entities/VoiceDevice.cs @@ -0,0 +1,25 @@ +using System.Diagnostics; +using Model = Discord.API.Rpc.VoiceDevice; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public struct VoiceDevice + { + public string Id { get; } + public string Name { get; } + + internal VoiceDevice(string id, string name) + { + Id = id; + Name = name; + } + internal static VoiceDevice Create(Model model) + { + return new VoiceDevice(model.Id, model.Name); + } + + public override string ToString() => $"{Name}"; + private string DebuggerDisplay => $"{Name} ({Id})"; + } +} diff --git a/experiment/Discord.Net.Rpc/Entities/VoiceDeviceProperties.cs b/experiment/Discord.Net.Rpc/Entities/VoiceDeviceProperties.cs new file mode 100644 index 0000000..bdf87b2 --- /dev/null +++ b/experiment/Discord.Net.Rpc/Entities/VoiceDeviceProperties.cs @@ -0,0 +1,9 @@ +namespace Discord.Rpc +{ + public class VoiceDeviceProperties + { + public Optional DeviceId { get; set; } + public Optional Volume { get; set; } + public Optional AvailableDevices { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/Entities/VoiceModeProperties.cs b/experiment/Discord.Net.Rpc/Entities/VoiceModeProperties.cs new file mode 100644 index 0000000..da791e7 --- /dev/null +++ b/experiment/Discord.Net.Rpc/Entities/VoiceModeProperties.cs @@ -0,0 +1,11 @@ +namespace Discord.Rpc +{ + public class VoiceModeProperties + { + public Optional Type { get; set; } + public Optional AutoThreshold { get; set; } + public Optional Threshold { get; set; } + public Optional Shortcut { get; set; } + public Optional Delay { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/Entities/VoiceProperties.cs b/experiment/Discord.Net.Rpc/Entities/VoiceProperties.cs new file mode 100644 index 0000000..5939f83 --- /dev/null +++ b/experiment/Discord.Net.Rpc/Entities/VoiceProperties.cs @@ -0,0 +1,14 @@ +namespace Discord.Rpc +{ + public class VoiceProperties + { + public VoiceDeviceProperties Input { get; set; } + public VoiceDeviceProperties Output { get; set; } + public VoiceModeProperties Mode { get; set; } + public Optional AutomaticGainControl { get; set; } + public Optional EchoCancellation { get; set; } + public Optional NoiseSuppression { get; set; } + public Optional QualityOfService { get; set; } + public Optional SilenceWarning { get; set; } + } +} diff --git a/experiment/Discord.Net.Rpc/Entities/VoiceSettings.cs b/experiment/Discord.Net.Rpc/Entities/VoiceSettings.cs new file mode 100644 index 0000000..2e8d6e7 --- /dev/null +++ b/experiment/Discord.Net.Rpc/Entities/VoiceSettings.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Model = Discord.API.Rpc.VoiceSettings; + +namespace Discord.Rpc +{ + public class VoiceSettings + { + public string InputDeviceId { get; private set; } + public float InputVolume { get; private set; } + public IReadOnlyCollection AvailableInputDevices { get; private set; } + + public string OutputDeviceId { get; private set; } + public float OutputVolume { get; private set; } + public IReadOnlyCollection AvailableOutputDevices { get; private set; } + + public bool AutomaticGainControl { get; private set; } + public bool EchoCancellation { get; private set; } + public bool NoiseSuppression { get; private set; } + public bool QualityOfService { get; private set; } + public bool SilenceWarning { get; private set; } + + public string ActivationMode { get; private set; } + public bool AutoThreshold { get; private set; } + public float Threshold { get; private set; } + public IReadOnlyCollection Shortcuts { get; private set; } + public float Delay { get; private set; } + + internal VoiceSettings() { } + internal static VoiceSettings Create(Model model) + { + var entity = new VoiceSettings(); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { + if (model.AutomaticGainControl.IsSpecified) + AutomaticGainControl = model.AutomaticGainControl.Value; + if (model.EchoCancellation.IsSpecified) + EchoCancellation = model.EchoCancellation.Value; + if (model.NoiseSuppression.IsSpecified) + NoiseSuppression = model.NoiseSuppression.Value; + if (model.QualityOfService.IsSpecified) + QualityOfService = model.QualityOfService.Value; + if (model.SilenceWarning.IsSpecified) + SilenceWarning = model.SilenceWarning.Value; + + if (model.Input.DeviceId.IsSpecified) + InputDeviceId = model.Input.DeviceId.Value; + if (model.Input.Volume.IsSpecified) + InputVolume = model.Input.Volume.Value; + if (model.Input.AvailableDevices.IsSpecified) + AvailableInputDevices = model.Input.AvailableDevices.Value.Select(x => VoiceDevice.Create(x)).ToImmutableArray(); + + if (model.Output.DeviceId.IsSpecified) + OutputDeviceId = model.Output.DeviceId.Value; + if (model.Output.Volume.IsSpecified) + OutputVolume = model.Output.Volume.Value; + if (model.Output.AvailableDevices.IsSpecified) + AvailableInputDevices = model.Output.AvailableDevices.Value.Select(x => VoiceDevice.Create(x)).ToImmutableArray(); + + if (model.Mode.Type.IsSpecified) + ActivationMode = model.Mode.Type.Value; + if (model.Mode.AutoThreshold.IsSpecified) + AutoThreshold = model.Mode.AutoThreshold.Value; + if (model.Mode.Threshold.IsSpecified) + Threshold = model.Mode.Threshold.Value; + if (model.Mode.Shortcut.IsSpecified) + Shortcuts = model.Mode.Shortcut.Value.Select(x => VoiceShortcut.Create(x)).ToImmutableArray(); + if (model.Mode.Delay.IsSpecified) + Delay = model.Mode.Delay.Value; + } + } +} diff --git a/experiment/Discord.Net.Rpc/Entities/VoiceShortcut.cs b/experiment/Discord.Net.Rpc/Entities/VoiceShortcut.cs new file mode 100644 index 0000000..ea9be99 --- /dev/null +++ b/experiment/Discord.Net.Rpc/Entities/VoiceShortcut.cs @@ -0,0 +1,27 @@ +using System.Diagnostics; +using Model = Discord.API.Rpc.VoiceShortcut; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public struct VoiceShortcut + { + public VoiceShortcutType Type { get; } + public int Code { get; } + public string Name { get; } + + internal VoiceShortcut(VoiceShortcutType type, int code, string name) + { + Type = type; + Code = code; + Name = name; + } + internal static VoiceShortcut Create(Model model) + { + return new VoiceShortcut(model.Type.Value, model.Code.Value, model.Name.Value); + } + + public override string ToString() => $"{Name}"; + private string DebuggerDisplay => $"{Name} ({Code}, {Type})"; + } +} diff --git a/experiment/Discord.Net.Rpc/Entities/VoiceShortcutType.cs b/experiment/Discord.Net.Rpc/Entities/VoiceShortcutType.cs new file mode 100644 index 0000000..f3b8280 --- /dev/null +++ b/experiment/Discord.Net.Rpc/Entities/VoiceShortcutType.cs @@ -0,0 +1,10 @@ +namespace Discord.Rpc +{ + public enum VoiceShortcutType + { + KeyboardKey = 0, + MouseButton = 1, + KeyboardModifierKey = 2, + GamepadButton = 3 + } +} diff --git a/experiment/Discord.Net.Rpc/Extensions/EntityExtensions.cs b/experiment/Discord.Net.Rpc/Extensions/EntityExtensions.cs new file mode 100644 index 0000000..00ccf5c --- /dev/null +++ b/experiment/Discord.Net.Rpc/Extensions/EntityExtensions.cs @@ -0,0 +1,31 @@ +namespace Discord.Rpc +{ + internal static class EntityExtensions + { + public static API.Rpc.Pan ToModel(this Pan entity) + { + return new API.Rpc.Pan + { + Left = entity.Left, + Right = entity.Right + }; + } + public static API.Rpc.VoiceDevice ToModel(this VoiceDevice entity) + { + return new API.Rpc.VoiceDevice + { + Id = entity.Id, + Name = entity.Name + }; + } + public static API.Rpc.VoiceShortcut ToModel(this VoiceShortcut entity) + { + return new API.Rpc.VoiceShortcut + { + Code = entity.Code, + Name = entity.Name, + Type = entity.Type + }; + } + } +} diff --git a/experiment/Discord.Net.Rpc/RpcChannelEvent.cs b/experiment/Discord.Net.Rpc/RpcChannelEvent.cs new file mode 100644 index 0000000..2916dc8 --- /dev/null +++ b/experiment/Discord.Net.Rpc/RpcChannelEvent.cs @@ -0,0 +1,14 @@ +namespace Discord.Rpc +{ + public enum RpcChannelEvent + { + VoiceStateCreate, + VoiceStateUpdate, + VoiceStateDelete, + SpeakingStart, + SpeakingStop, + MessageCreate, + MessageUpdate, + MessageDelete + } +} diff --git a/experiment/Discord.Net.Rpc/RpcGlobalEvent.cs b/experiment/Discord.Net.Rpc/RpcGlobalEvent.cs new file mode 100644 index 0000000..673eaed --- /dev/null +++ b/experiment/Discord.Net.Rpc/RpcGlobalEvent.cs @@ -0,0 +1,9 @@ +namespace Discord.Rpc +{ + public enum RpcGlobalEvent + { + ChannelCreated, + GuildCreated, + VoiceSettingsUpdated + } +} diff --git a/experiment/Discord.Net.Rpc/RpcGuildEvent.cs b/experiment/Discord.Net.Rpc/RpcGuildEvent.cs new file mode 100644 index 0000000..34e5dc3 --- /dev/null +++ b/experiment/Discord.Net.Rpc/RpcGuildEvent.cs @@ -0,0 +1,7 @@ +namespace Discord.Rpc +{ + public enum RpcGuildEvent + { + GuildStatus + } +}