├── .gitignore ├── DotnetRPC.Sample ├── App.config ├── AsyncExecutor.cs ├── DotnetRPC.Sample.csproj ├── Program.cs ├── Properties │ └── AssemblyInfo.cs ├── debugpayload.txt └── packages.config ├── DotnetRPC.sln ├── DotnetRPC ├── ApiClient.cs ├── AsyncEvent.cs ├── DotnetRPC.csproj ├── Entities │ ├── ActivitySetModel.cs │ ├── RpcCommand.cs │ ├── RpcFrame.cs │ ├── RpcHandshake.cs │ ├── RpcPresence.cs │ ├── RpcReady.cs │ └── RpcUser.cs ├── Net │ ├── Commands.cs │ └── Events.cs ├── PipeClient.cs ├── RpcClient.cs └── Util │ ├── Logger.cs │ └── RpcHelpers.cs ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | # NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | # .NET Core 46 | project.lock.json 47 | project.fragment.lock.json 48 | artifacts/ 49 | **/Properties/launchSettings.json 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # Visual Studio code coverage results 117 | *.coverage 118 | *.coveragexml 119 | 120 | # NCrunch 121 | _NCrunch_* 122 | .*crunch*.local.xml 123 | nCrunchTemp_* 124 | 125 | # MightyMoose 126 | *.mm.* 127 | AutoTest.Net/ 128 | 129 | # Web workbench (sass) 130 | .sass-cache/ 131 | 132 | # Installshield output folder 133 | [Ee]xpress/ 134 | 135 | # DocProject is a documentation generator add-in 136 | DocProject/buildhelp/ 137 | DocProject/Help/*.HxT 138 | DocProject/Help/*.HxC 139 | DocProject/Help/*.hhc 140 | DocProject/Help/*.hhk 141 | DocProject/Help/*.hhp 142 | DocProject/Help/Html2 143 | DocProject/Help/html 144 | 145 | # Click-Once directory 146 | publish/ 147 | 148 | # Publish Web Output 149 | *.[Pp]ublish.xml 150 | *.azurePubxml 151 | # TODO: Comment the next line if you want to checkin your web deploy settings 152 | # but database connection strings (with potential passwords) will be unencrypted 153 | *.pubxml 154 | *.publishproj 155 | 156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 157 | # checkin your Azure Web App publish settings, but sensitive information contained 158 | # in these scripts will be unencrypted 159 | PublishScripts/ 160 | 161 | # NuGet Packages 162 | *.nupkg 163 | # The packages folder can be ignored because of Package Restore 164 | **/packages/* 165 | # except build/, which is used as an MSBuild target. 166 | !**/packages/build/ 167 | # Uncomment if necessary however generally it will be regenerated when needed 168 | #!**/packages/repositories.config 169 | # NuGet v3's project.json files produces more ignorable files 170 | *.nuget.props 171 | *.nuget.targets 172 | 173 | # Microsoft Azure Build Output 174 | csx/ 175 | *.build.csdef 176 | 177 | # Microsoft Azure Emulator 178 | ecf/ 179 | rcf/ 180 | 181 | # Windows Store app package directories and files 182 | AppPackages/ 183 | BundleArtifacts/ 184 | Package.StoreAssociation.xml 185 | _pkginfo.txt 186 | 187 | # Visual Studio cache files 188 | # files ending in .cache can be ignored 189 | *.[Cc]ache 190 | # but keep track of directories ending in .cache 191 | !*.[Cc]ache/ 192 | 193 | # Others 194 | ClientBin/ 195 | ~$* 196 | *~ 197 | *.dbmdl 198 | *.dbproj.schemaview 199 | *.jfm 200 | *.pfx 201 | *.publishsettings 202 | orleans.codegen.cs 203 | 204 | # Since there are multiple workflows, uncomment next line to ignore bower_components 205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 206 | #bower_components/ 207 | 208 | # RIA/Silverlight projects 209 | Generated_Code/ 210 | 211 | # Backup & report files from converting an old project file 212 | # to a newer Visual Studio version. Backup files are not needed, 213 | # because we have git ;-) 214 | _UpgradeReport_Files/ 215 | Backup*/ 216 | UpgradeLog*.XML 217 | UpgradeLog*.htm 218 | 219 | # SQL Server files 220 | *.mdf 221 | *.ldf 222 | *.ndf 223 | 224 | # Business Intelligence projects 225 | *.rdl.data 226 | *.bim.layout 227 | *.bim_*.settings 228 | 229 | # Microsoft Fakes 230 | FakesAssemblies/ 231 | 232 | # GhostDoc plugin setting file 233 | *.GhostDoc.xml 234 | 235 | # Node.js Tools for Visual Studio 236 | .ntvs_analysis.dat 237 | node_modules/ 238 | 239 | # Typescript v1 declaration files 240 | typings/ 241 | 242 | # Visual Studio 6 build log 243 | *.plg 244 | 245 | # Visual Studio 6 workspace options file 246 | *.opt 247 | 248 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 249 | *.vbw 250 | 251 | # Visual Studio LightSwitch build output 252 | **/*.HTMLClient/GeneratedArtifacts 253 | **/*.DesktopClient/GeneratedArtifacts 254 | **/*.DesktopClient/ModelManifest.xml 255 | **/*.Server/GeneratedArtifacts 256 | **/*.Server/ModelManifest.xml 257 | _Pvt_Extensions 258 | 259 | # Paket dependency manager 260 | .paket/paket.exe 261 | paket-files/ 262 | 263 | # FAKE - F# Make 264 | .fake/ 265 | 266 | # JetBrains Rider 267 | .idea/ 268 | *.sln.iml 269 | 270 | # CodeRush 271 | .cr/ 272 | 273 | # Python Tools for Visual Studio (PTVS) 274 | __pycache__/ 275 | *.pyc 276 | 277 | # Cake - Uncomment if you are using it 278 | # tools/** 279 | # !tools/packages.config 280 | 281 | # Telerik's JustMock configuration file 282 | *.jmconfig 283 | 284 | # BizTalk build output 285 | *.btp.cs 286 | *.btm.cs 287 | *.odx.cs 288 | *.xsd.cs 289 | -------------------------------------------------------------------------------- /DotnetRPC.Sample/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /DotnetRPC.Sample/AsyncExecutor.cs: -------------------------------------------------------------------------------- 1 | // This file is part of Emzi0767.CompanionCube project 2 | // 3 | // Copyright 2017 Emzi0767 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | using System; 18 | using System.Collections.Generic; 19 | using System.Linq; 20 | using System.Text; 21 | using System.Threading; 22 | using System.Threading.Tasks; 23 | 24 | namespace DotnetRPC.Sample 25 | { 26 | public class AsyncExecutor 27 | { 28 | private SemaphoreSlim Semaphore { get; } 29 | 30 | public AsyncExecutor() 31 | { 32 | this.Semaphore = new SemaphoreSlim(1, 1); 33 | } 34 | 35 | public void Execute(Task task) 36 | { 37 | this.Semaphore.Wait(); 38 | 39 | Exception taskex = null; 40 | 41 | var are = new AutoResetEvent(false); 42 | _ = Task.Run(Executor); 43 | are.WaitOne(); 44 | 45 | this.Semaphore.Release(); 46 | 47 | if (taskex != null) 48 | throw taskex; 49 | 50 | async Task Executor() 51 | { 52 | try 53 | { 54 | await task; 55 | } 56 | catch (Exception ex) 57 | { 58 | taskex = ex; 59 | } 60 | 61 | are.Set(); 62 | } 63 | } 64 | 65 | public T Execute(Task task) 66 | { 67 | this.Semaphore.Wait(); 68 | 69 | Exception taskex = null; 70 | T result = default; 71 | 72 | var are = new AutoResetEvent(false); 73 | _ = Task.Run(Executor); 74 | are.WaitOne(); 75 | 76 | this.Semaphore.Release(); 77 | 78 | if (taskex != null) 79 | throw taskex; 80 | 81 | return result; 82 | 83 | async Task Executor() 84 | { 85 | try 86 | { 87 | result = await task; 88 | } 89 | catch (Exception ex) 90 | { 91 | taskex = ex; 92 | } 93 | 94 | are.Set(); 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /DotnetRPC.Sample/DotnetRPC.Sample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {F098DABE-D1ED-4B04-B91D-B74585B48DEF} 8 | Exe 9 | DotnetRPC.Sample 10 | DotnetRPC.Sample 11 | v4.7 12 | 512 13 | true 14 | 15 | 16 | AnyCPU 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | latest 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | latest 35 | 36 | 37 | 38 | ..\packages\Microsoft.Win32.Registry.4.5.0-preview1-26216-02\lib\net461\Microsoft.Win32.Registry.dll 39 | 40 | 41 | ..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | {c3ba6deb-bcb8-45aa-96a1-efafe1a33e59} 64 | DotnetRPC 65 | 66 | 67 | 68 | 69 | Always 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /DotnetRPC.Sample/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Security.Principal; 4 | using System.Threading.Tasks; 5 | using DotnetRPC.Entities; 6 | 7 | namespace DotnetRPC.Sample 8 | { 9 | internal static class Program 10 | { 11 | private static void Main(string[] args) 12 | { 13 | var admin = false; 14 | using (var identity = WindowsIdentity.GetCurrent()) 15 | { 16 | var principal = new WindowsPrincipal(identity); 17 | admin = principal.IsInRole(WindowsBuiltInRole.Administrator); 18 | } 19 | 20 | var asx = new AsyncExecutor(); 21 | asx.Execute(StartAsync(admin)); 22 | } 23 | 24 | public static async Task StartAsync(bool admin) 25 | { 26 | var client = new RpcClient("345229890980937739", admin, Assembly.GetExecutingAssembly().Location); 27 | client.ConnectionClosed += _ => 28 | { 29 | Console.WriteLine("Disconnected!"); 30 | return Task.CompletedTask; 31 | }; 32 | client.ClientErrored += args => 33 | { 34 | Console.WriteLine($"Client error: {args.Exception}"); 35 | return Task.CompletedTask; 36 | }; 37 | client.Ready += async args => 38 | { 39 | // Only start doing stuff on ready c: 40 | await client.SetActivityAsync(x => 41 | { 42 | x.Details = "DotnetRPC.Sample"; 43 | x.State = "Part of DSharpPlus"; 44 | x.StartUnix = DateTimeOffset.Now; 45 | x.EndUnix = DateTimeOffset.Now.AddHours(24); 46 | 47 | x.LargeImage = "canary-large"; 48 | x.LargeImageText = "Canary icon"; 49 | 50 | x.SmallImage = "ptb-small"; 51 | x.SmallImageText = "PTB icon"; 52 | 53 | x.CurrentPartySize = 1; 54 | x.MaxPartySize = 10; 55 | x.PartyId = "meme"; 56 | }); 57 | }; 58 | 59 | await client.ConnectAsync(); 60 | 61 | await Task.Delay(15000); 62 | client.Dispose(); 63 | Console.WriteLine("It's gone!"); 64 | Console.ReadKey(); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /DotnetRPC.Sample/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("DotnetRPC.Sample")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("")] 11 | [assembly: AssemblyProduct("DotnetRPC.Sample")] 12 | [assembly: AssemblyCopyright("Copyright © 2018")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("f098dabe-d1ed-4b04-b91d-b74585b48def")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] 36 | -------------------------------------------------------------------------------- /DotnetRPC.Sample/debugpayload.txt: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /DotnetRPC.Sample/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /DotnetRPC.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27428.2015 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotnetRPC", "DotnetRPC\DotnetRPC.csproj", "{C3BA6DEB-BCB8-45AA-96A1-EFAFE1A33E59}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotnetRPC.Sample", "DotnetRPC.Sample\DotnetRPC.Sample.csproj", "{F098DABE-D1ED-4B04-B91D-B74585B48DEF}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {C3BA6DEB-BCB8-45AA-96A1-EFAFE1A33E59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {C3BA6DEB-BCB8-45AA-96A1-EFAFE1A33E59}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {C3BA6DEB-BCB8-45AA-96A1-EFAFE1A33E59}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {C3BA6DEB-BCB8-45AA-96A1-EFAFE1A33E59}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {F098DABE-D1ED-4B04-B91D-B74585B48DEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {F098DABE-D1ED-4B04-B91D-B74585B48DEF}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {F098DABE-D1ED-4B04-B91D-B74585B48DEF}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {F098DABE-D1ED-4B04-B91D-B74585B48DEF}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {DF17DEDB-301A-42C6-BA30-AA1D20F9B189} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /DotnetRPC/ApiClient.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using DotnetRPC.Entities; 4 | using DotnetRPC.Net; 5 | using Newtonsoft.Json; 6 | using Newtonsoft.Json.Linq; 7 | 8 | namespace DotnetRPC 9 | { 10 | internal class ApiClient 11 | { 12 | private readonly PipeClient _pipe; 13 | private readonly Logger _logger; 14 | 15 | internal ApiClient(PipeClient pipe, Logger logger) 16 | { 17 | _pipe = pipe; 18 | _logger = logger; 19 | } 20 | 21 | /// 22 | /// Writes a command with arguments to the RPC pipe. 23 | /// 24 | /// The command to execute, from 25 | /// The command arguments object, to be serialized as JSON 26 | /// Task that resolves when the command has been sent 27 | public Task SendCommandAsync(string command, object arguments) 28 | { 29 | var frame = new RpcFrame(); 30 | 31 | var cmd = new RpcCommand 32 | { 33 | Arguments = JObject.FromObject(arguments), 34 | Command = command 35 | }; 36 | 37 | frame.OpCode = OpCode.Frame; 38 | frame.SetContent(JsonConvert.SerializeObject(cmd)); 39 | 40 | return _pipe.WriteAsync(frame); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /DotnetRPC/AsyncEvent.cs: -------------------------------------------------------------------------------- 1 | // Ported over from DSharpPlus. 2 | // it was somewhat inspired by Discord.NET 3 | // asynchronous event code 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | 10 | namespace DotnetRPC 11 | { 12 | /// 13 | /// Represents an asynchronous event handler. 14 | /// 15 | /// Event handling task. 16 | public delegate Task AsyncEventHandler(); 17 | 18 | /// 19 | /// Represents an asynchronous event handler. 20 | /// 21 | /// Type of EventArgs for the event. 22 | /// Event handling task. 23 | public delegate Task AsyncEventHandler(T e) where T : AsyncEventArgs; 24 | 25 | /// 26 | /// 27 | /// Represents asynchronous event arguments. 28 | /// 29 | public abstract class AsyncEventArgs : EventArgs 30 | { 31 | /// 32 | /// Gets or sets whether the event was completely handled. Setting this to true will prevent remaining handlers from running. 33 | /// 34 | public bool Handled { get; set; } 35 | } 36 | 37 | /// 38 | /// 39 | /// Represents event arguments for the event. 40 | /// 41 | public class ClientErroredEventArgs : AsyncEventArgs 42 | { 43 | /// 44 | /// Gets the exception that occurred. 45 | /// 46 | public Exception Exception { get; set; } 47 | } 48 | 49 | /// 50 | /// Represents an asynchronously-handled event. 51 | /// 52 | public sealed class AsyncEvent 53 | { 54 | private readonly object _lock = new object(); 55 | private List Handlers { get; } 56 | private Action ErrorHandler { get; } 57 | private string EventName { get; } 58 | 59 | public AsyncEvent(Action errhandler, string eventName) 60 | { 61 | Handlers = new List(); 62 | ErrorHandler = errhandler; 63 | EventName = eventName; 64 | } 65 | 66 | public void Register(AsyncEventHandler handler) 67 | { 68 | if (handler == null) 69 | throw new ArgumentNullException(nameof(handler), "Handler cannot be null"); 70 | 71 | lock (_lock) 72 | Handlers.Add(handler); 73 | } 74 | 75 | public void Unregister(AsyncEventHandler handler) 76 | { 77 | if (handler == null) 78 | throw new ArgumentNullException(nameof(handler), "Handler cannot be null"); 79 | 80 | lock (_lock) 81 | Handlers.Remove(handler); 82 | } 83 | 84 | public async Task InvokeAsync() 85 | { 86 | AsyncEventHandler[] handlers = null; 87 | lock (_lock) 88 | handlers = Handlers.ToArray(); 89 | 90 | if (!handlers.Any()) 91 | return; 92 | 93 | var exs = new List(handlers.Length); 94 | foreach (var handler in handlers) 95 | { 96 | try 97 | { 98 | await handler().ConfigureAwait(false); 99 | } 100 | catch (Exception ex) 101 | { 102 | exs.Add(ex); 103 | } 104 | } 105 | 106 | if (exs.Any()) 107 | ErrorHandler(EventName, new AggregateException("Exceptions occured within one or more event handlers. Check InnerExceptions for details.", exs)); 108 | } 109 | } 110 | 111 | /// 112 | /// Represents an asynchronously-handled event. 113 | /// 114 | /// Type of EventArgs for this event. 115 | public sealed class AsyncEvent where T : AsyncEventArgs 116 | { 117 | private readonly object _lock = new object(); 118 | private List> Handlers { get; } 119 | private Action ErrorHandler { get; } 120 | private string EventName { get; } 121 | 122 | public AsyncEvent(Action errhandler, string eventName) 123 | { 124 | Handlers = new List>(); 125 | ErrorHandler = errhandler; 126 | EventName = eventName; 127 | } 128 | 129 | public void Register(AsyncEventHandler handler) 130 | { 131 | if (handler == null) 132 | throw new ArgumentNullException(nameof(handler), "Handler cannot be null"); 133 | 134 | lock (_lock) 135 | Handlers.Add(handler); 136 | } 137 | 138 | public void Unregister(AsyncEventHandler handler) 139 | { 140 | if (handler == null) 141 | throw new ArgumentNullException(nameof(handler), "Handler cannot be null"); 142 | 143 | lock (_lock) 144 | Handlers.Remove(handler); 145 | } 146 | 147 | public async Task InvokeAsync(T e) 148 | { 149 | AsyncEventHandler[] handlers = null; 150 | lock (_lock) 151 | handlers = Handlers.ToArray(); 152 | 153 | if (!handlers.Any()) 154 | return; 155 | 156 | var exs = new List(handlers.Length); 157 | foreach (var handler in handlers) 158 | { 159 | try 160 | { 161 | await handler(e).ConfigureAwait(false); 162 | 163 | if (e.Handled) 164 | break; 165 | } 166 | catch (Exception ex) 167 | { 168 | exs.Add(ex); 169 | } 170 | } 171 | 172 | if (exs.Any()) 173 | ErrorHandler(EventName, new AggregateException("Exceptions occured within one or more event handlers. Check InnerExceptions for details.", exs)); 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /DotnetRPC/DotnetRPC.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /DotnetRPC/Entities/ActivitySetModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace DotnetRPC.Entities 6 | { 7 | public class ActivitySetModel 8 | { 9 | internal ActivitySetModel() { } 10 | 11 | public string State = ""; 12 | 13 | public string Details = ""; 14 | 15 | public Time StartUnix = 0; 16 | 17 | public Time EndUnix = 0; 18 | 19 | public string LargeImage = ""; 20 | 21 | public string LargeImageText = ""; 22 | 23 | public string SmallImage = ""; 24 | 25 | public string SmallImageText = ""; 26 | 27 | public string PartyId = ""; 28 | 29 | public int MaxPartySize = 0; 30 | 31 | public int CurrentPartySize = 0; 32 | 33 | public string JoinSecret = ""; 34 | 35 | public string MatchSecret = ""; 36 | 37 | public string SpectateSecret = ""; 38 | 39 | public RpcActivityType? Type { get; internal set; } = null; 40 | } 41 | 42 | public class Time 43 | { 44 | internal int _unixvalue = 0; 45 | 46 | internal Time(int unixvalue) 47 | { 48 | this._unixvalue = unixvalue; 49 | } 50 | 51 | public static implicit operator Time(int value) 52 | { 53 | return new Time(value); 54 | } 55 | 56 | public static implicit operator Time(DateTime value) 57 | { 58 | DateTimeOffset converted = new DateTimeOffset(value); 59 | return new Time((int)converted.ToUnixTimeSeconds()); 60 | } 61 | 62 | public static implicit operator Time(DateTimeOffset value) 63 | { 64 | return new Time((int)value.ToUnixTimeSeconds()); 65 | } 66 | 67 | public static implicit operator int(Time unix) 68 | { 69 | return unix._unixvalue; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /DotnetRPC/Entities/RpcCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Linq; 4 | 5 | namespace DotnetRPC.Entities 6 | { 7 | internal class RpcCommand 8 | { 9 | /// 10 | /// payload command 11 | /// 12 | [JsonProperty("cmd")] 13 | public string Command { get; internal set; } 14 | 15 | /// 16 | /// RPC Arguments 17 | /// 18 | [JsonProperty("args", NullValueHandling = NullValueHandling.Ignore)] 19 | public JObject Arguments { get; internal set; } 20 | 21 | /// 22 | /// unique string used once for replies from the server 23 | /// 24 | [JsonProperty("nonce")] 25 | public string Nonce { get; internal set; } = Guid.NewGuid().ToString(); 26 | 27 | /// 28 | /// event data 29 | /// 30 | [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)] 31 | public JObject Data { get; internal set; } 32 | 33 | /// 34 | /// Event name 35 | /// 36 | [JsonProperty("evt", NullValueHandling = NullValueHandling.Ignore)] 37 | public string Event { get; internal set; } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /DotnetRPC/Entities/RpcFrame.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace DotnetRPC.Entities 5 | { 6 | public enum OpCode 7 | { 8 | Handshake = 0, 9 | Frame = 1, 10 | Close = 2, 11 | Ping = 3, 12 | Pong = 4 13 | } 14 | 15 | internal class RpcFrame 16 | { 17 | public OpCode OpCode { get; internal set; } 18 | 19 | public byte[] Data { get; internal set; } 20 | 21 | public int Length => Data.Length; 22 | 23 | public void SetContent(string input) 24 | { 25 | Data = Encoding.UTF8.GetBytes(input); 26 | } 27 | 28 | public string GetStringContent() 29 | { 30 | return Encoding.UTF8.GetString(Data); 31 | } 32 | 33 | public byte[] GetByteData() 34 | { 35 | var opcode = BitConverter.GetBytes((int)OpCode); 36 | var length = BitConverter.GetBytes(Length); 37 | const int opcodeSize = sizeof(int); 38 | const int lengthSize = sizeof(int); 39 | 40 | var buffer = new byte[opcodeSize + lengthSize + Data.Length]; 41 | opcode.CopyTo(buffer, 0); 42 | length.CopyTo(buffer, opcodeSize); 43 | Data.CopyTo(buffer, opcodeSize + lengthSize); 44 | 45 | return buffer; 46 | } 47 | 48 | public static RpcFrame FromBytes(byte[] input) 49 | { 50 | var frame = new RpcFrame(); 51 | 52 | var opcode = new byte[4]; 53 | var length = new byte[4]; 54 | var data = new byte[input.Length - 8]; 55 | 56 | Array.Copy(input, 0, opcode, 0, 4); 57 | Array.Copy(input, 4, length, 0, 4); 58 | Array.Copy(input, 8, data, 0, data.Length); 59 | 60 | frame.OpCode = (OpCode)BitConverter.ToInt32(opcode, 0); 61 | frame.Data = data; 62 | 63 | return frame; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /DotnetRPC/Entities/RpcHandshake.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace DotnetRPC.Entities 4 | { 5 | internal class RpcHandshake 6 | { 7 | [JsonProperty("v")] 8 | public int Version = 1; 9 | 10 | [JsonProperty("client_id")] 11 | public string ClientId; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /DotnetRPC/Entities/RpcPresence.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | 4 | namespace DotnetRPC.Entities 5 | { 6 | internal class RpcEmptyActivityUpdate 7 | { 8 | [JsonProperty("pid")] 9 | public int ProcessId { get; internal set; } 10 | 11 | public override string ToString() 12 | { 13 | return JsonConvert.SerializeObject(this); 14 | } 15 | } 16 | 17 | internal class RpcActivityUpdate : RpcEmptyActivityUpdate 18 | { 19 | [JsonProperty("activity", NullValueHandling = NullValueHandling.Include)] 20 | public RpcActivity Activity { get; internal set; } 21 | } 22 | 23 | public enum RpcActivityType 24 | { 25 | Playing = 0, 26 | Streaming = 1, 27 | Listening = 2 28 | } 29 | 30 | public class RpcActivity 31 | { 32 | [JsonProperty("state", NullValueHandling = NullValueHandling.Ignore)] 33 | public string State { get; set; } 34 | 35 | [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] 36 | public RpcActivityType? Type { get; internal set; } = null; 37 | 38 | [JsonProperty("details", NullValueHandling = NullValueHandling.Ignore)] 39 | public string Details { get; set; } 40 | 41 | [JsonProperty("timestamps", NullValueHandling = NullValueHandling.Ignore)] 42 | public RpcTimestamps Timestamps { get; set; } 43 | 44 | [JsonProperty("assets", NullValueHandling = NullValueHandling.Ignore)] 45 | public RpcAssets Assets { get; set; } 46 | 47 | [JsonProperty("party", NullValueHandling = NullValueHandling.Ignore)] 48 | public RpcParty Party { get; set; } 49 | 50 | [JsonProperty("secrets", NullValueHandling = NullValueHandling.Ignore)] 51 | public RpcSecrets Secrets { get; set; } 52 | 53 | [JsonProperty("instance", NullValueHandling = NullValueHandling.Ignore)] 54 | public bool Instance { get; set; } = true; 55 | } 56 | 57 | public class RpcTimestamps 58 | { 59 | // THESE ARE DEFINED AS INTEGERS BY DOCS, SSG.. 60 | // https://discordapp.com/developers/docs/topics/gateway#activity-object-activity-timestamps 61 | 62 | [JsonProperty("start", NullValueHandling = NullValueHandling.Ignore)] 63 | internal int StartUnix { get; set; } 64 | 65 | [JsonProperty("end", NullValueHandling = NullValueHandling.Ignore)] 66 | internal int EndUnix { get; set; } 67 | 68 | [JsonIgnore] 69 | public DateTimeOffset Start { 70 | set => StartUnix = (int)value.ToUnixTimeSeconds(); 71 | get => DateTimeOffset.FromUnixTimeSeconds(StartUnix); 72 | } 73 | 74 | [JsonIgnore] 75 | public DateTimeOffset End { 76 | set => EndUnix = (int)value.ToUnixTimeSeconds(); 77 | get => DateTimeOffset.FromUnixTimeSeconds(EndUnix); 78 | } 79 | } 80 | 81 | public class RpcAssets 82 | { 83 | [JsonProperty("large_image", NullValueHandling = NullValueHandling.Ignore)] 84 | public string LargeImage { get; set; } 85 | 86 | [JsonProperty("large_text", NullValueHandling = NullValueHandling.Ignore)] 87 | public string LargeText { get; set; } 88 | 89 | [JsonProperty("small_image", NullValueHandling = NullValueHandling.Ignore)] 90 | public string SmallImage { get; set; } 91 | 92 | [JsonProperty("small_text", NullValueHandling = NullValueHandling.Ignore)] 93 | public string SmallText { get; set; } 94 | } 95 | 96 | public class RpcParty 97 | { 98 | [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] 99 | public string Id { get; set; } 100 | 101 | /// 102 | /// Two ints: current_size and max_size 103 | /// 104 | [JsonProperty("size", NullValueHandling = NullValueHandling.Ignore)] 105 | public int[] Size { get; set; } 106 | } 107 | 108 | public class RpcSecrets 109 | { 110 | [JsonProperty("join", NullValueHandling = NullValueHandling.Ignore)] 111 | public string Join { get; set; } 112 | 113 | [JsonProperty("spectate", NullValueHandling = NullValueHandling.Ignore)] 114 | public string Spectate { get; set; } 115 | 116 | [JsonProperty("match", NullValueHandling = NullValueHandling.Ignore)] 117 | public string Match { get; set; } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /DotnetRPC/Entities/RpcReady.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace DotnetRPC.Entities 7 | { 8 | public class RpcReady : AsyncEventArgs 9 | { 10 | [JsonProperty("v")] 11 | public int Version { get; internal set; } 12 | 13 | [JsonProperty("config")] 14 | public RpcConfig Config { get; internal set; } 15 | 16 | [JsonProperty("user")] 17 | public RpcUser User { get; internal set; } 18 | } 19 | 20 | public class RpcConfig 21 | { 22 | [JsonProperty("cdn_host")] 23 | public string CdnHost { get; internal set; } 24 | 25 | [JsonProperty("api_endpoint")] 26 | public string ApiEndpoint { get; internal set; } 27 | 28 | [JsonProperty("environment")] 29 | public string Environment { get; internal set; } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /DotnetRPC/Entities/RpcUser.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Globalization; 5 | using System.Text; 6 | 7 | // Some parts taken from DSharpPlus but oh well, I own that repo anyway 8 | namespace DotnetRPC.Entities 9 | { 10 | public class RpcUser 11 | { 12 | [JsonProperty("id")] 13 | public ulong Id { get; internal set; } 14 | 15 | [JsonProperty("username")] 16 | public string Username { get; internal set; } 17 | 18 | [JsonProperty("discriminator")] 19 | public string Discriminator { get; internal set; } 20 | 21 | internal int DiscriminatorInt 22 | => int.Parse(this.Discriminator, NumberStyles.Integer, CultureInfo.InvariantCulture); 23 | 24 | [JsonProperty("avatar")] 25 | public string AvatarHash { get; internal set; } 26 | 27 | [JsonIgnore] 28 | public string AvatarUrl 29 | => !string.IsNullOrWhiteSpace(this.AvatarHash) ? (AvatarHash.StartsWith("a_") ? $"https://cdn.discordapp.com/avatars/{this.Id.ToString(CultureInfo.InvariantCulture)}/{AvatarHash}.gif?size=1024" : $"https://cdn.discordapp.com/avatars/{Id}/{AvatarHash}.png?size=1024") : this.DefaultAvatarUrl; 30 | 31 | [JsonIgnore] 32 | public string DefaultAvatarUrl 33 | => $"https://cdn.discordapp.com/embed/avatars/{(this.DiscriminatorInt % 5).ToString(CultureInfo.InvariantCulture)}.png?size=1024"; 34 | 35 | [JsonProperty("bot")] 36 | public bool IsBot { get; internal set; } = false; // Very likely not kek 37 | 38 | // RPC doesn't return much more than this.. anything more actually 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /DotnetRPC/Net/Commands.cs: -------------------------------------------------------------------------------- 1 | namespace DotnetRPC.Net 2 | { 3 | internal static class Commands 4 | { 5 | // not all of these can be implemented over piping 6 | internal const string SetActivity = "SET_ACTIVITY"; 7 | internal const string Dispatch = "DISPATCH"; 8 | internal const string Authorize = "AUTHORIZE"; 9 | internal const string Authenticate = "AUTHENTICATE"; 10 | internal const string GetGuild = "GET_GUILD"; 11 | internal const string GetGuilds = "GET_GUILDS"; 12 | internal const string GetChannel = "GET_CHANNEL"; 13 | internal const string GetChannels = "GET_CHANNELS"; 14 | internal const string Subscribe = "SUBSCRIBE"; 15 | internal const string Unsubscribe = "UNSUBSCRIBE"; 16 | internal const string SetUserVoiceSettings = "SET_USER_VOICE_SETTINGS"; 17 | internal const string SelectVoiceChannel = "SELECT_VOICE_CHANNEL"; 18 | internal const string GetSelectedVoiceChannel = "GET_SELECTED_VOICE_CHANNEL"; 19 | internal const string SelectTextChannel = "SELECT_TEXT_CHANNEL"; 20 | internal const string GetVoiceSettings = "GET_VOICE_SETTINGS"; 21 | internal const string SetVoiceSettings = "SET_VOICE_SETTINGS"; 22 | internal const string CaptureShortcut = "CAPTURE_SHORTCUT"; 23 | internal const string SetCertifiedDevices = "SET_CERTIFIED_DEVICES"; 24 | internal const string SendActivityJoinInvite = "SEND_ACTIVITY_JOIN_INVITE"; 25 | internal const string CloseActivityRequest = "CLOSE_ACTIVITY_REQUEST"; 26 | } 27 | } -------------------------------------------------------------------------------- /DotnetRPC/Net/Events.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace DotnetRPC.Net 6 | { 7 | internal static class Events 8 | { 9 | internal const string Ready = "READY"; 10 | internal const string Error = "ERROR"; 11 | internal const string GuildStatus = "GUILD_STATUS"; 12 | internal const string GuildCreate = "GUILD_CREATE"; 13 | internal const string ChannelCreate = "CHANNEL_CREATE"; 14 | internal const string VoiceChannelSelect = "VOICE_CHANNEL_SELECT"; 15 | internal const string VoiceStateCreate = "VOICE_STATE_CREATE"; 16 | internal const string VoiceStateUpdate = "VOICE_STATE_UPDATE"; 17 | internal const string VoiceStateDelete = "VOICE_STATE_DELETE"; 18 | internal const string VoiceSettingsUpdate = "VOICE_SETTINGS_UPDATE"; 19 | internal const string VoiceConnectionStatus = "VOICE_CONNECTION_STATUS"; 20 | internal const string SpeakingStart = "SPEAKING_START"; 21 | internal const string SpeakingStop = "SPEAKING_STOP"; 22 | internal const string MessageCreate = "MESSAGE_CREATE"; 23 | internal const string MessageUpdate = "MESSAGE_UPDATE"; 24 | internal const string MessageDelete = "MESSAGE_DELETE"; 25 | internal const string NotificationCreate = "NOTIFICATION_CREATE"; 26 | internal const string CaptureShortcutChange = "CAPTURE_SHORTCUT_CHANGE"; 27 | internal const string ActivityJoin = "ACTIVITY_JOIN"; 28 | internal const string ActivitySpectate = "ACTIVITY_SPECTATE"; 29 | internal const string ActivityJoinRequest = "ACTIVITY_JOIN_REQUEST"; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /DotnetRPC/PipeClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO.Pipes; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using DotnetRPC.Entities; 6 | using Newtonsoft.Json; 7 | 8 | namespace DotnetRPC 9 | { 10 | internal class PipeClient 11 | { 12 | internal readonly NamedPipeClientStream Stream; 13 | 14 | private readonly string _pipeName; 15 | private readonly Logger _logger; 16 | 17 | public PipeClient(string pipeName, Logger logger) 18 | { 19 | _pipeName = pipeName; 20 | Stream = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut); 21 | _logger = logger; 22 | } 23 | 24 | public async Task ConnectAsync() 25 | { 26 | await Stream.ConnectAsync(); 27 | await Task.Delay(1000); 28 | if (!Stream.IsConnected) 29 | throw new Exception("Connection failed. Attempting next ipc pipe."); 30 | 31 | _logger.Print(LogLevel.Info, $"Connected to {_pipeName}.", DateTimeOffset.Now); 32 | } 33 | 34 | public async Task ReadNext() 35 | { 36 | if (!Stream.IsConnected) 37 | throw new Exception("Pipe is not connected!"); 38 | 39 | var buffer = new byte[Stream.InBufferSize]; 40 | await Stream.ReadAsync(buffer, 0, Stream.InBufferSize); 41 | 42 | return buffer; 43 | } 44 | 45 | public async Task WriteAsync(RpcFrame frame) 46 | { 47 | if (!Stream.IsConnected) 48 | throw new Exception("Pipe is not connected!"); 49 | 50 | var bf = frame.GetByteData(); 51 | await Stream.WriteAsync(bf, 0, bf.Length); 52 | _logger.Print(LogLevel.Debug, $"Wrote frame with OpCode {frame.OpCode}\nwith Data:\n{JsonConvert.SerializeObject(frame.GetStringContent())}", DateTimeOffset.Now); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /DotnetRPC/RpcClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Runtime.InteropServices; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using DotnetRPC.Entities; 7 | using DotnetRPC.Net; 8 | using Newtonsoft.Json; 9 | using Newtonsoft.Json.Linq; 10 | 11 | namespace DotnetRPC 12 | { 13 | public class RpcClient : IDisposable 14 | { 15 | #region Events 16 | 17 | public event AsyncEventHandler ConnectionClosed 18 | { 19 | add => _connectionClosed.Register(value); 20 | remove => _connectionClosed.Unregister(value); 21 | } 22 | private readonly AsyncEvent _connectionClosed; 23 | 24 | public event AsyncEventHandler ClientErrored 25 | { 26 | add => _clientErrored.Register(value); 27 | remove => _clientErrored.Unregister(value); 28 | } 29 | private readonly AsyncEvent _clientErrored; 30 | 31 | public event AsyncEventHandler Ready 32 | { 33 | add => _ready.Register(value); 34 | remove => _ready.Unregister(value); 35 | } 36 | private readonly AsyncEvent _ready; 37 | #endregion 38 | 39 | public RpcUser CurrentUser { get; internal set; } = null; 40 | public int Version { get; internal set; } = 1; 41 | 42 | private ApiClient ApiClient { get; set; } 43 | private PipeClient Pipe { get; set; } 44 | private string ClientId { get; } 45 | private Logger Logger { get; } 46 | 47 | public RpcClient(string appId, bool registerApp = false, string exePath = null) 48 | { 49 | ClientId = appId; 50 | Logger = new Logger(); 51 | 52 | if (registerApp) 53 | { 54 | if (exePath == null) 55 | throw new ArgumentNullException(nameof(exePath), "Pitiful! If you set registerApp to true, you " + 56 | "must provide path to the executable to register."); 57 | 58 | RegisterAppProtocol(exePath); 59 | } 60 | 61 | _connectionClosed = new AsyncEvent(EventError, "CONNECTION_CLOSE"); 62 | _clientErrored = new AsyncEvent(EventError, "CLIENT_ERROR"); 63 | _ready = new AsyncEvent(EventError, Events.Ready); 64 | } 65 | 66 | internal void EventError(string evname, Exception ex) 67 | { 68 | Logger.Print(LogLevel.Error, $"Whope! Error in event handler: {ex.StackTrace}", DateTimeOffset.Now); 69 | } 70 | 71 | public async Task ConnectAsync() 72 | { 73 | Logger.Print(LogLevel.Info, "Connecting to Discord RPC..", DateTimeOffset.Now); 74 | var rpc = 0; 75 | for (var i = 0; i < 10; i++) 76 | { 77 | Pipe = new PipeClient($"discord-ipc-{i}", Logger); 78 | try 79 | { 80 | await Pipe.ConnectAsync(); 81 | rpc = i; 82 | break; 83 | } 84 | catch (Exception) 85 | { 86 | } 87 | } 88 | // TODO: handle the failure case 89 | 90 | ApiClient = new ApiClient(Pipe, Logger); 91 | 92 | Logger.Print(LogLevel.Info, $"Connected to pipe discord-ipc-{rpc}", DateTimeOffset.Now); 93 | Logger.Print(LogLevel.Info, "Attempting handshake...", DateTimeOffset.Now); 94 | 95 | var shake = new RpcFrame { OpCode = OpCode.Handshake }; 96 | var hs = new RpcHandshake { ClientId = ClientId }; 97 | shake.SetContent(JsonConvert.SerializeObject(hs)); 98 | 99 | await Pipe.WriteAsync(shake); 100 | Logger.Print(LogLevel.Info, "Sent handshake", DateTimeOffset.Now); 101 | 102 | await Task.Factory.StartNew(async () => 103 | { 104 | try 105 | { 106 | while (Pipe != null && Pipe.Stream.IsConnected) 107 | { 108 | var frame = RpcFrame.FromBytes(await Pipe.ReadNext()); 109 | var content = JsonConvert.DeserializeObject(frame.GetStringContent()); 110 | Logger.Print(LogLevel.Debug, 111 | $"Received frame with OpCode {frame.OpCode}\nwith Data:\n{JsonConvert.SerializeObject(content)}", 112 | DateTimeOffset.Now); 113 | 114 | // Handle frame here 115 | 116 | switch (frame.OpCode) 117 | { 118 | case OpCode.Close: 119 | Dispose(); 120 | Logger.Print(LogLevel.Warning, "Received Opcode Close. Closing RPC connection.", DateTimeOffset.Now); 121 | await _connectionClosed.InvokeAsync(null); 122 | break; 123 | case OpCode.Frame: 124 | var cmd = JsonConvert.DeserializeObject(frame.GetStringContent()); 125 | Logger.Print(LogLevel.Debug, $"Received Opcode Frame with command {cmd.Command}", DateTimeOffset.Now); 126 | await HandleFrameAsync(cmd); 127 | break; 128 | } 129 | 130 | await Task.Delay(50); 131 | } 132 | 133 | Logger.Print(LogLevel.Info, "Disconnected! Thread pool is no longer a slave.", DateTimeOffset.Now); 134 | await _connectionClosed.InvokeAsync(null); 135 | } 136 | catch (Exception e) 137 | { 138 | await _clientErrored.InvokeAsync(new ClientErroredEventArgs { Exception = e }); 139 | } 140 | }); 141 | } 142 | 143 | internal async Task HandleFrameAsync(RpcCommand frame) 144 | { 145 | switch (frame.Command) 146 | { 147 | case Commands.Dispatch: 148 | await HandleEventAsync(frame.Event, frame.Data); 149 | break; 150 | } 151 | } 152 | 153 | internal async Task HandleEventAsync(string evt, JObject data) 154 | { 155 | switch (evt) 156 | { 157 | case Events.Ready: 158 | var ready = data.ToObject(); 159 | 160 | this.Logger.Print(LogLevel.Info, $"Received READY with user {ready.User.Username}#{ready.User.Discriminator} with ID {ready.User.Id}", DateTimeOffset.Now); 161 | 162 | this.CurrentUser = ready.User; 163 | this.Version = ready.Version; 164 | 165 | await _ready.InvokeAsync(ready); 166 | break; 167 | } 168 | } 169 | 170 | /// 171 | /// Used to update a user's Rich Presence. 172 | /// 173 | /// The activity/presence to set, or null to set a playing status without Rich 174 | /// Presence. 175 | /// The application's process ID, defaults to the current process' PID 176 | /// Task resolving when the command is executed 177 | public async Task SetActivityAsync(RpcActivity activity, int pid = -1) 178 | { 179 | await ApiClient.SendCommandAsync(Commands.SetActivity, new RpcActivityUpdate 180 | { 181 | ProcessId = pid != -1 ? pid : Process.GetCurrentProcess().Id, 182 | Activity = activity 183 | }); 184 | } 185 | 186 | public async Task SetActivityAsync(Action action, int pid = -1) 187 | { 188 | var mdl = new ActivitySetModel(); 189 | action(mdl); 190 | 191 | var activity = new RpcActivity(); 192 | 193 | #region oof 194 | if (!string.IsNullOrEmpty(mdl.LargeImage) 195 | || !string.IsNullOrEmpty(mdl.LargeImageText) 196 | || !string.IsNullOrEmpty(mdl.SmallImage) 197 | || !string.IsNullOrEmpty(mdl.SmallImageText)) 198 | activity.Assets = new RpcAssets(); 199 | 200 | if (!string.IsNullOrEmpty(mdl.LargeImage)) 201 | activity.Assets.LargeImage = mdl.LargeImage; 202 | if (!string.IsNullOrEmpty(mdl.SmallImage)) 203 | activity.Assets.SmallImage = mdl.SmallImage; 204 | if (!string.IsNullOrEmpty(mdl.LargeImageText)) 205 | activity.Assets.LargeText = mdl.LargeImageText; 206 | if (!string.IsNullOrEmpty(mdl.SmallImageText)) 207 | activity.Assets.SmallText = mdl.SmallImageText; 208 | 209 | if (!string.IsNullOrEmpty(mdl.Details)) 210 | activity.Details = mdl.Details; 211 | if (!string.IsNullOrEmpty(mdl.State)) 212 | activity.State = mdl.State; 213 | 214 | if ((mdl.CurrentPartySize > 0 && mdl.MaxPartySize > 0) || !string.IsNullOrEmpty(mdl.PartyId)) 215 | activity.Party = new RpcParty(); 216 | 217 | if (!string.IsNullOrEmpty(mdl.PartyId)) 218 | activity.Party.Id = mdl.PartyId; 219 | if (mdl.CurrentPartySize > 0 && mdl.MaxPartySize > 0) 220 | activity.Party.Size = new int[2] { mdl.CurrentPartySize, mdl.MaxPartySize }; 221 | 222 | if (!string.IsNullOrEmpty(mdl.JoinSecret) 223 | || !string.IsNullOrEmpty(mdl.MatchSecret) 224 | || !string.IsNullOrEmpty(mdl.SpectateSecret)) 225 | activity.Secrets = new RpcSecrets(); 226 | 227 | if (!string.IsNullOrEmpty(mdl.JoinSecret)) 228 | activity.Secrets.Join = mdl.JoinSecret; 229 | if (!string.IsNullOrEmpty(mdl.MatchSecret)) 230 | activity.Secrets.Match = mdl.MatchSecret; 231 | if (!string.IsNullOrEmpty(mdl.SpectateSecret)) 232 | activity.Secrets.Spectate = mdl.SpectateSecret; 233 | 234 | if (mdl.StartUnix._unixvalue > 0 && mdl.EndUnix._unixvalue > 0) 235 | activity.Timestamps = new RpcTimestamps(); 236 | 237 | if (mdl.StartUnix._unixvalue > 0) 238 | activity.Timestamps.StartUnix = mdl.StartUnix; 239 | 240 | if (mdl.EndUnix._unixvalue > 0) 241 | activity.Timestamps.EndUnix = mdl.EndUnix; 242 | #endregion 243 | 244 | await this.SetActivityAsync(activity, pid); 245 | } 246 | 247 | /// 248 | /// Used to clear a user's Rich Presence and playing status. 249 | /// 250 | /// The application's process ID, defaults to the current process' PID 251 | /// Task resolving when the command is executed 252 | public async Task ClearActivityAsync(int pid = -1) 253 | { 254 | await ApiClient.SendCommandAsync(Commands.SetActivity, new RpcEmptyActivityUpdate 255 | { 256 | ProcessId = pid != -1 ? pid : Process.GetCurrentProcess().Id, 257 | }); 258 | } 259 | 260 | public void RegisterAppProtocol(string exePath) 261 | { 262 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 263 | RpcHelpers.RegisterAppWin(ClientId, exePath, Logger); // Register app protocol for Windows 264 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 265 | throw new PlatformNotSupportedException("App protocols on Linux environments are not (yet) supported!"); // Register app protocol for Linux 266 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 267 | throw new PlatformNotSupportedException("App protocols on OSX environments are not (yet) supported!"); // Register app protocol for OSX 268 | } 269 | 270 | /// 271 | /// Disconnects from RPC. This will free up an IPC slot, and will require calling to 272 | /// use the client again. 273 | /// 274 | public void Disconnect() => Dispose(); 275 | 276 | public void Dispose() 277 | { 278 | Pipe.Stream.Close(); 279 | Pipe.Stream.Dispose(); 280 | Pipe = null; 281 | ApiClient = null; 282 | } 283 | } 284 | } -------------------------------------------------------------------------------- /DotnetRPC/Util/Logger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DotnetRPC 4 | { 5 | internal class Logger 6 | { 7 | public void Print(LogLevel level, string content, DateTimeOffset time) 8 | { 9 | switch (level) 10 | { 11 | default: 12 | Console.ForegroundColor = ConsoleColor.White; 13 | break; 14 | case LogLevel.Debug: 15 | Console.ForegroundColor = ConsoleColor.Blue; 16 | break; 17 | case LogLevel.Info: 18 | Console.ForegroundColor = ConsoleColor.Magenta; 19 | break; 20 | case LogLevel.Warning: 21 | Console.ForegroundColor = ConsoleColor.Yellow; 22 | break; 23 | case LogLevel.Error: 24 | Console.ForegroundColor = ConsoleColor.Red; 25 | break; 26 | case LogLevel.Critical: 27 | Console.ForegroundColor = ConsoleColor.Black; 28 | Console.BackgroundColor = ConsoleColor.Red; 29 | break; 30 | } 31 | Console.Write($"[{level.ToString().ToUpper()}]"); 32 | Console.ResetColor(); 33 | Console.ForegroundColor = ConsoleColor.Green; 34 | Console.Write($"[{time.ToString()}] "); 35 | Console.ResetColor(); 36 | Console.WriteLine(content); 37 | } 38 | } 39 | 40 | internal enum LogLevel 41 | { 42 | Debug, 43 | Info, 44 | Warning, 45 | Error, 46 | Critical 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /DotnetRPC/Util/RpcHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Win32; 3 | 4 | namespace DotnetRPC 5 | { 6 | internal class RpcHelpers 7 | { 8 | /// 9 | /// Register application protocol as discord-[appid]:// (Windows only) 10 | /// 11 | /// Your app's Client ID 12 | /// Path to your app EXE 13 | public static void RegisterAppWin(string appId, string exePath, Logger logger) 14 | { 15 | // Register application protocol as discord-[appid]:// 16 | var key = Registry.ClassesRoot.OpenSubKey($"discord-{appId}"); // Open protocol key 17 | 18 | if (key != null) 19 | Registry.ClassesRoot.DeleteSubKeyTree($"discord-{appId}"); 20 | 21 | key = Registry.ClassesRoot.CreateSubKey($"discord-{appId}"); // Create new key if not exists 22 | 23 | key.SetValue(string.Empty, $"URL: Run game {appId} Protocol"); 24 | key.SetValue("URL Protocol", string.Empty); 25 | 26 | var command = key.CreateSubKey(@"shell\open\command"); 27 | command.SetValue(string.Empty, exePath); 28 | 29 | var defaulticon = key.CreateSubKey(@"DefaultIcon"); 30 | defaulticon.SetValue(string.Empty, exePath); 31 | 32 | // Close registry keys. 33 | command.Close(); 34 | defaulticon.Close(); 35 | key.Close(); 36 | 37 | logger.Print(LogLevel.Info, "Registered registry key for this app.", DateTimeOffset.Now); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 DSharpPlus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DotnetRPC 2 | Discord RPC using the .NET framework. 3 | --------------------------------------------------------------------------------