├── .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 |
--------------------------------------------------------------------------------