mirror of
https://github.com/EnderIce2/SDR-RPC.git
synced 2025-05-25 20:04:25 +00:00
Update plugin
This commit is contained in:
commit
f2e866868a
7
.deepsource.toml
Normal file
7
.deepsource.toml
Normal file
@ -0,0 +1,7 @@
|
||||
version = 1
|
||||
|
||||
exclude_patterns = ["DiscordAPI/**"]
|
||||
|
||||
[[analyzers]]
|
||||
name = "csharp"
|
||||
enabled = true
|
4
.editorconfig
Normal file
4
.editorconfig
Normal file
@ -0,0 +1,4 @@
|
||||
[*.cs]
|
||||
|
||||
# CA1031: Do not catch general exception types
|
||||
dotnet_diagnostic.CA1031.severity = none
|
63
.gitattributes
vendored
Normal file
63
.gitattributes
vendored
Normal file
@ -0,0 +1,63 @@
|
||||
###############################################################################
|
||||
# Set default behavior to automatically normalize line endings.
|
||||
###############################################################################
|
||||
* text=auto
|
||||
|
||||
###############################################################################
|
||||
# Set default behavior for command prompt diff.
|
||||
#
|
||||
# This is need for earlier builds of msysgit that does not have it on by
|
||||
# default for csharp files.
|
||||
# Note: This is only used by command line
|
||||
###############################################################################
|
||||
#*.cs diff=csharp
|
||||
|
||||
###############################################################################
|
||||
# Set the merge driver for project and solution files
|
||||
#
|
||||
# Merging from the command prompt will add diff markers to the files if there
|
||||
# are conflicts (Merging from VS is not affected by the settings below, in VS
|
||||
# the diff markers are never inserted). Diff markers may cause the following
|
||||
# file extensions to fail to load in VS. An alternative would be to treat
|
||||
# these files as binary and thus will always conflict and require user
|
||||
# intervention with every merge. To do so, just uncomment the entries below
|
||||
###############################################################################
|
||||
#*.sln merge=binary
|
||||
#*.csproj merge=binary
|
||||
#*.vbproj merge=binary
|
||||
#*.vcxproj merge=binary
|
||||
#*.vcproj merge=binary
|
||||
#*.dbproj merge=binary
|
||||
#*.fsproj merge=binary
|
||||
#*.lsproj merge=binary
|
||||
#*.wixproj merge=binary
|
||||
#*.modelproj merge=binary
|
||||
#*.sqlproj merge=binary
|
||||
#*.wwaproj merge=binary
|
||||
|
||||
###############################################################################
|
||||
# behavior for image files
|
||||
#
|
||||
# image files are treated as binary by default.
|
||||
###############################################################################
|
||||
#*.jpg binary
|
||||
#*.png binary
|
||||
#*.gif binary
|
||||
|
||||
###############################################################################
|
||||
# diff behavior for common document formats
|
||||
#
|
||||
# Convert binary document formats to text before diffing them. This feature
|
||||
# is only available from the command line. Turn it on by uncommenting the
|
||||
# entries below.
|
||||
###############################################################################
|
||||
#*.doc diff=astextplain
|
||||
#*.DOC diff=astextplain
|
||||
#*.docx diff=astextplain
|
||||
#*.DOCX diff=astextplain
|
||||
#*.dot diff=astextplain
|
||||
#*.DOT diff=astextplain
|
||||
#*.pdf diff=astextplain
|
||||
#*.PDF diff=astextplain
|
||||
#*.rtf diff=astextplain
|
||||
#*.RTF diff=astextplain
|
12
.github/FUNDING.yml
vendored
Normal file
12
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: EnderIce2 # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. Windows 7]
|
||||
- Plugin Version [e.g. 1.2]
|
||||
- SDR# Version [e.g. revision 1770 4698f0a]
|
||||
|
||||
**Plugin Log**
|
||||
A copy of \RPCLogs\DiscordRPCLog_XX.XX.XXXX.log on https://pastebin.com or other copy-paste website.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
BIN
.github/MEDIA/Captură de ecran 2020-10-26 023639.png
vendored
Normal file
BIN
.github/MEDIA/Captură de ecran 2020-10-26 023639.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
BIN
.github/MEDIA/Captură de ecran 2020-10-26 023706.png
vendored
Normal file
BIN
.github/MEDIA/Captură de ecran 2020-10-26 023706.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
.github/MEDIA/Captură de ecran 2020-10-26 023915.png
vendored
Normal file
BIN
.github/MEDIA/Captură de ecran 2020-10-26 023915.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.9 KiB |
BIN
.github/MEDIA/Captură de ecran 2020-10-26 024024.png
vendored
Normal file
BIN
.github/MEDIA/Captură de ecran 2020-10-26 024024.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 768 B |
BIN
.github/MEDIA/Captură de ecran 2020-10-26 025111.png
vendored
Normal file
BIN
.github/MEDIA/Captură de ecran 2020-10-26 025111.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
1
.github/MEDIA/README.md
vendored
Normal file
1
.github/MEDIA/README.md
vendored
Normal file
@ -0,0 +1 @@
|
||||
# Files for README.md
|
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "nuget" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "daily"
|
340
.gitignore
vendored
Normal file
340
.gitignore
vendored
Normal file
@ -0,0 +1,340 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUNIT
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JustCode is a .NET coding add-in
|
||||
.JustCode
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- Backup*.rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# JetBrains Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
32
DiscordAPI/Configuration.cs
Normal file
32
DiscordAPI/Configuration.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration of the current RPC connection
|
||||
/// </summary>
|
||||
public class Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// The Discord API endpoint that should be used.
|
||||
/// </summary>
|
||||
[JsonProperty("api_endpoint")]
|
||||
public string ApiEndpoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The CDN endpoint
|
||||
/// </summary>
|
||||
[JsonProperty("cdn_host")]
|
||||
public string CdnHost { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of enviroment the connection on. Usually Production.
|
||||
/// </summary>
|
||||
[JsonProperty("enviroment")]
|
||||
public string Enviroment { get; set; }
|
||||
}
|
||||
}
|
98
DiscordAPI/Converters/EnumSnakeCaseConverter.cs
Normal file
98
DiscordAPI/Converters/EnumSnakeCaseConverter.cs
Normal file
@ -0,0 +1,98 @@
|
||||
using DiscordRPC.Helper;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace DiscordRPC.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts enums with the <see cref="EnumValueAttribute"/> into Json friendly terms.
|
||||
/// </summary>
|
||||
internal class EnumSnakeCaseConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType.IsEnum;
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.Value == null) return null;
|
||||
|
||||
object val = null;
|
||||
if (TryParseEnum(objectType, (string)reader.Value, out val))
|
||||
return val;
|
||||
|
||||
return existingValue;
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
var enumtype = value.GetType();
|
||||
var name = Enum.GetName(enumtype, value);
|
||||
|
||||
//Get each member and look for hte correct one
|
||||
var members = enumtype.GetMembers(BindingFlags.Public | BindingFlags.Static);
|
||||
foreach (var m in members)
|
||||
{
|
||||
if (m.Name.Equals(name))
|
||||
{
|
||||
var attributes = m.GetCustomAttributes(typeof(EnumValueAttribute), true);
|
||||
if (attributes.Length > 0)
|
||||
{
|
||||
name = ((EnumValueAttribute)attributes[0]).Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writer.WriteValue(name);
|
||||
}
|
||||
|
||||
|
||||
public bool TryParseEnum(Type enumType, string str, out object obj)
|
||||
{
|
||||
//Make sure the string isn;t null
|
||||
if (str == null)
|
||||
{
|
||||
obj = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
//Get the real type
|
||||
Type type = enumType;
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
|
||||
type = type.GetGenericArguments().First();
|
||||
|
||||
//Make sure its actually a enum
|
||||
if (!type.IsEnum)
|
||||
{
|
||||
obj = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
//Get each member and look for hte correct one
|
||||
var members = type.GetMembers(BindingFlags.Public | BindingFlags.Static);
|
||||
foreach (var m in members)
|
||||
{
|
||||
var attributes = m.GetCustomAttributes(typeof(EnumValueAttribute), true);
|
||||
foreach(var a in attributes)
|
||||
{
|
||||
var enumval = (EnumValueAttribute)a;
|
||||
if (str.Equals(enumval.Value))
|
||||
{
|
||||
obj = Enum.Parse(type, m.Name, ignoreCase: true);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//We failed
|
||||
obj = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
16
DiscordAPI/Converters/EnumValueAttribute.cs
Normal file
16
DiscordAPI/Converters/EnumValueAttribute.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Converters
|
||||
{
|
||||
internal class EnumValueAttribute : Attribute
|
||||
{
|
||||
public string Value { get; set; }
|
||||
public EnumValueAttribute(string value)
|
||||
{
|
||||
this.Value = value;
|
||||
}
|
||||
}
|
||||
}
|
901
DiscordAPI/DiscordRpcClient.cs
Normal file
901
DiscordAPI/DiscordRpcClient.cs
Normal file
@ -0,0 +1,901 @@
|
||||
using DiscordRPC.Events;
|
||||
using DiscordRPC.Exceptions;
|
||||
using DiscordRPC.IO;
|
||||
using DiscordRPC.Logging;
|
||||
using DiscordRPC.Message;
|
||||
using DiscordRPC.Registry;
|
||||
using DiscordRPC.RPC;
|
||||
using DiscordRPC.RPC.Commands;
|
||||
using System;
|
||||
|
||||
namespace DiscordRPC
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// A Discord RPC Client which is used to send Rich Presence updates and receive Join / Spectate events.
|
||||
/// </summary>
|
||||
public sealed class DiscordRpcClient : IDisposable
|
||||
{
|
||||
#region Properties
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating if the client has registered a URI Scheme. If this is false, Join / Spectate events will fail.
|
||||
/// <para>To register a URI Scheme, call <see cref="RegisterUriScheme(string, string)"/>.</para>
|
||||
/// </summary>
|
||||
public bool HasRegisteredUriScheme { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Application ID of the RPC Client.
|
||||
/// </summary>
|
||||
public string ApplicationID { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Steam ID of the RPC Client. This value can be null if none was supplied.
|
||||
/// </summary>
|
||||
public string SteamID { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ID of the process used to run the RPC Client. Discord tracks this process ID and waits for its termination. Defaults to the current application process ID.
|
||||
/// </summary>
|
||||
public int ProcessID { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum size of the message queue received from Discord.
|
||||
/// </summary>
|
||||
public int MaxQueueSize { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The dispose state of the client object.
|
||||
/// </summary>
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The logger used this client and its associated components. <see cref="ILogger"/> are not called safely and can come from any thread. It is upto the <see cref="ILogger"/> to account for this and apply appropriate thread safe methods.
|
||||
/// </summary>
|
||||
public ILogger Logger
|
||||
{
|
||||
get { return _logger; }
|
||||
set
|
||||
{
|
||||
this._logger = value;
|
||||
if (connection != null) connection.Logger = value;
|
||||
}
|
||||
}
|
||||
private ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if the client will automatically invoke the events without <see cref="Invoke"/> having to be called.
|
||||
/// </summary>
|
||||
public bool AutoEvents { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Skips sending presences that are identical to the current one.
|
||||
/// </summary>
|
||||
public bool SkipIdenticalPresence { get; set; }
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// The pipe the discord client is on, ranging from 0 to 9. Use -1 to scan through all pipes.
|
||||
/// <para>This property can be used for testing multiple clients. For example, if a Discord Client was on pipe 0, the Discord Canary is most likely on pipe 1.</para>
|
||||
/// </summary>
|
||||
public int TargetPipe { get; private set; }
|
||||
|
||||
private RpcConnection connection;
|
||||
|
||||
/// <summary>
|
||||
/// The current presence that the client has. Gets set with <see cref="SetPresence(RichPresence)"/> and updated on <see cref="OnPresenceUpdate"/>.
|
||||
/// </summary>
|
||||
public RichPresence CurrentPresence { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current subscription to events. Gets set with <see cref="Subscribe(EventType)"/>, <see cref="UnsubscribeMessage"/> and updated on <see cref="OnSubscribe"/>, <see cref="OnUnsubscribe"/>.
|
||||
/// </summary>
|
||||
public EventType Subscription { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current discord user. This is updated with the ready event and will be null until the event is fired from the connection.
|
||||
/// </summary>
|
||||
public User CurrentUser { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current configuration the connection is using. Only becomes available after a ready event.
|
||||
/// </summary>
|
||||
public Configuration Configuration { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Represents if the client has been <see cref="Initialize"/>
|
||||
/// </summary>
|
||||
public bool IsInitialized { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Forces the connection to shutdown gracefully instead of just aborting the connection.
|
||||
/// <para>This option helps prevents ghosting in applications where the Process ID is a host and the game is executed within the host (ie: the Unity3D editor). This will tell Discord that we have no presence and we are closing the connection manually, instead of waiting for the process to terminate.</para>
|
||||
/// </summary>
|
||||
public bool ShutdownOnly
|
||||
{
|
||||
get { return _shutdownOnly; }
|
||||
set
|
||||
{
|
||||
_shutdownOnly = value;
|
||||
if (connection != null) connection.ShutdownOnly = value;
|
||||
}
|
||||
}
|
||||
private bool _shutdownOnly = true;
|
||||
private object _sync = new object();
|
||||
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// Called when the discord client is ready to send and receive messages.
|
||||
/// <para>If <see cref="AutoEvents"/> is true then this event will execute on a different thread. If it is not true however, then this event is not invoked untill <see cref="Invoke"/> and will be on the calling thread.</para>
|
||||
/// </summary>
|
||||
public event OnReadyEvent OnReady;
|
||||
|
||||
/// <summary>
|
||||
/// Called when connection to the Discord Client is lost. The connection will remain close and unready to accept messages until the Ready event is called again.
|
||||
/// <para>If <see cref="AutoEvents"/> is true then this event will execute on a different thread. If it is not true however, then this event is not invoked untill <see cref="Invoke"/> and will be on the calling thread.</para>
|
||||
/// </summary>
|
||||
public event OnCloseEvent OnClose;
|
||||
|
||||
/// <summary>
|
||||
/// Called when a error has occured during the transmission of a message. For example, if a bad Rich Presence payload is sent, this event will be called explaining what went wrong.
|
||||
/// <para>If <see cref="AutoEvents"/> is true then this event will execute on a different thread. If it is not true however, then this event is not invoked untill <see cref="Invoke"/> and will be on the calling thread.</para>
|
||||
/// </summary>
|
||||
public event OnErrorEvent OnError;
|
||||
|
||||
/// <summary>
|
||||
/// Called when the Discord Client has updated the presence.
|
||||
/// <para>If <see cref="AutoEvents"/> is true then this event will execute on a different thread. If it is not true however, then this event is not invoked untill <see cref="Invoke"/> and will be on the calling thread.</para>
|
||||
/// </summary>
|
||||
public event OnPresenceUpdateEvent OnPresenceUpdate;
|
||||
|
||||
/// <summary>
|
||||
/// Called when the Discord Client has subscribed to an event.
|
||||
/// <para>If <see cref="AutoEvents"/> is true then this event will execute on a different thread. If it is not true however, then this event is not invoked untill <see cref="Invoke"/> and will be on the calling thread.</para>
|
||||
/// </summary>
|
||||
public event OnSubscribeEvent OnSubscribe;
|
||||
|
||||
/// <summary>
|
||||
/// Called when the Discord Client has unsubscribed from an event.
|
||||
/// <para>If <see cref="AutoEvents"/> is true then this event will execute on a different thread. If it is not true however, then this event is not invoked untill <see cref="Invoke"/> and will be on the calling thread.</para>
|
||||
/// </summary>
|
||||
public event OnUnsubscribeEvent OnUnsubscribe;
|
||||
|
||||
/// <summary>
|
||||
/// Called when the Discord Client wishes for this process to join a game.
|
||||
/// <para>If <see cref="AutoEvents"/> is true then this event will execute on a different thread. If it is not true however, then this event is not invoked untill <see cref="Invoke"/> and will be on the calling thread.</para>
|
||||
/// </summary>
|
||||
public event OnJoinEvent OnJoin;
|
||||
|
||||
/// <summary>
|
||||
/// Called when the Discord Client wishes for this process to spectate a game.
|
||||
/// <para>If <see cref="AutoEvents"/> is true then this event will execute on a different thread. If it is not true however, then this event is not invoked untill <see cref="Invoke"/> and will be on the calling thread.</para>
|
||||
/// </summary>
|
||||
public event OnSpectateEvent OnSpectate;
|
||||
|
||||
/// <summary>
|
||||
/// Called when another discord user requests permission to join this game.
|
||||
/// <para>This event is not invoked untill <see cref="Invoke"/> is executed.</para>
|
||||
/// </summary>
|
||||
public event OnJoinRequestedEvent OnJoinRequested;
|
||||
|
||||
/// <summary>
|
||||
/// The connection to the discord client was succesfull. This is called before <see cref="MessageType.Ready"/>.
|
||||
/// <para>If <see cref="AutoEvents"/> is true then this event will execute on a different thread. If it is not true however, then this event is not invoked untill <see cref="Invoke"/> and will be on the calling thread.</para>
|
||||
/// </summary>
|
||||
public event OnConnectionEstablishedEvent OnConnectionEstablished;
|
||||
|
||||
/// <summary>
|
||||
/// Failed to establish any connection with discord. Discord is potentially not running?
|
||||
/// <para>If <see cref="AutoEvents"/> is true then this event will execute on a different thread. If it is not true however, then this event is not invoked untill <see cref="Invoke"/> and will be on the calling thread.</para>
|
||||
/// </summary>
|
||||
public event OnConnectionFailedEvent OnConnectionFailed;
|
||||
|
||||
/// <summary>
|
||||
/// The RPC Connection has sent a message. Called before any other event and executed from the RPC Thread.
|
||||
/// </summary>
|
||||
public event OnRpcMessageEvent OnRpcMessage;
|
||||
#endregion
|
||||
|
||||
#region Initialization
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Discord RPC Client which can be used to send Rich Presence and receive Join / Spectate events.
|
||||
/// </summary>
|
||||
/// <param name="applicationID">The ID of the application created at discord's developers portal.</param>
|
||||
public DiscordRpcClient(string applicationID) : this(applicationID, -1) { }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Discord RPC Client which can be used to send Rich Presence and receive Join / Spectate events. This constructor exposes more advance features such as custom NamedPipeClients and Loggers.
|
||||
/// </summary>
|
||||
/// <param name="applicationID">The ID of the application created at discord's developers portal.</param>
|
||||
/// <param name="pipe">The pipe to connect too. If -1, then the client will scan for the first available instance of Discord.</param>
|
||||
/// <param name="logger">The logger used to report messages. If null, then a <see cref="NullLogger"/> will be created and logs will be ignored.</param>
|
||||
/// <param name="autoEvents">Should events be automatically invoked from the RPC Thread as they arrive from discord?</param>
|
||||
/// <param name="client">The pipe client to use and communicate to discord through. If null, the default <see cref="ManagedNamedPipeClient"/> will be used.</param>
|
||||
public DiscordRpcClient(string applicationID, int pipe = -1, ILogger logger = null, bool autoEvents = true, INamedPipeClient client = null)
|
||||
{
|
||||
//Make sure appID is NOT null.
|
||||
if (string.IsNullOrEmpty(applicationID))
|
||||
throw new ArgumentNullException("applicationID");
|
||||
|
||||
//Ensure we actually have json ahead of time. If statement is pointless, but its there just to ensure there is no unused warnings.
|
||||
var jsonConverterType = typeof(Newtonsoft.Json.JsonConverter);
|
||||
if (jsonConverterType == null) throw new Exception("JsonConverter Type Not Found");
|
||||
|
||||
//Store the properties
|
||||
ApplicationID = applicationID.Trim();
|
||||
TargetPipe = pipe;
|
||||
ProcessID = System.Diagnostics.Process.GetCurrentProcess().Id;
|
||||
HasRegisteredUriScheme = false;
|
||||
AutoEvents = autoEvents;
|
||||
SkipIdenticalPresence = true;
|
||||
|
||||
//Prepare the logger
|
||||
_logger = logger ?? new NullLogger();
|
||||
|
||||
//Create the RPC client, giving it the important details
|
||||
connection = new RpcConnection(ApplicationID, ProcessID, TargetPipe, client ?? new ManagedNamedPipeClient(), autoEvents ? 0 : 128U)
|
||||
{
|
||||
ShutdownOnly = _shutdownOnly,
|
||||
Logger = _logger
|
||||
};
|
||||
|
||||
//Subscribe to its event
|
||||
connection.OnRpcMessage += (sender, msg) =>
|
||||
{
|
||||
if (OnRpcMessage != null)
|
||||
OnRpcMessage.Invoke(this, msg);
|
||||
|
||||
if (AutoEvents)
|
||||
ProcessMessage(msg);
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Message Handling
|
||||
/// <summary>
|
||||
/// Dequeues all the messages from Discord, processes them and then invoke appropriate event handlers. This will process the message and update the internal state before invoking the events. Returns the messages that were invoked in the order they were invoked.
|
||||
/// <para>This method cannot be used if <see cref="AutoEvents"/> is enabled.</para>
|
||||
/// </summary>
|
||||
/// <returns>Returns the messages that were invoked and in the order they were invoked.</returns>
|
||||
public IMessage[] Invoke()
|
||||
{
|
||||
if (AutoEvents)
|
||||
{
|
||||
Logger.Error("Cannot Invoke client when AutomaticallyInvokeEvents has been set.");
|
||||
return new IMessage[0];
|
||||
//throw new InvalidOperationException("Cannot Invoke client when AutomaticallyInvokeEvents has been set.");
|
||||
}
|
||||
|
||||
//Dequeue all the messages and process them
|
||||
IMessage[] messages = connection.DequeueMessages();
|
||||
for (int i = 0; i < messages.Length; i++)
|
||||
{
|
||||
//Do a bit of pre-processing
|
||||
var message = messages[i];
|
||||
ProcessMessage(message);
|
||||
}
|
||||
|
||||
//Finally, return the messages
|
||||
return messages;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes the message, updating our internal state and then invokes the events.
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
private void ProcessMessage(IMessage message)
|
||||
{
|
||||
if (message == null) return;
|
||||
switch (message.Type)
|
||||
{
|
||||
//We got a update, so we will update our current presence
|
||||
case MessageType.PresenceUpdate:
|
||||
lock (_sync)
|
||||
{
|
||||
var pm = message as PresenceMessage;
|
||||
if (pm != null)
|
||||
{
|
||||
//We need to merge these presences together
|
||||
if (CurrentPresence == null)
|
||||
{
|
||||
CurrentPresence = pm.Presence;
|
||||
}
|
||||
else if (pm.Presence == null)
|
||||
{
|
||||
CurrentPresence = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentPresence.Merge(pm.Presence);
|
||||
}
|
||||
|
||||
//Update the message
|
||||
pm.Presence = CurrentPresence;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
//Update our configuration
|
||||
case MessageType.Ready:
|
||||
var rm = message as ReadyMessage;
|
||||
if (rm != null)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
Configuration = rm.Configuration;
|
||||
CurrentUser = rm.User;
|
||||
}
|
||||
|
||||
//Resend our presence and subscription
|
||||
SynchronizeState();
|
||||
}
|
||||
break;
|
||||
|
||||
//Update the request's CDN for the avatar helpers
|
||||
case MessageType.JoinRequest:
|
||||
if (Configuration != null)
|
||||
{
|
||||
//Update the User object within the join request if the current Cdn
|
||||
var jrm = message as JoinRequestMessage;
|
||||
if (jrm != null) jrm.User.SetConfiguration(Configuration);
|
||||
}
|
||||
break;
|
||||
|
||||
case MessageType.Subscribe:
|
||||
lock (_sync)
|
||||
{
|
||||
var sub = message as SubscribeMessage;
|
||||
Subscription |= sub.Event;
|
||||
}
|
||||
break;
|
||||
|
||||
case MessageType.Unsubscribe:
|
||||
lock (_sync)
|
||||
{
|
||||
var unsub = message as UnsubscribeMessage;
|
||||
Subscription &= ~unsub.Event;
|
||||
}
|
||||
break;
|
||||
|
||||
//We got a message we dont know what to do with.
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
//Invoke the appropriate methods
|
||||
switch (message.Type)
|
||||
{
|
||||
case MessageType.Ready:
|
||||
if (OnReady != null) OnReady.Invoke(this, message as ReadyMessage);
|
||||
break;
|
||||
|
||||
case MessageType.Close:
|
||||
if (OnClose != null) OnClose.Invoke(this, message as CloseMessage);
|
||||
break;
|
||||
|
||||
case MessageType.Error:
|
||||
if (OnError != null) OnError.Invoke(this, message as ErrorMessage);
|
||||
break;
|
||||
|
||||
case MessageType.PresenceUpdate:
|
||||
if (OnPresenceUpdate != null) OnPresenceUpdate.Invoke(this, message as PresenceMessage);
|
||||
break;
|
||||
|
||||
case MessageType.Subscribe:
|
||||
if (OnSubscribe != null) OnSubscribe.Invoke(this, message as SubscribeMessage);
|
||||
break;
|
||||
|
||||
case MessageType.Unsubscribe:
|
||||
if (OnUnsubscribe != null) OnUnsubscribe.Invoke(this, message as UnsubscribeMessage);
|
||||
break;
|
||||
|
||||
case MessageType.Join:
|
||||
if (OnJoin != null) OnJoin.Invoke(this, message as JoinMessage);
|
||||
break;
|
||||
|
||||
case MessageType.Spectate:
|
||||
if (OnSpectate != null) OnSpectate.Invoke(this, message as SpectateMessage);
|
||||
break;
|
||||
|
||||
case MessageType.JoinRequest:
|
||||
if (OnJoinRequested != null) OnJoinRequested.Invoke(this, message as JoinRequestMessage);
|
||||
break;
|
||||
|
||||
case MessageType.ConnectionEstablished:
|
||||
if (OnConnectionEstablished != null) OnConnectionEstablished.Invoke(this, message as ConnectionEstablishedMessage);
|
||||
break;
|
||||
|
||||
case MessageType.ConnectionFailed:
|
||||
if (OnConnectionFailed != null) OnConnectionFailed.Invoke(this, message as ConnectionFailedMessage);
|
||||
break;
|
||||
|
||||
default:
|
||||
//This in theory can never happen, but its a good idea as a reminder to update this part of the library if any new messages are implemented.
|
||||
Logger.Error("Message was queued with no appropriate handle! {0}", message.Type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Respond to a Join Request. All requests will timeout after 30 seconds.
|
||||
/// <para>Because of the 30 second timeout, it is recommended to call <seealso cref="Invoke"/> faster than every 15 seconds to give your users adequate time to respond to the request.</para>
|
||||
/// </summary>
|
||||
/// <param name="request">The request that is being responded too.</param>
|
||||
/// <param name="acceptRequest">Accept the join request.</param>
|
||||
public void Respond(JoinRequestMessage request, bool acceptRequest)
|
||||
{
|
||||
if (IsDisposed)
|
||||
throw new ObjectDisposedException("Discord IPC Client");
|
||||
|
||||
if (connection == null)
|
||||
throw new ObjectDisposedException("Connection", "Cannot initialize as the connection has been deinitialized");
|
||||
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException();
|
||||
|
||||
connection.EnqueueCommand(new RespondCommand() { Accept = acceptRequest, UserID = request.User.ID.ToString() });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Rich Presence.
|
||||
/// </summary>
|
||||
/// <param name="presence">The Rich Presence to set on the current Discord user.</param>
|
||||
public void SetPresence(RichPresence presence)
|
||||
{
|
||||
if (IsDisposed)
|
||||
throw new ObjectDisposedException("Discord IPC Client");
|
||||
|
||||
if (connection == null)
|
||||
throw new ObjectDisposedException("Connection", "Cannot initialize as the connection has been deinitialized");
|
||||
|
||||
if (!IsInitialized)
|
||||
Logger.Warning("The client is not yet initialized, storing the presence as a state instead.");
|
||||
|
||||
//Send the event
|
||||
if (!presence)
|
||||
{
|
||||
//Clear the presence
|
||||
if (!SkipIdenticalPresence || CurrentPresence != null)
|
||||
connection.EnqueueCommand(new PresenceCommand() { PID = this.ProcessID, Presence = null });
|
||||
}
|
||||
else
|
||||
{
|
||||
//Send valid presence
|
||||
//Validate the presence with our settings
|
||||
if (presence.HasSecrets() && !HasRegisteredUriScheme)
|
||||
throw new BadPresenceException("Cannot send a presence with secrets as this object has not registered a URI scheme. Please enable the uri scheme registration in the DiscordRpcClient constructor.");
|
||||
|
||||
if (presence.HasParty() && presence.Party.Max < presence.Party.Size)
|
||||
throw new BadPresenceException("Presence maximum party size cannot be smaller than the current size.");
|
||||
|
||||
if (presence.HasSecrets() && !presence.HasParty())
|
||||
Logger.Warning("The presence has set the secrets but no buttons will show as there is no party available.");
|
||||
|
||||
//Send the presence, but only if we are not skipping
|
||||
if (!SkipIdenticalPresence || !presence.Matches(CurrentPresence))
|
||||
connection.EnqueueCommand(new PresenceCommand() { PID = this.ProcessID, Presence = presence.Clone() });
|
||||
}
|
||||
|
||||
//Update our local store
|
||||
lock (_sync) { CurrentPresence = presence != null ? presence.Clone() : null; }
|
||||
}
|
||||
|
||||
#region Updates
|
||||
/// <summary>
|
||||
/// Updates only the <see cref="RichPresence.Details"/> of the <see cref="CurrentPresence"/> and sends the updated presence to Discord. Returns the newly edited Rich Presence.
|
||||
/// </summary>
|
||||
/// <param name="details">The details of the Rich Presence</param>
|
||||
/// <returns>Updated Rich Presence</returns>
|
||||
public RichPresence UpdateDetails(string details)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException();
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (CurrentPresence == null) CurrentPresence = new RichPresence();
|
||||
CurrentPresence.Details = details;
|
||||
SetPresence(CurrentPresence);
|
||||
}
|
||||
return CurrentPresence;
|
||||
}
|
||||
/// <summary>
|
||||
/// Updates only the <see cref="RichPresence.State"/> of the <see cref="CurrentPresence"/> and sends the updated presence to Discord. Returns the newly edited Rich Presence.
|
||||
/// </summary>
|
||||
/// <param name="state">The state of the Rich Presence</param>
|
||||
/// <returns>Updated Rich Presence</returns>
|
||||
public RichPresence UpdateState(string state)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException();
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (CurrentPresence == null) CurrentPresence = new RichPresence();
|
||||
CurrentPresence.State = state;
|
||||
SetPresence(CurrentPresence);
|
||||
}
|
||||
return CurrentPresence;
|
||||
}
|
||||
/// <summary>
|
||||
/// Updates only the <see cref="RichPresence.Party"/> of the <see cref="CurrentPresence"/> and sends the updated presence to Discord. Returns the newly edited Rich Presence.
|
||||
/// </summary>
|
||||
/// <param name="party">The party of the Rich Presence</param>
|
||||
/// <returns>Updated Rich Presence</returns>
|
||||
public RichPresence UpdateParty(Party party)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException();
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (CurrentPresence == null) CurrentPresence = new RichPresence();
|
||||
CurrentPresence.Party = party;
|
||||
}
|
||||
|
||||
SetPresence(CurrentPresence);
|
||||
return CurrentPresence;
|
||||
}
|
||||
/// <summary>
|
||||
/// Updates the <see cref="Party.Size"/> of the <see cref="CurrentPresence"/> and sends the update presence to Discord. Returns the newly edited Rich Presence.
|
||||
/// <para>Will return null if no presence exists and will throw a new <see cref="NullReferenceException"/> if the Party does not exist.</para>
|
||||
/// </summary>
|
||||
/// <param name="size">The new size of the party. It cannot be greater than <see cref="Party.Max"/></param>
|
||||
/// <returns>Updated Rich Presence</returns>
|
||||
public RichPresence UpdatePartySize(int size)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException();
|
||||
|
||||
if (CurrentPresence == null) return null;
|
||||
if (CurrentPresence.Party == null)
|
||||
throw new BadPresenceException("Cannot set the size of the party if the party does not exist");
|
||||
|
||||
try { UpdatePartySize(size, CurrentPresence.Party.Max); } catch (Exception) { throw; }
|
||||
return CurrentPresence;
|
||||
}
|
||||
/// <summary>
|
||||
/// Updates the <see cref="Party.Size"/> of the <see cref="CurrentPresence"/> and sends the update presence to Discord. Returns the newly edited Rich Presence.
|
||||
/// <para>Will return null if no presence exists and will throw a new <see cref="NullReferenceException"/> if the Party does not exist.</para>
|
||||
/// </summary>
|
||||
/// <param name="size">The new size of the party. It cannot be greater than <see cref="Party.Max"/></param>
|
||||
/// <param name="max">The new size of the party. It cannot be smaller than <see cref="Party.Size"/></param>
|
||||
/// <returns>Updated Rich Presence</returns>
|
||||
public RichPresence UpdatePartySize(int size, int max)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException();
|
||||
|
||||
if (CurrentPresence == null) return null;
|
||||
if (CurrentPresence.Party == null)
|
||||
throw new BadPresenceException("Cannot set the size of the party if the party does not exist");
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
CurrentPresence.Party.Size = size;
|
||||
CurrentPresence.Party.Max = max;
|
||||
}
|
||||
|
||||
SetPresence(CurrentPresence);
|
||||
return CurrentPresence;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the large <see cref="Assets"/> of the <see cref="CurrentPresence"/> and sends the updated presence to Discord. Both <paramref name="key"/> and <paramref name="tooltip"/> are optional and will be ignored it null.
|
||||
/// </summary>
|
||||
/// <param name="key">Optional: The new key to set the asset too</param>
|
||||
/// <param name="tooltip">Optional: The new tooltip to display on the asset</param>
|
||||
/// <returns>Updated Rich Presence</returns>
|
||||
public RichPresence UpdateLargeAsset(string key = null, string tooltip = null)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException();
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (CurrentPresence == null) CurrentPresence = new RichPresence();
|
||||
if (CurrentPresence.Assets == null) CurrentPresence.Assets = new Assets();
|
||||
CurrentPresence.Assets.LargeImageKey = key ?? CurrentPresence.Assets.LargeImageKey;
|
||||
CurrentPresence.Assets.LargeImageText = tooltip ?? CurrentPresence.Assets.LargeImageText;
|
||||
}
|
||||
|
||||
SetPresence(CurrentPresence);
|
||||
return CurrentPresence;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the small <see cref="Assets"/> of the <see cref="CurrentPresence"/> and sends the updated presence to Discord. Both <paramref name="key"/> and <paramref name="tooltip"/> are optional and will be ignored it null.
|
||||
/// </summary>
|
||||
/// <param name="key">Optional: The new key to set the asset too</param>
|
||||
/// <param name="tooltip">Optional: The new tooltip to display on the asset</param>
|
||||
/// <returns>Updated Rich Presence</returns>
|
||||
public RichPresence UpdateSmallAsset(string key = null, string tooltip = null)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException();
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (CurrentPresence == null) CurrentPresence = new RichPresence();
|
||||
if (CurrentPresence.Assets == null) CurrentPresence.Assets = new Assets();
|
||||
CurrentPresence.Assets.SmallImageKey = key ?? CurrentPresence.Assets.SmallImageKey;
|
||||
CurrentPresence.Assets.SmallImageText = tooltip ?? CurrentPresence.Assets.SmallImageText;
|
||||
}
|
||||
|
||||
SetPresence(CurrentPresence);
|
||||
return CurrentPresence;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the <see cref="Secrets"/> of the <see cref="CurrentPresence"/> and sends the updated presence to Discord. Will override previous secret entirely.
|
||||
/// </summary>
|
||||
/// <param name="secrets">The new secret to send to discord.</param>
|
||||
/// <returns>Updated Rich Presence</returns>
|
||||
public RichPresence UpdateSecrets(Secrets secrets)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException();
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (CurrentPresence == null) CurrentPresence = new RichPresence();
|
||||
CurrentPresence.Secrets = secrets;
|
||||
}
|
||||
|
||||
SetPresence(CurrentPresence);
|
||||
return CurrentPresence;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the start time of the <see cref="CurrentPresence"/> to now and sends the updated presence to Discord.
|
||||
/// </summary>
|
||||
/// <returns>Updated Rich Presence</returns>
|
||||
public RichPresence UpdateStartTime() { try { return UpdateStartTime(DateTime.UtcNow); } catch (Exception) { throw; } }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the start time of the <see cref="CurrentPresence"/> and sends the updated presence to Discord.
|
||||
/// </summary>
|
||||
/// <param name="time">The new time for the start</param>
|
||||
/// <returns>Updated Rich Presence</returns>
|
||||
public RichPresence UpdateStartTime(DateTime time)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException();
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (CurrentPresence == null) CurrentPresence = new RichPresence();
|
||||
if (CurrentPresence.Timestamps == null) CurrentPresence.Timestamps = new Timestamps();
|
||||
CurrentPresence.Timestamps.Start = time;
|
||||
}
|
||||
|
||||
SetPresence(CurrentPresence);
|
||||
return CurrentPresence;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the end time of the <see cref="CurrentPresence"/> to now and sends the updated presence to Discord.
|
||||
/// </summary>
|
||||
/// <returns>Updated Rich Presence</returns>
|
||||
public RichPresence UpdateEndTime() { try { return UpdateEndTime(DateTime.UtcNow); } catch (Exception) { throw; } }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the end time of the <see cref="CurrentPresence"/> and sends the updated presence to Discord.
|
||||
/// </summary>
|
||||
/// <param name="time">The new time for the end</param>
|
||||
/// <returns>Updated Rich Presence</returns>
|
||||
public RichPresence UpdateEndTime(DateTime time)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException();
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (CurrentPresence == null) CurrentPresence = new RichPresence();
|
||||
if (CurrentPresence.Timestamps == null) CurrentPresence.Timestamps = new Timestamps();
|
||||
CurrentPresence.Timestamps.End = time;
|
||||
}
|
||||
|
||||
SetPresence(CurrentPresence);
|
||||
return CurrentPresence;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the start and end time of <see cref="CurrentPresence"/> to null and sends it to Discord.
|
||||
/// </summary>
|
||||
/// <returns>Updated Rich Presence</returns>
|
||||
public RichPresence UpdateClearTime()
|
||||
{
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException();
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (CurrentPresence == null) return null;
|
||||
CurrentPresence.Timestamps = null;
|
||||
}
|
||||
|
||||
SetPresence(CurrentPresence);
|
||||
return CurrentPresence;
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Clears the Rich Presence. Use this just before disposal to prevent ghosting.
|
||||
/// </summary>
|
||||
public void ClearPresence()
|
||||
{
|
||||
if (IsDisposed)
|
||||
throw new ObjectDisposedException("Discord IPC Client");
|
||||
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException();
|
||||
|
||||
if (connection == null)
|
||||
throw new ObjectDisposedException("Connection", "Cannot initialize as the connection has been deinitialized");
|
||||
|
||||
//Just a wrapper function for sending null
|
||||
SetPresence(null);
|
||||
}
|
||||
|
||||
#region Subscriptions
|
||||
|
||||
/// <summary>
|
||||
/// Registers the application executable to a custom URI Scheme.
|
||||
/// <para>This is required for the Join and Spectate features. Discord will run this custom URI Scheme to launch your application when a user presses either of the buttons.</para>
|
||||
/// </summary>
|
||||
/// <param name="steamAppID">Optional Steam ID. If supplied, Discord will launch the game through steam instead of directly calling it.</param>
|
||||
/// <param name="executable">The path to the executable. If null, the path to the current executable will be used instead.</param>
|
||||
/// <returns></returns>
|
||||
public bool RegisterUriScheme(string steamAppID = null, string executable = null)
|
||||
{
|
||||
var urischeme = new UriSchemeRegister(_logger, ApplicationID, steamAppID, executable);
|
||||
return HasRegisteredUriScheme = urischeme.RegisterUriScheme();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to an event sent from discord. Used for Join / Spectate feature.
|
||||
/// <para>Requires the UriScheme to be registered.</para>
|
||||
/// </summary>
|
||||
/// <param name="type">The event type to subscribe to</param>
|
||||
public void Subscribe(EventType type) { SetSubscription(Subscription | type); }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="type"></param>
|
||||
[System.Obsolete("Replaced with Unsubscribe", true)]
|
||||
public void Unubscribe(EventType type) { SetSubscription(Subscription & ~type); }
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribe from the event sent by discord. Used for Join / Spectate feature.
|
||||
/// <para>Requires the UriScheme to be registered.</para>
|
||||
/// </summary>
|
||||
/// <param name="type">The event type to unsubscribe from</param>
|
||||
public void Unsubscribe(EventType type) { SetSubscription(Subscription & ~type); }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the subscription to the events sent from Discord.
|
||||
/// <para>Requires the UriScheme to be registered.</para>
|
||||
/// </summary>
|
||||
/// <param name="type">The new subscription as a flag. Events selected in the flag will be subscribed too and the other events will be unsubscribed.</param>
|
||||
public void SetSubscription(EventType type)
|
||||
{
|
||||
if (IsInitialized)
|
||||
{
|
||||
//Calculate what needs to be unsubscrinbed
|
||||
SubscribeToTypes(Subscription & ~type, true);
|
||||
SubscribeToTypes(~Subscription & type, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Warning("Client has not yet initialized, but events are being subscribed too. Storing them as state instead.");
|
||||
}
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
Subscription = type;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple helper function that will subscribe to the specified types in the flag.
|
||||
/// </summary>
|
||||
/// <param name="type">The flag to subscribe to</param>
|
||||
/// <param name="isUnsubscribe">Represents if the unsubscribe payload should be sent instead.</param>
|
||||
private void SubscribeToTypes(EventType type, bool isUnsubscribe)
|
||||
{
|
||||
//Because of SetSubscription, this can actually be none as there is no differences.
|
||||
//If that is the case, we should just stop here
|
||||
if (type == EventType.None) return;
|
||||
|
||||
//We cannot do anything if we are disposed or missing our connection.
|
||||
if (IsDisposed)
|
||||
throw new ObjectDisposedException("Discord IPC Client");
|
||||
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException();
|
||||
|
||||
if (connection == null)
|
||||
throw new ObjectDisposedException("Connection", "Cannot initialize as the connection has been deinitialized");
|
||||
|
||||
//We dont have the Uri Scheme registered, we should throw a exception to tell the user.
|
||||
if (!HasRegisteredUriScheme)
|
||||
throw new InvalidConfigurationException("Cannot subscribe/unsubscribe to an event as this application has not registered a URI Scheme. Call RegisterUriScheme().");
|
||||
|
||||
//Add the subscribe command to be sent when the connection is able too
|
||||
if ((type & EventType.Spectate) == EventType.Spectate)
|
||||
connection.EnqueueCommand(new SubscribeCommand() { Event = RPC.Payload.ServerEvent.ActivitySpectate, IsUnsubscribe = isUnsubscribe });
|
||||
|
||||
if ((type & EventType.Join) == EventType.Join)
|
||||
connection.EnqueueCommand(new SubscribeCommand() { Event = RPC.Payload.ServerEvent.ActivityJoin, IsUnsubscribe = isUnsubscribe });
|
||||
|
||||
if ((type & EventType.JoinRequest) == EventType.JoinRequest)
|
||||
connection.EnqueueCommand(new SubscribeCommand() { Event = RPC.Payload.ServerEvent.ActivityJoinRequest, IsUnsubscribe = isUnsubscribe });
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Resends the current presence and subscription. This is used when Ready is called to keep the current state within discord.
|
||||
/// </summary>
|
||||
public void SynchronizeState()
|
||||
{
|
||||
//Cannot sync over uninitialized connection
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException();
|
||||
|
||||
//Set the presence and if we have registered the uri scheme, resubscribe.
|
||||
SetPresence(CurrentPresence);
|
||||
if (HasRegisteredUriScheme)
|
||||
SubscribeToTypes(Subscription, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to initalize a connection to the Discord IPC.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool Initialize()
|
||||
{
|
||||
if (IsDisposed)
|
||||
throw new ObjectDisposedException("Discord IPC Client");
|
||||
|
||||
if (IsInitialized)
|
||||
throw new UninitializedException("Cannot initialize a client that is already initialized");
|
||||
|
||||
if (connection == null)
|
||||
throw new ObjectDisposedException("Connection", "Cannot initialize as the connection has been deinitialized");
|
||||
|
||||
return IsInitialized = connection.AttemptConnection();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to disconnect and deinitialize the IPC connection while retaining the settings.
|
||||
/// </summary>
|
||||
public void Deinitialize()
|
||||
{
|
||||
if (!IsInitialized)
|
||||
throw new UninitializedException("Cannot deinitialize a client that has not been initalized.");
|
||||
|
||||
connection.Close();
|
||||
IsInitialized = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Terminates the connection to Discord and disposes of the object.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (IsDisposed) return;
|
||||
if (IsInitialized) Deinitialize();
|
||||
IsDisposed = true;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
34
DiscordAPI/EventType.cs
Normal file
34
DiscordAPI/EventType.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of event receieved by the RPC. A flag type that can be combined.
|
||||
/// </summary>
|
||||
[System.Flags]
|
||||
public enum EventType
|
||||
{
|
||||
/// <summary>
|
||||
/// No event
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Called when the Discord Client wishes to enter a game to spectate
|
||||
/// </summary>
|
||||
Spectate = 0x1,
|
||||
|
||||
/// <summary>
|
||||
/// Called when the Discord Client wishes to enter a game to play.
|
||||
/// </summary>
|
||||
Join = 0x2,
|
||||
|
||||
/// <summary>
|
||||
/// Called when another Discord Client has requested permission to join this game.
|
||||
/// </summary>
|
||||
JoinRequest = 0x4
|
||||
}
|
||||
}
|
94
DiscordAPI/Events.cs
Normal file
94
DiscordAPI/Events.cs
Normal file
@ -0,0 +1,94 @@
|
||||
using DiscordRPC.Message;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// Called when the Discord Client is ready to send and receive messages.
|
||||
/// </summary>
|
||||
/// <param name="sender">The Discord client handler that sent this event</param>
|
||||
/// <param name="args">The arguments supplied with the event</param>
|
||||
public delegate void OnReadyEvent(object sender, ReadyMessage args);
|
||||
|
||||
/// <summary>
|
||||
/// Called when connection to the Discord Client is lost. The connection will remain close and unready to accept messages until the Ready event is called again.
|
||||
/// </summary>
|
||||
/// <param name="sender">The Discord client handler that sent this event</param>
|
||||
/// <param name="args">The arguments supplied with the event</param>
|
||||
public delegate void OnCloseEvent(object sender, CloseMessage args);
|
||||
|
||||
/// <summary>
|
||||
/// Called when a error has occured during the transmission of a message. For example, if a bad Rich Presence payload is sent, this event will be called explaining what went wrong.
|
||||
/// </summary>
|
||||
/// <param name="sender">The Discord client handler that sent this event</param>
|
||||
/// <param name="args">The arguments supplied with the event</param>
|
||||
public delegate void OnErrorEvent(object sender, ErrorMessage args);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the Discord Client has updated the presence.
|
||||
/// </summary>
|
||||
/// <param name="sender">The Discord client handler that sent this event</param>
|
||||
/// <param name="args">The arguments supplied with the event</param>
|
||||
public delegate void OnPresenceUpdateEvent(object sender, PresenceMessage args);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the Discord Client has subscribed to an event.
|
||||
/// </summary>
|
||||
/// <param name="sender">The Discord client handler that sent this event</param>
|
||||
/// <param name="args">The arguments supplied with the event</param>
|
||||
public delegate void OnSubscribeEvent(object sender, SubscribeMessage args);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the Discord Client has unsubscribed from an event.
|
||||
/// </summary>
|
||||
/// <param name="sender">The Discord client handler that sent this event</param>
|
||||
/// <param name="args">The arguments supplied with the event</param>
|
||||
public delegate void OnUnsubscribeEvent(object sender, UnsubscribeMessage args);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the Discord Client wishes for this process to join a game.
|
||||
/// </summary>
|
||||
/// <param name="sender">The Discord client handler that sent this event</param>
|
||||
/// <param name="args">The arguments supplied with the event</param>
|
||||
public delegate void OnJoinEvent(object sender, JoinMessage args);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the Discord Client wishes for this process to spectate a game.
|
||||
/// </summary>
|
||||
/// <param name="sender">The Discord client handler that sent this event</param>
|
||||
/// <param name="args">The arguments supplied with the event</param>
|
||||
public delegate void OnSpectateEvent(object sender, SpectateMessage args);
|
||||
|
||||
/// <summary>
|
||||
/// Called when another discord user requests permission to join this game.
|
||||
/// </summary>
|
||||
/// <param name="sender">The Discord client handler that sent this event</param>
|
||||
/// <param name="args">The arguments supplied with the event</param>
|
||||
public delegate void OnJoinRequestedEvent(object sender, JoinRequestMessage args);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The connection to the discord client was succesfull. This is called before <see cref="OnReadyEvent"/>.
|
||||
/// </summary>
|
||||
/// <param name="sender">The Discord client handler that sent this event</param>
|
||||
/// <param name="args">The arguments supplied with the event</param>
|
||||
public delegate void OnConnectionEstablishedEvent(object sender, ConnectionEstablishedMessage args);
|
||||
|
||||
/// <summary>
|
||||
/// Failed to establish any connection with discord. Discord is potentially not running?
|
||||
/// </summary>
|
||||
/// <param name="sender">The Discord client handler that sent this event</param>
|
||||
/// <param name="args">The arguments supplied with the event</param>
|
||||
public delegate void OnConnectionFailedEvent(object sender, ConnectionFailedMessage args);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A RPC Message is received.
|
||||
/// </summary>
|
||||
/// <param name="sender">The handler that sent this event</param>
|
||||
/// <param name="msg">The raw message from the RPC</param>
|
||||
public delegate void OnRpcMessageEvent(object sender, IMessage msg);
|
||||
}
|
15
DiscordAPI/Exceptions/BadPresenceException.cs
Normal file
15
DiscordAPI/Exceptions/BadPresenceException.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// A BadPresenceException is thrown when invalid, incompatible or conflicting properties and is unable to be sent.
|
||||
/// </summary>
|
||||
public class BadPresenceException : Exception
|
||||
{
|
||||
internal BadPresenceException(string message) : base(message) { }
|
||||
}
|
||||
}
|
15
DiscordAPI/Exceptions/InvalidConfigurationException.cs
Normal file
15
DiscordAPI/Exceptions/InvalidConfigurationException.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// A InvalidConfigurationException is thrown when trying to perform a action that conflicts with the current configuration.
|
||||
/// </summary>
|
||||
public class InvalidConfigurationException : Exception
|
||||
{
|
||||
internal InvalidConfigurationException(string message) : base(message) { }
|
||||
}
|
||||
}
|
16
DiscordAPI/Exceptions/InvalidPipeException.cs
Normal file
16
DiscordAPI/Exceptions/InvalidPipeException.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// The exception that is thrown when a error occurs while communicating with a pipe or when a connection attempt fails.
|
||||
/// </summary>
|
||||
[System.Obsolete("Not actually used anywhere")]
|
||||
public class InvalidPipeException : Exception
|
||||
{
|
||||
internal InvalidPipeException(string message) : base(message) { }
|
||||
}
|
||||
}
|
50
DiscordAPI/Exceptions/StringOutOfRangeException.cs
Normal file
50
DiscordAPI/Exceptions/StringOutOfRangeException.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// A StringOutOfRangeException is thrown when the length of a string exceeds the allowed limit.
|
||||
/// </summary>
|
||||
public class StringOutOfRangeException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Maximum length the string is allowed to be.
|
||||
/// </summary>
|
||||
public int MaximumLength { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Minimum length the string is allowed to be.
|
||||
/// </summary>
|
||||
public int MinimumLength { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new string out of range exception with a range of min to max and a custom message
|
||||
/// </summary>
|
||||
/// <param name="message">The custom message</param>
|
||||
/// <param name="min">Minimum length the string can be</param>
|
||||
/// <param name="max">Maximum length the string can be</param>
|
||||
internal StringOutOfRangeException(string message, int min, int max) : base(message)
|
||||
{
|
||||
MinimumLength = min;
|
||||
MaximumLength = max;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new sting out of range exception with a range of min to max
|
||||
/// </summary>
|
||||
/// <param name="minumum"></param>
|
||||
/// <param name="max"></param>
|
||||
internal StringOutOfRangeException(int minumum, int max)
|
||||
: this("Length of string is out of range. Expected a value between " + minumum + " and " + max, minumum, max) { }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new sting out of range exception with a range of 0 to max
|
||||
/// </summary>
|
||||
/// <param name="max"></param>
|
||||
internal StringOutOfRangeException(int max)
|
||||
: this("Length of string is out of range. Expected a value with a maximum length of " + max, 0, max) { }
|
||||
}
|
||||
}
|
24
DiscordAPI/Exceptions/UninitializedException.cs
Normal file
24
DiscordAPI/Exceptions/UninitializedException.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Thrown when an action is performed on a client that has not yet been initialized
|
||||
/// </summary>
|
||||
public class UninitializedException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new unintialized exception
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
internal UninitializedException(string message) : base(message) { }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new uninitialized exception with default message.
|
||||
/// </summary>
|
||||
internal UninitializedException() : this("Cannot perform action because the client has not been initialized yet or has been deinitialized.") { }
|
||||
}
|
||||
}
|
71
DiscordAPI/Helper/BackoffDelay.cs
Normal file
71
DiscordAPI/Helper/BackoffDelay.cs
Normal file
@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Helper
|
||||
{
|
||||
|
||||
internal class BackoffDelay
|
||||
{
|
||||
/// <summary>
|
||||
/// The maximum time the backoff can reach
|
||||
/// </summary>
|
||||
public int Maximum { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The minimum time the backoff can start at
|
||||
/// </summary>
|
||||
public int Minimum { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current time of the backoff
|
||||
/// </summary>
|
||||
public int Current { get { return _current; } }
|
||||
private int _current;
|
||||
|
||||
/// <summary>
|
||||
/// The current number of failures
|
||||
/// </summary>
|
||||
public int Fails { get { return _fails; } }
|
||||
private int _fails;
|
||||
|
||||
/// <summary>
|
||||
/// The random generator
|
||||
/// </summary>
|
||||
public Random Random { get; set; }
|
||||
|
||||
private BackoffDelay() { }
|
||||
public BackoffDelay(int min, int max) : this(min, max, new Random()) { }
|
||||
public BackoffDelay(int min, int max, Random random)
|
||||
{
|
||||
this.Minimum = min;
|
||||
this.Maximum = max;
|
||||
|
||||
this._current = min;
|
||||
this._fails = 0;
|
||||
this.Random = random;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the backoff
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
_fails = 0;
|
||||
_current = Minimum;
|
||||
}
|
||||
|
||||
public int NextDelay()
|
||||
{
|
||||
//Increment the failures
|
||||
_fails++;
|
||||
|
||||
double diff = (Maximum - Minimum) / 100f;
|
||||
_current = (int)Math.Floor(diff * _fails) + Minimum;
|
||||
|
||||
|
||||
return Math.Min(Math.Max(_current, Minimum), Maximum);
|
||||
}
|
||||
}
|
||||
}
|
73
DiscordAPI/Helper/StringTools.cs
Normal file
73
DiscordAPI/Helper/StringTools.cs
Normal file
@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Helper
|
||||
{
|
||||
/// <summary>
|
||||
/// Collectin of helpful string extensions
|
||||
/// </summary>
|
||||
public static class StringTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Will return null if the string is whitespace, otherwise it will return the string.
|
||||
/// </summary>
|
||||
/// <param name="str">The string to check</param>
|
||||
/// <returns>Null if the string is empty, otherwise the string</returns>
|
||||
public static string GetNullOrString(this string str)
|
||||
{
|
||||
return str.Length == 0 || string.IsNullOrEmpty(str.Trim()) ? null : str;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does the string fit within the given amount of bytes? Uses UTF8 encoding.
|
||||
/// </summary>
|
||||
/// <param name="str">The string to check</param>
|
||||
/// <param name="bytes">The maximum number of bytes the string can take up</param>
|
||||
/// <returns>True if the string fits within the number of bytes</returns>
|
||||
public static bool WithinLength(this string str, int bytes)
|
||||
{
|
||||
return str.WithinLength(bytes, Encoding.UTF8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does the string fit within the given amount of bytes?
|
||||
/// </summary>
|
||||
/// <param name="str">The string to check</param>
|
||||
/// <param name="bytes">The maximum number of bytes the string can take up</param>
|
||||
/// <param name="encoding">The encoding to count the bytes with</param>
|
||||
/// <returns>True if the string fits within the number of bytes</returns>
|
||||
public static bool WithinLength(this string str, int bytes, Encoding encoding)
|
||||
{
|
||||
return encoding.GetByteCount(str) <= bytes;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Converts the string into UpperCamelCase (Pascal Case).
|
||||
/// </summary>
|
||||
/// <param name="str">The string to convert</param>
|
||||
/// <returns></returns>
|
||||
public static string ToCamelCase(this string str)
|
||||
{
|
||||
if (str == null) return null;
|
||||
|
||||
return str.ToLower()
|
||||
.Split(new[] { "_", " " }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(s => char.ToUpper(s[0]) + s.Substring(1, s.Length - 1))
|
||||
.Aggregate(string.Empty, (s1, s2) => s1 + s2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the string into UPPER_SNAKE_CASE
|
||||
/// </summary>
|
||||
/// <param name="str">The string to convert</param>
|
||||
/// <returns></returns>
|
||||
public static string ToSnakeCase(this string str)
|
||||
{
|
||||
if (str == null) return null;
|
||||
var concat = string.Concat(str.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + x.ToString() : x.ToString()).ToArray());
|
||||
return concat.ToUpper();
|
||||
}
|
||||
}
|
||||
}
|
23
DiscordAPI/IO/Handshake.cs
Normal file
23
DiscordAPI/IO/Handshake.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.IO
|
||||
{
|
||||
internal class Handshake
|
||||
{
|
||||
/// <summary>
|
||||
/// Version of the IPC API we are using
|
||||
/// </summary>
|
||||
[JsonProperty("v")]
|
||||
public int Version { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the app.
|
||||
/// </summary>
|
||||
[JsonProperty("client_id")]
|
||||
public string ClientID { get; set; }
|
||||
}
|
||||
}
|
56
DiscordAPI/IO/INamedPipeClient.cs
Normal file
56
DiscordAPI/IO/INamedPipeClient.cs
Normal file
@ -0,0 +1,56 @@
|
||||
using DiscordRPC.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// Pipe Client used to communicate with Discord.
|
||||
/// </summary>
|
||||
public interface INamedPipeClient : IDisposable
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// The logger for the Pipe client to use
|
||||
/// </summary>
|
||||
ILogger Logger { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is the pipe client currently connected?
|
||||
/// </summary>
|
||||
bool IsConnected { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The pipe the client is currently connected too
|
||||
/// </summary>
|
||||
int ConnectedPipe { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to connect to the pipe. If 0-9 is passed to pipe, it should try to only connect to the specified pipe. If -1 is passed, the pipe will find the first available pipe.
|
||||
/// </summary>
|
||||
/// <param name="pipe">If -1 is passed, the pipe will find the first available pipe, otherwise it connects to the pipe that was supplied</param>
|
||||
/// <returns></returns>
|
||||
bool Connect(int pipe);
|
||||
|
||||
/// <summary>
|
||||
/// Reads a frame if there is one available. Returns false if there is none. This should be non blocking (aka use a Peek first).
|
||||
/// </summary>
|
||||
/// <param name="frame">The frame that has been read. Will be <code>default(PipeFrame)</code> if it fails to read</param>
|
||||
/// <returns>Returns true if a frame has been read, otherwise false.</returns>
|
||||
bool ReadFrame(out PipeFrame frame);
|
||||
|
||||
/// <summary>
|
||||
/// Writes the frame to the pipe. Returns false if any errors occur.
|
||||
/// </summary>
|
||||
/// <param name="frame">The frame to be written</param>
|
||||
bool WriteFrame(PipeFrame frame);
|
||||
|
||||
/// <summary>
|
||||
/// Closes the connection
|
||||
/// </summary>
|
||||
void Close();
|
||||
|
||||
}
|
||||
}
|
509
DiscordAPI/IO/ManagedNamedPipeClient.cs
Normal file
509
DiscordAPI/IO/ManagedNamedPipeClient.cs
Normal file
@ -0,0 +1,509 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using DiscordRPC.Logging;
|
||||
using System.IO.Pipes;
|
||||
using System.Threading;
|
||||
using System.IO;
|
||||
|
||||
namespace DiscordRPC.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// A named pipe client using the .NET framework <see cref="NamedPipeClientStream"/>
|
||||
/// </summary>
|
||||
public sealed class ManagedNamedPipeClient : INamedPipeClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Name format of the pipe
|
||||
/// </summary>
|
||||
const string PIPE_NAME = @"discord-ipc-{0}";
|
||||
|
||||
/// <summary>
|
||||
/// The logger for the Pipe client to use
|
||||
/// </summary>
|
||||
public ILogger Logger { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the client is connected
|
||||
/// </summary>
|
||||
public bool IsConnected
|
||||
{
|
||||
get
|
||||
{
|
||||
//This will trigger if the stream is disabled. This should prevent the lock check
|
||||
if (_isClosed) return false;
|
||||
lock (l_stream)
|
||||
{
|
||||
//We cannot be sure its still connected, so lets double check
|
||||
return _stream != null && _stream.IsConnected;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The pipe we are currently connected too.
|
||||
/// </summary>
|
||||
public int ConnectedPipe { get { return _connectedPipe; } }
|
||||
|
||||
private int _connectedPipe;
|
||||
private NamedPipeClientStream _stream;
|
||||
|
||||
private byte[] _buffer = new byte[PipeFrame.MAX_SIZE];
|
||||
|
||||
private Queue<PipeFrame> _framequeue = new Queue<PipeFrame>();
|
||||
private object _framequeuelock = new object();
|
||||
|
||||
private volatile bool _isDisposed = false;
|
||||
private volatile bool _isClosed = true;
|
||||
|
||||
private object l_stream = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of a Managed NamedPipe client. Doesn't connect to anything yet, just setups the values.
|
||||
/// </summary>
|
||||
public ManagedNamedPipeClient()
|
||||
{
|
||||
_buffer = new byte[PipeFrame.MAX_SIZE];
|
||||
Logger = new NullLogger();
|
||||
_stream = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the pipe
|
||||
/// </summary>
|
||||
/// <param name="pipe"></param>
|
||||
/// <returns></returns>
|
||||
public bool Connect(int pipe)
|
||||
{
|
||||
Logger.Trace("ManagedNamedPipeClient.Connection(" + pipe + ")");
|
||||
|
||||
if (_isDisposed)
|
||||
throw new ObjectDisposedException("NamedPipe");
|
||||
|
||||
if (pipe > 9)
|
||||
throw new ArgumentOutOfRangeException("pipe", "Argument cannot be greater than 9");
|
||||
|
||||
if (pipe < 0)
|
||||
{
|
||||
//Iterate until we connect to a pipe
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
if (AttemptConnection(i) || AttemptConnection(i, true))
|
||||
{
|
||||
BeginReadStream();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//Attempt to connect to a specific pipe
|
||||
if (AttemptConnection(pipe) || AttemptConnection(pipe, true))
|
||||
{
|
||||
BeginReadStream();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
//We failed to connect
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts a new connection
|
||||
/// </summary>
|
||||
/// <param name="pipe">The pipe number to connect too.</param>
|
||||
/// <param name="isSandbox">Should the connection to a sandbox be attempted?</param>
|
||||
/// <returns></returns>
|
||||
private bool AttemptConnection(int pipe, bool isSandbox = false)
|
||||
{
|
||||
if (_isDisposed)
|
||||
throw new ObjectDisposedException("_stream");
|
||||
|
||||
//If we are sandbox but we dont support sandbox, then skip
|
||||
string sandbox = isSandbox ? GetPipeSandbox() : "";
|
||||
if (isSandbox && sandbox == null)
|
||||
{
|
||||
Logger.Trace("Skipping sandbox connection.");
|
||||
return false;
|
||||
}
|
||||
|
||||
//Prepare the pipename
|
||||
Logger.Trace("Connection Attempt " + pipe + " (" + sandbox + ")");
|
||||
string pipename = GetPipeName(pipe, sandbox);
|
||||
|
||||
try
|
||||
{
|
||||
//Create the client
|
||||
lock (l_stream)
|
||||
{
|
||||
Logger.Info("Attempting to connect to " + pipename);
|
||||
_stream = new NamedPipeClientStream(".", pipename, PipeDirection.InOut, PipeOptions.Asynchronous);
|
||||
_stream.Connect(1000);
|
||||
|
||||
//Spin for a bit while we wait for it to finish connecting
|
||||
Logger.Trace("Waiting for connection...");
|
||||
do { Thread.Sleep(10); } while (!_stream.IsConnected);
|
||||
}
|
||||
|
||||
//Store the value
|
||||
Logger.Info("Connected to " + pipename);
|
||||
_connectedPipe = pipe;
|
||||
_isClosed = false;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
//Something happened, try again
|
||||
//TODO: Log the failure condition
|
||||
Logger.Error("Failed connection to {0}. {1}", pipename, e.Message);
|
||||
Close();
|
||||
}
|
||||
|
||||
Logger.Trace("Done. Result: {0}", _isClosed);
|
||||
return !_isClosed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a read. Can be executed in another thread.
|
||||
/// </summary>
|
||||
private void BeginReadStream()
|
||||
{
|
||||
if (_isClosed) return;
|
||||
try
|
||||
{
|
||||
lock (l_stream)
|
||||
{
|
||||
//Make sure the stream is valid
|
||||
if (_stream == null || !_stream.IsConnected) return;
|
||||
|
||||
Logger.Trace("Begining Read of {0} bytes", _buffer.Length);
|
||||
_stream.BeginRead(_buffer, 0, _buffer.Length, new AsyncCallback(EndReadStream), _stream.IsConnected);
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
Logger.Warning("Attempted to start reading from a disposed pipe");
|
||||
return;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
//The pipe has been closed
|
||||
Logger.Warning("Attempted to start reading from a closed pipe");
|
||||
return;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error("An exception occured while starting to read a stream: {0}", e.Message);
|
||||
Logger.Error(e.StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ends a read. Can be executed in another thread.
|
||||
/// </summary>
|
||||
/// <param name="callback"></param>
|
||||
private void EndReadStream(IAsyncResult callback)
|
||||
{
|
||||
Logger.Trace("Ending Read");
|
||||
int bytes = 0;
|
||||
|
||||
try
|
||||
{
|
||||
//Attempt to read the bytes, catching for IO exceptions or dispose exceptions
|
||||
lock (l_stream)
|
||||
{
|
||||
//Make sure the stream is still valid
|
||||
if (_stream == null || !_stream.IsConnected) return;
|
||||
|
||||
//Read our btyes
|
||||
bytes = _stream.EndRead(callback);
|
||||
}
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
Logger.Warning("Attempted to end reading from a closed pipe");
|
||||
return;
|
||||
}
|
||||
catch (NullReferenceException)
|
||||
{
|
||||
Logger.Warning("Attempted to read from a null pipe");
|
||||
return;
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
Logger.Warning("Attemped to end reading from a disposed pipe");
|
||||
return;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error("An exception occured while ending a read of a stream: {0}", e.Message);
|
||||
Logger.Error(e.StackTrace);
|
||||
return;
|
||||
}
|
||||
|
||||
//How much did we read?
|
||||
Logger.Trace("Read {0} bytes", bytes);
|
||||
|
||||
//Did we read anything? If we did we should enqueue it.
|
||||
if (bytes > 0)
|
||||
{
|
||||
//Load it into a memory stream and read the frame
|
||||
using (MemoryStream memory = new MemoryStream(_buffer, 0, bytes))
|
||||
{
|
||||
try
|
||||
{
|
||||
PipeFrame frame = new PipeFrame();
|
||||
if (frame.ReadStream(memory))
|
||||
{
|
||||
Logger.Trace("Read a frame: {0}", frame.Opcode);
|
||||
|
||||
//Enqueue the stream
|
||||
lock (_framequeuelock)
|
||||
_framequeue.Enqueue(frame);
|
||||
}
|
||||
else
|
||||
{
|
||||
//TODO: Enqueue a pipe close event here as we failed to read something.
|
||||
Logger.Error("Pipe failed to read from the data received by the stream.");
|
||||
Close();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error("A exception has occured while trying to parse the pipe data: " + e.Message);
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//If we read 0 bytes, its probably a broken pipe. However, I have only confirmed this is the case for MacOSX.
|
||||
// I have added this check here just so the Windows builds are not effected and continue to work as expected.
|
||||
if (IsUnix())
|
||||
{
|
||||
Logger.Error("Empty frame was read on " + Environment.OSVersion.ToString() + ", aborting.");
|
||||
Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Warning("Empty frame was read. Please send report to Lachee.");
|
||||
}
|
||||
}
|
||||
|
||||
//We are still connected, so continue to read
|
||||
if (!_isClosed && IsConnected)
|
||||
{
|
||||
Logger.Trace("Starting another read");
|
||||
BeginReadStream();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a frame, returning false if none are available
|
||||
/// </summary>
|
||||
/// <param name="frame"></param>
|
||||
/// <returns></returns>
|
||||
public bool ReadFrame(out PipeFrame frame)
|
||||
{
|
||||
if (_isDisposed)
|
||||
throw new ObjectDisposedException("_stream");
|
||||
|
||||
//Check the queue, returning the pipe if we have anything available. Otherwise null.
|
||||
lock (_framequeuelock)
|
||||
{
|
||||
if (_framequeue.Count == 0)
|
||||
{
|
||||
//We found nothing, so just default and return null
|
||||
frame = default(PipeFrame);
|
||||
return false;
|
||||
}
|
||||
|
||||
//Return the dequed frame
|
||||
frame = _framequeue.Dequeue();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a frame to the pipe
|
||||
/// </summary>
|
||||
/// <param name="frame"></param>
|
||||
/// <returns></returns>
|
||||
public bool WriteFrame(PipeFrame frame)
|
||||
{
|
||||
if (_isDisposed)
|
||||
throw new ObjectDisposedException("_stream");
|
||||
|
||||
//Write the frame. We are assuming proper duplex connection here
|
||||
if (_isClosed || !IsConnected)
|
||||
{
|
||||
Logger.Error("Failed to write frame because the stream is closed");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
//Write the pipe
|
||||
//This can only happen on the main thread so it should be fine.
|
||||
frame.WriteStream(_stream);
|
||||
return true;
|
||||
}
|
||||
catch (IOException io)
|
||||
{
|
||||
Logger.Error("Failed to write frame because of a IO Exception: {0}", io.Message);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
Logger.Warning("Failed to write frame as the stream was already disposed");
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
Logger.Warning("Failed to write frame because of a invalid operation");
|
||||
}
|
||||
|
||||
//We must have failed the try catch
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the pipe
|
||||
/// </summary>
|
||||
public void Close()
|
||||
{
|
||||
//If we are already closed, jsut exit
|
||||
if (_isClosed)
|
||||
{
|
||||
Logger.Warning("Tried to close a already closed pipe.");
|
||||
return;
|
||||
}
|
||||
|
||||
//flush and dispose
|
||||
try
|
||||
{
|
||||
//Wait for the stream object to become available.
|
||||
lock (l_stream)
|
||||
{
|
||||
if (_stream != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
//Stream isn't null, so flush it and then dispose of it.\
|
||||
// We are doing a catch here because it may throw an error during this process and we dont care if it fails.
|
||||
_stream.Flush();
|
||||
_stream.Dispose();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
//We caught an error, but we dont care anyways because we are disposing of the stream.
|
||||
}
|
||||
|
||||
//Make the stream null and set our flag.
|
||||
_stream = null;
|
||||
_isClosed = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
//The stream is already null?
|
||||
Logger.Warning("Stream was closed, but no stream was available to begin with!");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
//ITs already been disposed
|
||||
Logger.Warning("Tried to dispose already disposed stream");
|
||||
}
|
||||
finally
|
||||
{
|
||||
//For good measures, we will mark the pipe as closed anyways
|
||||
_isClosed = true;
|
||||
_connectedPipe = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes of the stream
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
//Prevent double disposing
|
||||
if (_isDisposed) return;
|
||||
|
||||
//Close the stream (disposing of it too)
|
||||
if (!_isClosed) Close();
|
||||
|
||||
//Dispose of the stream if it hasnt been destroyed already.
|
||||
lock (l_stream)
|
||||
{
|
||||
if (_stream != null)
|
||||
{
|
||||
_stream.Dispose();
|
||||
_stream = null;
|
||||
}
|
||||
}
|
||||
|
||||
//Set our dispose flag
|
||||
_isDisposed = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a platform specific path that Discord is hosting the IPC on.
|
||||
/// </summary>
|
||||
/// <param name="pipe">The pipe number.</param>
|
||||
/// <param name="sandbox">The sandbox the pipe is in. Leave blank for no sandbox.</param>
|
||||
/// <returns></returns>
|
||||
public static string GetPipeName(int pipe, string sandbox = "")
|
||||
{
|
||||
if (!IsUnix()) return sandbox + string.Format(PIPE_NAME, pipe);
|
||||
return Path.Combine(GetTemporaryDirectory(), sandbox + string.Format(PIPE_NAME, pipe));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the possible sandbox enviroment the pipe might be located within. If the platform doesn't support sandboxed Discord, then it will return null.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static string GetPipeSandbox()
|
||||
{
|
||||
switch (Environment.OSVersion.Platform)
|
||||
{
|
||||
default:
|
||||
return null;
|
||||
case PlatformID.Unix:
|
||||
return "snap.discord/";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the temporary path for the current enviroment. Only applicable for UNIX based systems.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private static string GetTemporaryDirectory()
|
||||
{
|
||||
string temp = null;
|
||||
temp = temp ?? Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR");
|
||||
temp = temp ?? Environment.GetEnvironmentVariable("TMPDIR");
|
||||
temp = temp ?? Environment.GetEnvironmentVariable("TMP");
|
||||
temp = temp ?? Environment.GetEnvironmentVariable("TEMP");
|
||||
temp = temp ?? "/tmp";
|
||||
return temp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the current OS platform is Unix based (Unix or MacOSX).
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static bool IsUnix()
|
||||
{
|
||||
switch (Environment.OSVersion.Platform)
|
||||
{
|
||||
default:
|
||||
return false;
|
||||
|
||||
case PlatformID.Unix:
|
||||
case PlatformID.MacOSX:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
33
DiscordAPI/IO/Opcode.cs
Normal file
33
DiscordAPI/IO/Opcode.cs
Normal file
@ -0,0 +1,33 @@
|
||||
namespace DiscordRPC.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// The operation code that the <see cref="PipeFrame"/> was sent under. This defines the type of frame and the data to expect.
|
||||
/// </summary>
|
||||
public enum Opcode : uint
|
||||
{
|
||||
/// <summary>
|
||||
/// Initial handshake frame
|
||||
/// </summary>
|
||||
Handshake = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Generic message frame
|
||||
/// </summary>
|
||||
Frame = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Discord has closed the connection
|
||||
/// </summary>
|
||||
Close = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Ping frame (not used?)
|
||||
/// </summary>
|
||||
Ping = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Pong frame (not used?)
|
||||
/// </summary>
|
||||
Pong = 4
|
||||
}
|
||||
}
|
204
DiscordAPI/IO/PipeFrame.cs
Normal file
204
DiscordAPI/IO/PipeFrame.cs
Normal file
@ -0,0 +1,204 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// A frame received and sent to the Discord client for RPC communications.
|
||||
/// </summary>
|
||||
public struct PipeFrame
|
||||
{
|
||||
/// <summary>
|
||||
/// The maxium size of a pipe frame (16kb).
|
||||
/// </summary>
|
||||
public static readonly int MAX_SIZE = 16 * 1024;
|
||||
|
||||
/// <summary>
|
||||
/// The opcode of the frame
|
||||
/// </summary>
|
||||
public Opcode Opcode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The length of the frame data
|
||||
/// </summary>
|
||||
public uint Length { get { return (uint) Data.Length; } }
|
||||
|
||||
/// <summary>
|
||||
/// The data in the frame
|
||||
/// </summary>
|
||||
public byte[] Data { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The data represented as a string.
|
||||
/// </summary>
|
||||
public string Message
|
||||
{
|
||||
get { return GetMessage(); }
|
||||
set { SetMessage(value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new pipe frame instance
|
||||
/// </summary>
|
||||
/// <param name="opcode">The opcode of the frame</param>
|
||||
/// <param name="data">The data of the frame that will be serialized as JSON</param>
|
||||
public PipeFrame(Opcode opcode, object data)
|
||||
{
|
||||
//Set the opcode and a temp field for data
|
||||
Opcode = opcode;
|
||||
Data = null;
|
||||
|
||||
//Set the data
|
||||
SetObject(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the encoding used for the pipe frames
|
||||
/// </summary>
|
||||
public Encoding MessageEncoding { get { return Encoding.UTF8; } }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the data based of a string
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
private void SetMessage(string str) { Data = MessageEncoding.GetBytes(str); }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a string based of the data
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private string GetMessage() { return MessageEncoding.GetString(Data); }
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the object into json string then encodes it into <see cref="Data"/>.
|
||||
/// </summary>
|
||||
/// <param name="obj"></param>
|
||||
public void SetObject(object obj)
|
||||
{
|
||||
string json = JsonConvert.SerializeObject(obj);
|
||||
SetMessage(json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the opcodes and serializes the object into a json string.
|
||||
/// </summary>
|
||||
/// <param name="opcode"></param>
|
||||
/// <param name="obj"></param>
|
||||
public void SetObject(Opcode opcode, object obj)
|
||||
{
|
||||
Opcode = opcode;
|
||||
SetObject(obj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes the data into the supplied type using JSON.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||
/// <returns></returns>
|
||||
public T GetObject<T>()
|
||||
{
|
||||
string json = GetMessage();
|
||||
return JsonConvert.DeserializeObject<T>(json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to read the contents of the frame from the stream
|
||||
/// </summary>
|
||||
/// <param name="stream"></param>
|
||||
/// <returns></returns>
|
||||
public bool ReadStream(Stream stream)
|
||||
{
|
||||
//Try to read the opcode
|
||||
uint op;
|
||||
if (!TryReadUInt32(stream, out op))
|
||||
return false;
|
||||
|
||||
//Try to read the length
|
||||
uint len;
|
||||
if (!TryReadUInt32(stream, out len))
|
||||
return false;
|
||||
|
||||
uint readsRemaining = len;
|
||||
|
||||
//Read the contents
|
||||
using (var mem = new MemoryStream())
|
||||
{
|
||||
byte[] buffer = new byte[Min(2048, len)]; // read in chunks of 2KB
|
||||
int bytesRead;
|
||||
while ((bytesRead = stream.Read(buffer, 0, Min(buffer.Length, readsRemaining))) > 0)
|
||||
{
|
||||
readsRemaining -= len;
|
||||
mem.Write(buffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
byte[] result = mem.ToArray();
|
||||
if (result.LongLength != len)
|
||||
return false;
|
||||
|
||||
Opcode = (Opcode)op;
|
||||
Data = result;
|
||||
return true;
|
||||
}
|
||||
|
||||
//fun
|
||||
//if (a != null) { do { yield return true; switch (a) { case 1: await new Task(); default: lock (obj) { foreach (b in c) { for (int d = 0; d < 1; d++) { a++; } } } while (a is typeof(int) || (new Class()) != null) } goto MY_LABEL;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns minimum value between a int and a unsigned int
|
||||
/// </summary>
|
||||
private int Min(int a, uint b)
|
||||
{
|
||||
if (b >= a) return a;
|
||||
return (int) b;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to read a UInt32
|
||||
/// </summary>
|
||||
/// <param name="stream"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
private bool TryReadUInt32(Stream stream, out uint value)
|
||||
{
|
||||
//Read the bytes available to us
|
||||
byte[] bytes = new byte[4];
|
||||
int cnt = stream.Read(bytes, 0, bytes.Length);
|
||||
|
||||
//Make sure we actually have a valid value
|
||||
if (cnt != 4)
|
||||
{
|
||||
value = default(uint);
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BitConverter.ToUInt32(bytes, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the frame into the target frame as one big byte block.
|
||||
/// </summary>
|
||||
/// <param name="stream"></param>
|
||||
public void WriteStream(Stream stream)
|
||||
{
|
||||
//Get all the bytes
|
||||
byte[] op = BitConverter.GetBytes((uint) Opcode);
|
||||
byte[] len = BitConverter.GetBytes(Length);
|
||||
|
||||
//Copy it all into a buffer
|
||||
byte[] buff = new byte[op.Length + len.Length + Data.Length];
|
||||
op.CopyTo(buff, 0);
|
||||
len.CopyTo(buff, op.Length);
|
||||
Data.CopyTo(buff, op.Length + len.Length);
|
||||
|
||||
//Write it to the stream
|
||||
stream.Write(buff, 0, buff.Length);
|
||||
}
|
||||
}
|
||||
}
|
21
DiscordAPI/LICENSE.txt
Normal file
21
DiscordAPI/LICENSE.txt
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Lachee
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
101
DiscordAPI/Logging/ConsoleLogger.cs
Normal file
101
DiscordAPI/Logging/ConsoleLogger.cs
Normal file
@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Logs the outputs to the console using <see cref="Console.WriteLine()"/>
|
||||
/// </summary>
|
||||
public class ConsoleLogger : ILogger
|
||||
{
|
||||
/// <summary>
|
||||
/// The level of logging to apply to this logger.
|
||||
/// </summary>
|
||||
public LogLevel Level { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Should the output be coloured?
|
||||
/// </summary>
|
||||
public bool Coloured { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A alias too <see cref="Coloured"/>
|
||||
/// </summary>
|
||||
public bool Colored { get { return Coloured; } set { Coloured = value; } }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of a Console Logger.
|
||||
/// </summary>
|
||||
public ConsoleLogger()
|
||||
{
|
||||
this.Level = LogLevel.Info;
|
||||
Coloured = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of a Console Logger with a set log level
|
||||
/// </summary>
|
||||
/// <param name="level"></param>
|
||||
/// <param name="coloured"></param>
|
||||
public ConsoleLogger(LogLevel level, bool coloured = false)
|
||||
{
|
||||
Level = level;
|
||||
Coloured = coloured;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Informative log messages
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
public void Trace(string message, params object[] args)
|
||||
{
|
||||
if (Level > LogLevel.Trace) return;
|
||||
|
||||
if (Coloured) Console.ForegroundColor = ConsoleColor.Gray;
|
||||
Console.WriteLine("TRACE: " + message, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Informative log messages
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
public void Info(string message, params object[] args)
|
||||
{
|
||||
if (Level > LogLevel.Info) return;
|
||||
|
||||
if (Coloured) Console.ForegroundColor = ConsoleColor.White;
|
||||
Console.WriteLine("INFO: " + message, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Warning log messages
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
public void Warning(string message, params object[] args)
|
||||
{
|
||||
if (Level > LogLevel.Warning) return;
|
||||
|
||||
if (Coloured) Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine("WARN: " + message, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Error log messsages
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
public void Error(string message, params object[] args)
|
||||
{
|
||||
if (Level > LogLevel.Error) return;
|
||||
|
||||
if (Coloured) Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine("ERR : " + message, args);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
92
DiscordAPI/Logging/FileLogger.cs
Normal file
92
DiscordAPI/Logging/FileLogger.cs
Normal file
@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Logs the outputs to a file
|
||||
/// </summary>
|
||||
public class FileLogger : ILogger
|
||||
{
|
||||
/// <summary>
|
||||
/// The level of logging to apply to this logger.
|
||||
/// </summary>
|
||||
public LogLevel Level { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Should the output be coloured?
|
||||
/// </summary>
|
||||
public string File { get; set; }
|
||||
|
||||
private object filelock;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the file logger
|
||||
/// </summary>
|
||||
/// <param name="path">The path of the log file.</param>
|
||||
public FileLogger(string path)
|
||||
: this(path, LogLevel.Info) { }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the file logger
|
||||
/// </summary>
|
||||
/// <param name="path">The path of the log file.</param>
|
||||
/// <param name="level">The level to assign to the logger.</param>
|
||||
public FileLogger(string path, LogLevel level)
|
||||
{
|
||||
Level = level;
|
||||
File = path;
|
||||
filelock = new object();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Informative log messages
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
public void Trace(string message, params object[] args)
|
||||
{
|
||||
if (Level > LogLevel.Trace) return;
|
||||
lock (filelock) System.IO.File.AppendAllText(File, "\r\nTRCE: " + (args.Length > 0 ? string.Format(message, args) : message));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Informative log messages
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
public void Info(string message, params object[] args)
|
||||
{
|
||||
if (Level > LogLevel.Info) return;
|
||||
lock(filelock) System.IO.File.AppendAllText(File, "\r\nINFO: " + (args.Length > 0 ? string.Format(message, args) : message));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Warning log messages
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
public void Warning(string message, params object[] args)
|
||||
{
|
||||
if (Level > LogLevel.Warning) return;
|
||||
lock (filelock)
|
||||
System.IO.File.AppendAllText(File, "\r\nWARN: " + (args.Length > 0 ? string.Format(message, args) : message));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Error log messsages
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
public void Error(string message, params object[] args)
|
||||
{
|
||||
if (Level > LogLevel.Error) return;
|
||||
lock (filelock)
|
||||
System.IO.File.AppendAllText(File, "\r\nERR : " + (args.Length > 0 ? string.Format(message, args) : message));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
46
DiscordAPI/Logging/ILogger.cs
Normal file
46
DiscordAPI/Logging/ILogger.cs
Normal file
@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Logging interface to log the internal states of the pipe. Logs are sent in a NON thread safe way. They can come from multiple threads and it is upto the ILogger to account for it.
|
||||
/// </summary>
|
||||
public interface ILogger
|
||||
{
|
||||
/// <summary>
|
||||
/// The level of logging to apply to this logger.
|
||||
/// </summary>
|
||||
LogLevel Level { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Debug trace messeages used for debugging internal elements.
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
void Trace(string message, params object[] args);
|
||||
|
||||
/// <summary>
|
||||
/// Informative log messages
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
void Info(string message, params object[] args);
|
||||
|
||||
/// <summary>
|
||||
/// Warning log messages
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
void Warning(string message, params object[] args);
|
||||
|
||||
/// <summary>
|
||||
/// Error log messsages
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
void Error(string message, params object[] args);
|
||||
}
|
||||
}
|
38
DiscordAPI/Logging/LogLevel.cs
Normal file
38
DiscordAPI/Logging/LogLevel.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Level of logging to use.
|
||||
/// </summary>
|
||||
public enum LogLevel
|
||||
{
|
||||
/// <summary>
|
||||
/// Trace, Info, Warning and Errors are logged
|
||||
/// </summary>
|
||||
Trace = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Info, Warning and Errors are logged
|
||||
/// </summary>
|
||||
Info = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Warning and Errors are logged
|
||||
/// </summary>
|
||||
Warning = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Only Errors are logged
|
||||
/// </summary>
|
||||
Error = 4,
|
||||
|
||||
/// <summary>
|
||||
/// Nothing is logged
|
||||
/// </summary>
|
||||
None = 256
|
||||
}
|
||||
}
|
58
DiscordAPI/Logging/NullLogger.cs
Normal file
58
DiscordAPI/Logging/NullLogger.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Ignores all log events
|
||||
/// </summary>
|
||||
public class NullLogger : ILogger
|
||||
{
|
||||
/// <summary>
|
||||
/// The level of logging to apply to this logger.
|
||||
/// </summary>
|
||||
public LogLevel Level { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Informative log messages
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
public void Trace(string message, params object[] args)
|
||||
{
|
||||
//Null Logger, so no messages are acutally sent
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Informative log messages
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
public void Info(string message, params object[] args)
|
||||
{
|
||||
//Null Logger, so no messages are acutally sent
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Warning log messages
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
public void Warning(string message, params object[] args)
|
||||
{
|
||||
//Null Logger, so no messages are acutally sent
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Error log messsages
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="args"></param>
|
||||
public void Error(string message, params object[] args)
|
||||
{
|
||||
//Null Logger, so no messages are acutally sent
|
||||
}
|
||||
}
|
||||
}
|
26
DiscordAPI/Message/CloseMessage.cs
Normal file
26
DiscordAPI/Message/CloseMessage.cs
Normal file
@ -0,0 +1,26 @@
|
||||
namespace DiscordRPC.Message
|
||||
{
|
||||
/// <summary>
|
||||
/// Called when the IPC has closed.
|
||||
/// </summary>
|
||||
public class CloseMessage : IMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of message
|
||||
/// </summary>
|
||||
public override MessageType Type { get { return MessageType.Close; } }
|
||||
|
||||
/// <summary>
|
||||
/// The reason for the close
|
||||
/// </summary>
|
||||
public string Reason { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The closure code
|
||||
/// </summary>
|
||||
public int Code { get; internal set; }
|
||||
|
||||
internal CloseMessage() { }
|
||||
internal CloseMessage(string reason) { this.Reason = reason; }
|
||||
}
|
||||
}
|
24
DiscordAPI/Message/ConnectionEstablishedMessage.cs
Normal file
24
DiscordAPI/Message/ConnectionEstablishedMessage.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Message
|
||||
{
|
||||
/// <summary>
|
||||
/// The connection to the discord client was succesfull. This is called before <see cref="MessageType.Ready"/>.
|
||||
/// </summary>
|
||||
public class ConnectionEstablishedMessage : IMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of message received from discord
|
||||
/// </summary>
|
||||
public override MessageType Type { get { return MessageType.ConnectionEstablished; } }
|
||||
|
||||
/// <summary>
|
||||
/// The pipe we ended up connecting too
|
||||
/// </summary>
|
||||
public int ConnectedPipe { get; internal set; }
|
||||
}
|
||||
}
|
24
DiscordAPI/Message/ConnectionFailedMessage.cs
Normal file
24
DiscordAPI/Message/ConnectionFailedMessage.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Message
|
||||
{
|
||||
/// <summary>
|
||||
/// Failed to establish any connection with discord. Discord is potentially not running?
|
||||
/// </summary>
|
||||
public class ConnectionFailedMessage : IMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of message received from discord
|
||||
/// </summary>
|
||||
public override MessageType Type { get { return MessageType.ConnectionFailed; } }
|
||||
|
||||
/// <summary>
|
||||
/// The pipe we failed to connect too.
|
||||
/// </summary>
|
||||
public int FailedPipe { get; internal set; }
|
||||
}
|
||||
}
|
76
DiscordAPI/Message/ErrorMessage.cs
Normal file
76
DiscordAPI/Message/ErrorMessage.cs
Normal file
@ -0,0 +1,76 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace DiscordRPC.Message
|
||||
{
|
||||
/// <summary>
|
||||
/// Created when a error occurs within the ipc and it is sent to the client.
|
||||
/// </summary>
|
||||
public class ErrorMessage : IMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of message received from discord
|
||||
/// </summary>
|
||||
public override MessageType Type { get { return MessageType.Error; } }
|
||||
|
||||
/// <summary>
|
||||
/// The Discord error code.
|
||||
/// </summary>
|
||||
[JsonProperty("code")]
|
||||
public ErrorCode Code { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The message associated with the error code.
|
||||
/// </summary>
|
||||
[JsonProperty("message")]
|
||||
public string Message { get; internal set; }
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The error message received by discord. See https://discordapp.com/developers/docs/topics/rpc#rpc-server-payloads-rpc-errors for documentation
|
||||
/// </summary>
|
||||
public enum ErrorCode
|
||||
{
|
||||
//Pipe Error Codes
|
||||
/// <summary> Pipe was Successful </summary>
|
||||
Success = 0,
|
||||
|
||||
///<summary>The pipe had an exception</summary>
|
||||
PipeException = 1,
|
||||
|
||||
///<summary>The pipe received corrupted data</summary>
|
||||
ReadCorrupt = 2,
|
||||
|
||||
//Custom Error Code
|
||||
///<summary>The functionality was not yet implemented</summary>
|
||||
NotImplemented = 10,
|
||||
|
||||
//Discord RPC error codes
|
||||
///<summary>Unkown Discord error</summary>
|
||||
UnkownError = 1000,
|
||||
|
||||
///<summary>Invalid Payload received</summary>
|
||||
InvalidPayload = 4000,
|
||||
|
||||
///<summary>Invalid command was sent</summary>
|
||||
InvalidCommand = 4002,
|
||||
|
||||
/// <summary>Invalid event was sent </summary>
|
||||
InvalidEvent = 4004,
|
||||
|
||||
/*
|
||||
InvalidGuild = 4003,
|
||||
InvalidChannel = 4005,
|
||||
InvalidPermissions = 4006,
|
||||
InvalidClientID = 4007,
|
||||
InvalidOrigin = 4008,
|
||||
InvalidToken = 4009,
|
||||
InvalidUser = 4010,
|
||||
OAuth2Error = 5000,
|
||||
SelectChannelTimeout = 5001,
|
||||
GetGuildTimeout = 5002,
|
||||
SelectVoiceForceRequired = 5003,
|
||||
CaptureShortcutAlreadyListening = 5004
|
||||
*/
|
||||
}
|
||||
}
|
29
DiscordAPI/Message/IMessage.cs
Normal file
29
DiscordAPI/Message/IMessage.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using System;
|
||||
|
||||
namespace DiscordRPC.Message
|
||||
{
|
||||
/// <summary>
|
||||
/// Messages received from discord.
|
||||
/// </summary>
|
||||
public abstract class IMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of message received from discord
|
||||
/// </summary>
|
||||
public abstract MessageType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The time the message was created
|
||||
/// </summary>
|
||||
public DateTime TimeCreated { get { return _timecreated; } }
|
||||
private DateTime _timecreated;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the message
|
||||
/// </summary>
|
||||
public IMessage()
|
||||
{
|
||||
_timecreated = DateTime.Now;
|
||||
}
|
||||
}
|
||||
}
|
25
DiscordAPI/Message/JoinMessage.cs
Normal file
25
DiscordAPI/Message/JoinMessage.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Message
|
||||
{
|
||||
/// <summary>
|
||||
/// Called when the Discord Client wishes for this process to join a game. D -> C.
|
||||
/// </summary>
|
||||
public class JoinMessage : IMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of message received from discord
|
||||
/// </summary>
|
||||
public override MessageType Type { get { return MessageType.Join; } }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Secrets.JoinSecret" /> to connect with.
|
||||
/// </summary>
|
||||
[JsonProperty("secret")]
|
||||
public string Secret { get; internal set; }
|
||||
}
|
||||
}
|
25
DiscordAPI/Message/JoinRequestMessage.cs
Normal file
25
DiscordAPI/Message/JoinRequestMessage.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Message
|
||||
{
|
||||
/// <summary>
|
||||
/// Called when some other person has requested access to this game. C -> D -> C.
|
||||
/// </summary>
|
||||
public class JoinRequestMessage : IMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of message received from discord
|
||||
/// </summary>
|
||||
public override MessageType Type { get { return MessageType.JoinRequest; } }
|
||||
|
||||
/// <summary>
|
||||
/// The discord user that is requesting access.
|
||||
/// </summary>
|
||||
[JsonProperty("user")]
|
||||
public User User { get; internal set; }
|
||||
}
|
||||
}
|
64
DiscordAPI/Message/MessageType.cs
Normal file
64
DiscordAPI/Message/MessageType.cs
Normal file
@ -0,0 +1,64 @@
|
||||
|
||||
namespace DiscordRPC.Message
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of message.
|
||||
/// </summary>
|
||||
public enum MessageType
|
||||
{
|
||||
/// <summary>
|
||||
/// The Discord Client is ready to send and receive messages.
|
||||
/// </summary>
|
||||
Ready,
|
||||
|
||||
/// <summary>
|
||||
/// The connection to the Discord Client is lost. The connection will remain close and unready to accept messages until the Ready event is called again.
|
||||
/// </summary>
|
||||
Close,
|
||||
|
||||
/// <summary>
|
||||
/// A error has occured during the transmission of a message. For example, if a bad Rich Presence payload is sent, this event will be called explaining what went wrong.
|
||||
/// </summary>
|
||||
Error,
|
||||
|
||||
/// <summary>
|
||||
/// The Discord Client has updated the presence.
|
||||
/// </summary>
|
||||
PresenceUpdate,
|
||||
|
||||
/// <summary>
|
||||
/// The Discord Client has subscribed to an event.
|
||||
/// </summary>
|
||||
Subscribe,
|
||||
|
||||
/// <summary>
|
||||
/// The Discord Client has unsubscribed from an event.
|
||||
/// </summary>
|
||||
Unsubscribe,
|
||||
|
||||
/// <summary>
|
||||
/// The Discord Client wishes for this process to join a game.
|
||||
/// </summary>
|
||||
Join,
|
||||
|
||||
/// <summary>
|
||||
/// The Discord Client wishes for this process to spectate a game.
|
||||
/// </summary>
|
||||
Spectate,
|
||||
|
||||
/// <summary>
|
||||
/// Another discord user requests permission to join this game.
|
||||
/// </summary>
|
||||
JoinRequest,
|
||||
|
||||
/// <summary>
|
||||
/// The connection to the discord client was succesfull. This is called before <see cref="Ready"/>.
|
||||
/// </summary>
|
||||
ConnectionEstablished,
|
||||
|
||||
/// <summary>
|
||||
/// Failed to establish any connection with discord. Discord is potentially not running?
|
||||
/// </summary>
|
||||
ConnectionFailed
|
||||
}
|
||||
}
|
47
DiscordAPI/Message/PresenceMessage.cs
Normal file
47
DiscordAPI/Message/PresenceMessage.cs
Normal file
@ -0,0 +1,47 @@
|
||||
|
||||
|
||||
namespace DiscordRPC.Message
|
||||
{
|
||||
/// <summary>
|
||||
/// Representation of the message received by discord when the presence has been updated.
|
||||
/// </summary>
|
||||
public class PresenceMessage : IMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of message received from discord
|
||||
/// </summary>
|
||||
public override MessageType Type { get { return MessageType.PresenceUpdate; } }
|
||||
|
||||
internal PresenceMessage() : this(null) { }
|
||||
internal PresenceMessage(RichPresenceResponse rpr)
|
||||
{
|
||||
if (rpr == null)
|
||||
{
|
||||
Presence = null;
|
||||
Name = "No Rich Presence";
|
||||
ApplicationID = "";
|
||||
}
|
||||
else
|
||||
{
|
||||
Presence = (RichPresence)rpr;
|
||||
Name = rpr.Name;
|
||||
ApplicationID = rpr.ClientID;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The rich presence Discord has set
|
||||
/// </summary>
|
||||
public RichPresence Presence { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the application Discord has set it for
|
||||
/// </summary>
|
||||
public string Name { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the application discord has set it for
|
||||
/// </summary>
|
||||
public string ApplicationID { get; internal set; }
|
||||
}
|
||||
}
|
35
DiscordAPI/Message/ReadyMessage.cs
Normal file
35
DiscordAPI/Message/ReadyMessage.cs
Normal file
@ -0,0 +1,35 @@
|
||||
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace DiscordRPC.Message
|
||||
{
|
||||
/// <summary>
|
||||
/// Called when the ipc is ready to send arguments.
|
||||
/// </summary>
|
||||
public class ReadyMessage : IMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of message received from discord
|
||||
/// </summary>
|
||||
public override MessageType Type { get { return MessageType.Ready; } }
|
||||
|
||||
/// <summary>
|
||||
/// The configuration of the connection
|
||||
/// </summary>
|
||||
[JsonProperty("config")]
|
||||
public Configuration Configuration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// User the connection belongs too
|
||||
/// </summary>
|
||||
[JsonProperty("user")]
|
||||
public User User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The version of the RPC
|
||||
/// </summary>
|
||||
[JsonProperty("v")]
|
||||
public int Version { get; set; }
|
||||
}
|
||||
}
|
19
DiscordAPI/Message/SpectateMessage.cs
Normal file
19
DiscordAPI/Message/SpectateMessage.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Message
|
||||
{
|
||||
/// <summary>
|
||||
/// Called when the Discord Client wishes for this process to spectate a game. D -> C.
|
||||
/// </summary>
|
||||
public class SpectateMessage : JoinMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of message received from discord
|
||||
/// </summary>
|
||||
public override MessageType Type { get { return MessageType.Spectate; } }
|
||||
}
|
||||
}
|
40
DiscordAPI/Message/SubscribeMessage.cs
Normal file
40
DiscordAPI/Message/SubscribeMessage.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using DiscordRPC.RPC.Payload;
|
||||
|
||||
namespace DiscordRPC.Message
|
||||
{
|
||||
/// <summary>
|
||||
/// Called as validation of a subscribe
|
||||
/// </summary>
|
||||
public class SubscribeMessage : IMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of message received from discord
|
||||
/// </summary>
|
||||
public override MessageType Type { get { return MessageType.Subscribe; } }
|
||||
|
||||
/// <summary>
|
||||
/// The event that was subscribed too.
|
||||
/// </summary>
|
||||
public EventType Event { get; internal set; }
|
||||
|
||||
internal SubscribeMessage(ServerEvent evt)
|
||||
{
|
||||
switch (evt)
|
||||
{
|
||||
default:
|
||||
case ServerEvent.ActivityJoin:
|
||||
Event = EventType.Join;
|
||||
break;
|
||||
|
||||
case ServerEvent.ActivityJoinRequest:
|
||||
Event = EventType.JoinRequest;
|
||||
break;
|
||||
|
||||
case ServerEvent.ActivitySpectate:
|
||||
Event = EventType.Spectate;
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
39
DiscordAPI/Message/UnsubscribeMsesage.cs
Normal file
39
DiscordAPI/Message/UnsubscribeMsesage.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using DiscordRPC.RPC.Payload;
|
||||
|
||||
namespace DiscordRPC.Message
|
||||
{
|
||||
/// <summary>
|
||||
/// Called as validation of a subscribe
|
||||
/// </summary>
|
||||
public class UnsubscribeMessage : IMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of message received from discord
|
||||
/// </summary>
|
||||
public override MessageType Type { get { return MessageType.Unsubscribe; } }
|
||||
|
||||
/// <summary>
|
||||
/// The event that was subscribed too.
|
||||
/// </summary>
|
||||
public EventType Event { get; internal set; }
|
||||
|
||||
internal UnsubscribeMessage(ServerEvent evt)
|
||||
{
|
||||
switch (evt)
|
||||
{
|
||||
default:
|
||||
case ServerEvent.ActivityJoin:
|
||||
Event = EventType.Join;
|
||||
break;
|
||||
|
||||
case ServerEvent.ActivityJoinRequest:
|
||||
Event = EventType.JoinRequest;
|
||||
break;
|
||||
|
||||
case ServerEvent.ActivitySpectate:
|
||||
Event = EventType.Spectate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
34
DiscordAPI/RPC/Commands/CloseCommand.cs
Normal file
34
DiscordAPI/RPC/Commands/CloseCommand.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using DiscordRPC.RPC.Payload;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace DiscordRPC.RPC.Commands
|
||||
{
|
||||
internal class CloseCommand : ICommand
|
||||
{
|
||||
/// <summary>
|
||||
/// The process ID
|
||||
/// </summary>
|
||||
[JsonProperty("pid")]
|
||||
public int PID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The rich presence to be set. Can be null.
|
||||
/// </summary>
|
||||
[JsonProperty("close_reason")]
|
||||
public string value = "Unity 5.5 doesn't handle thread aborts. Can you please close me discord?";
|
||||
|
||||
public IPayload PreparePayload(long nonce)
|
||||
{
|
||||
return new ArgumentPayload()
|
||||
{
|
||||
Command = Command.Dispatch,
|
||||
Nonce = null,
|
||||
Arguments = null
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
13
DiscordAPI/RPC/Commands/ICommand.cs
Normal file
13
DiscordAPI/RPC/Commands/ICommand.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using DiscordRPC.RPC.Payload;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.RPC.Commands
|
||||
{
|
||||
internal interface ICommand
|
||||
{
|
||||
IPayload PreparePayload(long nonce);
|
||||
}
|
||||
}
|
32
DiscordAPI/RPC/Commands/PresenceCommand.cs
Normal file
32
DiscordAPI/RPC/Commands/PresenceCommand.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using DiscordRPC.RPC.Payload;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace DiscordRPC.RPC.Commands
|
||||
{
|
||||
internal class PresenceCommand : ICommand
|
||||
{
|
||||
/// <summary>
|
||||
/// The process ID
|
||||
/// </summary>
|
||||
[JsonProperty("pid")]
|
||||
public int PID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The rich presence to be set. Can be null.
|
||||
/// </summary>
|
||||
[JsonProperty("activity")]
|
||||
public RichPresence Presence { get; set; }
|
||||
|
||||
public IPayload PreparePayload(long nonce)
|
||||
{
|
||||
return new ArgumentPayload(this, nonce)
|
||||
{
|
||||
Command = Command.SetActivity
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
32
DiscordAPI/RPC/Commands/RespondCommand.cs
Normal file
32
DiscordAPI/RPC/Commands/RespondCommand.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using DiscordRPC.RPC.Payload;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace DiscordRPC.RPC.Commands
|
||||
{
|
||||
internal class RespondCommand : ICommand
|
||||
{
|
||||
/// <summary>
|
||||
/// The user ID that we are accepting / rejecting
|
||||
/// </summary>
|
||||
[JsonProperty("user_id")]
|
||||
public string UserID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, the user will be allowed to connect.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool Accept { get; set; }
|
||||
|
||||
public IPayload PreparePayload(long nonce)
|
||||
{
|
||||
return new ArgumentPayload(this, nonce)
|
||||
{
|
||||
Command = Accept ? Command.SendActivityJoinInvite : Command.CloseActivityJoinRequest
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
23
DiscordAPI/RPC/Commands/SubscribeCommand.cs
Normal file
23
DiscordAPI/RPC/Commands/SubscribeCommand.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using DiscordRPC.RPC.Payload;
|
||||
|
||||
namespace DiscordRPC.RPC.Commands
|
||||
{
|
||||
internal class SubscribeCommand : ICommand
|
||||
{
|
||||
public ServerEvent Event { get; set; }
|
||||
public bool IsUnsubscribe { get; set; }
|
||||
|
||||
public IPayload PreparePayload(long nonce)
|
||||
{
|
||||
return new EventPayload(nonce)
|
||||
{
|
||||
Command = IsUnsubscribe ? Command.Unsubscribe : Command.Subscribe,
|
||||
Event = Event
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
23
DiscordAPI/RPC/Payload/ClosePayload.cs
Normal file
23
DiscordAPI/RPC/Payload/ClosePayload.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.RPC.Payload
|
||||
{
|
||||
internal class ClosePayload : IPayload
|
||||
{
|
||||
/// <summary>
|
||||
/// The close code the discord gave us
|
||||
/// </summary>
|
||||
[JsonProperty("code")]
|
||||
public int Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The close reason discord gave us
|
||||
/// </summary>
|
||||
[JsonProperty("message")]
|
||||
public string Reason { get; set; }
|
||||
}
|
||||
}
|
129
DiscordAPI/RPC/Payload/Command.cs
Normal file
129
DiscordAPI/RPC/Payload/Command.cs
Normal file
@ -0,0 +1,129 @@
|
||||
using DiscordRPC.Converters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.RPC.Payload
|
||||
{
|
||||
/// <summary>
|
||||
/// The possible commands that can be sent and received by the server.
|
||||
/// </summary>
|
||||
internal enum Command
|
||||
{
|
||||
/// <summary>
|
||||
/// event dispatch
|
||||
/// </summary>
|
||||
[EnumValue("DISPATCH")]
|
||||
Dispatch,
|
||||
|
||||
/// <summary>
|
||||
/// Called to set the activity
|
||||
/// </summary>
|
||||
[EnumValue("SET_ACTIVITY")]
|
||||
SetActivity,
|
||||
|
||||
/// <summary>
|
||||
/// used to subscribe to an RPC event
|
||||
/// </summary>
|
||||
[EnumValue("SUBSCRIBE")]
|
||||
Subscribe,
|
||||
|
||||
/// <summary>
|
||||
/// used to unsubscribe from an RPC event
|
||||
/// </summary>
|
||||
[EnumValue("UNSUBSCRIBE")]
|
||||
Unsubscribe,
|
||||
|
||||
/// <summary>
|
||||
/// Used to accept join requests.
|
||||
/// </summary>
|
||||
[EnumValue("SEND_ACTIVITY_JOIN_INVITE")]
|
||||
SendActivityJoinInvite,
|
||||
|
||||
/// <summary>
|
||||
/// Used to reject join requests.
|
||||
/// </summary>
|
||||
[EnumValue("CLOSE_ACTIVITY_JOIN_REQUEST")]
|
||||
CloseActivityJoinRequest,
|
||||
|
||||
/// <summary>
|
||||
/// used to authorize a new client with your app
|
||||
/// </summary>
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
Authorize,
|
||||
|
||||
/// <summary>
|
||||
/// used to authenticate an existing client with your app
|
||||
/// </summary>
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
Authenticate,
|
||||
|
||||
/// <summary>
|
||||
/// used to retrieve guild information from the client
|
||||
/// </summary>
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
GetGuild,
|
||||
|
||||
/// <summary>
|
||||
/// used to retrieve a list of guilds from the client
|
||||
/// </summary>
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
GetGuilds,
|
||||
|
||||
/// <summary>
|
||||
/// used to retrieve channel information from the client
|
||||
/// </summary>
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
GetChannel,
|
||||
|
||||
/// <summary>
|
||||
/// used to retrieve a list of channels for a guild from the client
|
||||
/// </summary>
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
GetChannels,
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// used to change voice settings of users in voice channels
|
||||
/// </summary>
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
SetUserVoiceSettings,
|
||||
|
||||
/// <summary>
|
||||
/// used to join or leave a voice channel, group dm, or dm
|
||||
/// </summary>
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
SelectVoiceChannel,
|
||||
|
||||
/// <summary>
|
||||
/// used to get the current voice channel the client is in
|
||||
/// </summary>
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
GetSelectedVoiceChannel,
|
||||
|
||||
/// <summary>
|
||||
/// used to join or leave a text channel, group dm, or dm
|
||||
/// </summary>
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
SelectTextChannel,
|
||||
|
||||
/// <summary>
|
||||
/// used to retrieve the client's voice settings
|
||||
/// </summary>
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
GetVoiceSettings,
|
||||
|
||||
/// <summary>
|
||||
/// used to set the client's voice settings
|
||||
/// </summary>
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
SetVoiceSettings,
|
||||
|
||||
/// <summary>
|
||||
/// used to capture a keyboard shortcut entered by the user RPC Events
|
||||
/// </summary>
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
CaptureShortcut
|
||||
}
|
||||
}
|
35
DiscordAPI/RPC/Payload/IPayload.cs
Normal file
35
DiscordAPI/RPC/Payload/IPayload.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using DiscordRPC.Converters;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace DiscordRPC.RPC.Payload
|
||||
{
|
||||
/// <summary>
|
||||
/// Base Payload that is received by both client and server
|
||||
/// </summary>
|
||||
internal abstract class IPayload
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of payload
|
||||
/// </summary>
|
||||
[JsonProperty("cmd"), JsonConverter(typeof(EnumSnakeCaseConverter))]
|
||||
public Command Command { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A incremental value to help identify payloads
|
||||
/// </summary>
|
||||
[JsonProperty("nonce")]
|
||||
public string Nonce { get; set; }
|
||||
|
||||
protected IPayload() { }
|
||||
protected IPayload(long nonce)
|
||||
{
|
||||
Nonce = nonce.ToString();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "Payload || Command: " + Command.ToString() + ", Nonce: " + (Nonce != null ? Nonce.ToString() : "NULL");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
53
DiscordAPI/RPC/Payload/PayloadArgument.cs
Normal file
53
DiscordAPI/RPC/Payload/PayloadArgument.cs
Normal file
@ -0,0 +1,53 @@
|
||||
using DiscordRPC.Converters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace DiscordRPC.RPC.Payload
|
||||
{
|
||||
/// <summary>
|
||||
/// The payload that is sent by the client to discord for events such as setting the rich presence.
|
||||
/// <para>
|
||||
/// SetPrecense
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal class ArgumentPayload : IPayload
|
||||
{
|
||||
/// <summary>
|
||||
/// The data the server sent too us
|
||||
/// </summary>
|
||||
[JsonProperty("args", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public JObject Arguments { get; set; }
|
||||
|
||||
public ArgumentPayload() : base() { Arguments = null; }
|
||||
public ArgumentPayload(long nonce) : base(nonce) { Arguments = null; }
|
||||
public ArgumentPayload(object args, long nonce) : base(nonce)
|
||||
{
|
||||
SetObject(args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the obejct stored within the data.
|
||||
/// </summary>
|
||||
/// <param name="obj"></param>
|
||||
public void SetObject(object obj)
|
||||
{
|
||||
Arguments = JObject.FromObject(obj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the object stored within the Data
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T GetObject<T>()
|
||||
{
|
||||
return Arguments.ToObject<T>();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "Argument " + base.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
57
DiscordAPI/RPC/Payload/PayloadEvent.cs
Normal file
57
DiscordAPI/RPC/Payload/PayloadEvent.cs
Normal file
@ -0,0 +1,57 @@
|
||||
using DiscordRPC.Converters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace DiscordRPC.RPC.Payload
|
||||
{
|
||||
/// <summary>
|
||||
/// Used for Discord IPC Events
|
||||
/// </summary>
|
||||
internal class EventPayload : IPayload
|
||||
{
|
||||
/// <summary>
|
||||
/// The data the server sent too us
|
||||
/// </summary>
|
||||
[JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public JObject Data { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of event the server sent
|
||||
/// </summary>
|
||||
[JsonProperty("evt"), JsonConverter(typeof(EnumSnakeCaseConverter))]
|
||||
public ServerEvent? Event { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a payload with empty data
|
||||
/// </summary>
|
||||
public EventPayload() : base() { Data = null; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a payload with empty data and a set nonce
|
||||
/// </summary>
|
||||
/// <param name="nonce"></param>
|
||||
public EventPayload(long nonce) : base(nonce) { Data = null; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the object stored within the Data
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T GetObject<T>()
|
||||
{
|
||||
if (Data == null) return default(T);
|
||||
return Data.ToObject<T>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the object into a human readable string
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return "Event " + base.ToString() + ", Event: " + (Event.HasValue ? Event.ToString() : "N/A");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
95
DiscordAPI/RPC/Payload/ServerEvent.cs
Normal file
95
DiscordAPI/RPC/Payload/ServerEvent.cs
Normal file
@ -0,0 +1,95 @@
|
||||
using DiscordRPC.Converters;
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace DiscordRPC.RPC.Payload
|
||||
{
|
||||
/// <summary>
|
||||
/// See https://discordapp.com/developers/docs/topics/rpc#rpc-server-payloads-rpc-events for documentation
|
||||
/// </summary>
|
||||
internal enum ServerEvent
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Sent when the server is ready to accept messages
|
||||
/// </summary>
|
||||
[EnumValue("READY")]
|
||||
Ready,
|
||||
|
||||
/// <summary>
|
||||
/// Sent when something bad has happened
|
||||
/// </summary>
|
||||
[EnumValue("ERROR")]
|
||||
Error,
|
||||
|
||||
/// <summary>
|
||||
/// Join Event
|
||||
/// </summary>
|
||||
[EnumValue("ACTIVITY_JOIN")]
|
||||
ActivityJoin,
|
||||
|
||||
/// <summary>
|
||||
/// Spectate Event
|
||||
/// </summary>
|
||||
[EnumValue("ACTIVITY_SPECTATE")]
|
||||
ActivitySpectate,
|
||||
|
||||
/// <summary>
|
||||
/// Request Event
|
||||
/// </summary>
|
||||
[EnumValue("ACTIVITY_JOIN_REQUEST")]
|
||||
ActivityJoinRequest,
|
||||
|
||||
#if INCLUDE_FULL_RPC
|
||||
//Old things that are obsolete
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("GUILD_STATUS")]
|
||||
GuildStatus,
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("GUILD_CREATE")]
|
||||
GuildCreate,
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("CHANNEL_CREATE")]
|
||||
ChannelCreate,
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("VOICE_CHANNEL_SELECT")]
|
||||
VoiceChannelSelect,
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("VOICE_STATE_CREATED")]
|
||||
VoiceStateCreated,
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("VOICE_STATE_UPDATED")]
|
||||
VoiceStateUpdated,
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("VOICE_STATE_DELETE")]
|
||||
VoiceStateDelete,
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("VOICE_SETTINGS_UPDATE")]
|
||||
VoiceSettingsUpdate,
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("VOICE_CONNECTION_STATUS")]
|
||||
VoiceConnectionStatus,
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("SPEAKING_START")]
|
||||
SpeakingStart,
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("SPEAKING_STOP")]
|
||||
SpeakingStop,
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("MESSAGE_CREATE")]
|
||||
MessageCreate,
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("MESSAGE_UPDATE")]
|
||||
MessageUpdate,
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("MESSAGE_DELETE")]
|
||||
MessageDelete,
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("NOTIFICATION_CREATE")]
|
||||
NotificationCreate,
|
||||
[Obsolete("This value is appart of the RPC API and is not supported by this library.", true)]
|
||||
[EnumValue("CAPTURE_SHORTCUT_CHANGE")]
|
||||
CaptureShortcutChange
|
||||
#endif
|
||||
}
|
||||
}
|
869
DiscordAPI/RPC/RpcConnection.cs
Normal file
869
DiscordAPI/RPC/RpcConnection.cs
Normal file
@ -0,0 +1,869 @@
|
||||
using DiscordRPC.Helper;
|
||||
using DiscordRPC.Message;
|
||||
using DiscordRPC.IO;
|
||||
using DiscordRPC.RPC.Commands;
|
||||
using DiscordRPC.RPC.Payload;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Newtonsoft.Json;
|
||||
using DiscordRPC.Logging;
|
||||
using DiscordRPC.Events;
|
||||
|
||||
namespace DiscordRPC.RPC
|
||||
{
|
||||
/// <summary>
|
||||
/// Communicates between the client and discord through RPC
|
||||
/// </summary>
|
||||
internal class RpcConnection : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Version of the RPC Protocol
|
||||
/// </summary>
|
||||
public static readonly int VERSION = 1;
|
||||
|
||||
/// <summary>
|
||||
/// The rate of poll to the discord pipe.
|
||||
/// </summary>
|
||||
public static readonly int POLL_RATE = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// Should we send a null presence on the fairwells?
|
||||
/// </summary>
|
||||
private static readonly bool CLEAR_ON_SHUTDOWN = true;
|
||||
|
||||
/// <summary>
|
||||
/// Should we work in a lock step manner? This option is semi-obsolete and may not work as expected.
|
||||
/// </summary>
|
||||
private static readonly bool LOCK_STEP = false;
|
||||
|
||||
/// <summary>
|
||||
/// The logger used by the RPC connection
|
||||
/// </summary>
|
||||
public ILogger Logger
|
||||
{
|
||||
get { return _logger; }
|
||||
set
|
||||
{
|
||||
_logger = value;
|
||||
if (namedPipe != null)
|
||||
namedPipe.Logger = value;
|
||||
}
|
||||
}
|
||||
private ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Called when a message is received from the RPC and is about to be enqueued. This is cross-thread and will execute on the RPC thread.
|
||||
/// </summary>
|
||||
public event OnRpcMessageEvent OnRpcMessage;
|
||||
|
||||
#region States
|
||||
|
||||
/// <summary>
|
||||
/// The current state of the RPC connection
|
||||
/// </summary>
|
||||
public RpcState State { get { var tmp = RpcState.Disconnected; lock (l_states) tmp = _state; return tmp; } }
|
||||
private RpcState _state;
|
||||
private readonly object l_states = new object();
|
||||
|
||||
/// <summary>
|
||||
/// The configuration received by the Ready
|
||||
/// </summary>
|
||||
public Configuration Configuration { get { Configuration tmp = null; lock (l_config) tmp = _configuration; return tmp; } }
|
||||
private Configuration _configuration = null;
|
||||
private readonly object l_config = new object();
|
||||
|
||||
private volatile bool aborting = false;
|
||||
private volatile bool shutdown = false;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if the RPC connection is still running in the background
|
||||
/// </summary>
|
||||
public bool IsRunning { get { return thread != null; } }
|
||||
|
||||
/// <summary>
|
||||
/// Forces the <see cref="Close"/> to call <see cref="Shutdown"/> instead, safely saying goodbye to Discord.
|
||||
/// <para>This option helps prevents ghosting in applications where the Process ID is a host and the game is executed within the host (ie: the Unity3D editor). This will tell Discord that we have no presence and we are closing the connection manually, instead of waiting for the process to terminate.</para>
|
||||
/// </summary>
|
||||
public bool ShutdownOnly { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Privates
|
||||
|
||||
private string applicationID; //ID of the Discord APP
|
||||
private int processID; //ID of the process to track
|
||||
|
||||
private long nonce; //Current command index
|
||||
|
||||
private Thread thread; //The current thread
|
||||
private INamedPipeClient namedPipe;
|
||||
|
||||
private int targetPipe; //The pipe to taget. Leave as -1 for any available pipe.
|
||||
|
||||
private readonly object l_rtqueue = new object(); //Lock for the send queue
|
||||
private readonly uint _maxRtQueueSize;
|
||||
private Queue<ICommand> _rtqueue; //The send queue
|
||||
|
||||
private readonly object l_rxqueue = new object(); //Lock for the receive queue
|
||||
private readonly uint _maxRxQueueSize; //The max size of the RX queue
|
||||
private Queue<IMessage> _rxqueue; //The receive queue
|
||||
|
||||
private AutoResetEvent queueUpdatedEvent = new AutoResetEvent(false);
|
||||
private BackoffDelay delay; //The backoff delay before reconnecting.
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the RPC.
|
||||
/// </summary>
|
||||
/// <param name="applicationID">The ID of the Discord App</param>
|
||||
/// <param name="processID">The ID of the currently running process</param>
|
||||
/// <param name="targetPipe">The target pipe to connect too</param>
|
||||
/// <param name="client">The pipe client we shall use.</param>
|
||||
/// <param name="maxRxQueueSize">The maximum size of the out queue</param>
|
||||
/// <param name="maxRtQueueSize">The maximum size of the in queue</param>
|
||||
public RpcConnection(string applicationID, int processID, int targetPipe, INamedPipeClient client, uint maxRxQueueSize = 128, uint maxRtQueueSize = 512)
|
||||
{
|
||||
this.applicationID = applicationID;
|
||||
this.processID = processID;
|
||||
this.targetPipe = targetPipe;
|
||||
this.namedPipe = client;
|
||||
this.ShutdownOnly = true;
|
||||
|
||||
//Assign a default logger
|
||||
Logger = new ConsoleLogger();
|
||||
|
||||
delay = new BackoffDelay(500, 60 * 1000);
|
||||
_maxRtQueueSize = maxRtQueueSize;
|
||||
_rtqueue = new Queue<ICommand>((int)_maxRtQueueSize + 1);
|
||||
|
||||
_maxRxQueueSize = maxRxQueueSize;
|
||||
_rxqueue = new Queue<IMessage>((int)_maxRxQueueSize + 1);
|
||||
|
||||
nonce = 0;
|
||||
}
|
||||
|
||||
|
||||
private long GetNextNonce()
|
||||
{
|
||||
nonce += 1;
|
||||
return nonce;
|
||||
}
|
||||
|
||||
#region Queues
|
||||
/// <summary>
|
||||
/// Enqueues a command
|
||||
/// </summary>
|
||||
/// <param name="command">The command to enqueue</param>
|
||||
internal void EnqueueCommand(ICommand command)
|
||||
{
|
||||
Logger.Trace("Enqueue Command: " + command.GetType().FullName);
|
||||
|
||||
//We cannot add anything else if we are aborting or shutting down.
|
||||
if (aborting || shutdown) return;
|
||||
|
||||
//Enqueue the set presence argument
|
||||
lock (l_rtqueue)
|
||||
{
|
||||
//If we are too big drop the last element
|
||||
if (_rtqueue.Count == _maxRtQueueSize)
|
||||
{
|
||||
Logger.Error("Too many enqueued commands, dropping oldest one. Maybe you are pushing new presences to fast?");
|
||||
_rtqueue.Dequeue();
|
||||
}
|
||||
|
||||
//Enqueue the message
|
||||
_rtqueue.Enqueue(command);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a message to the message queue. Does not copy the message, so besure to copy it yourself or dereference it.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to add</param>
|
||||
private void EnqueueMessage(IMessage message)
|
||||
{
|
||||
//Invoke the message
|
||||
try
|
||||
{
|
||||
if (OnRpcMessage != null)
|
||||
OnRpcMessage.Invoke(this, message);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error("Unhandled Exception while processing event: {0}", e.GetType().FullName);
|
||||
Logger.Error(e.Message);
|
||||
Logger.Error(e.StackTrace);
|
||||
}
|
||||
|
||||
//Small queue sizes should just ignore messages
|
||||
if (_maxRxQueueSize <= 0)
|
||||
{
|
||||
Logger.Trace("Enqueued Message, but queue size is 0.");
|
||||
return;
|
||||
}
|
||||
|
||||
//Large queue sizes should keep the queue in check
|
||||
Logger.Trace("Enqueue Message: " + message.Type);
|
||||
lock (l_rxqueue)
|
||||
{
|
||||
//If we are too big drop the last element
|
||||
if (_rxqueue.Count == _maxRxQueueSize)
|
||||
{
|
||||
Logger.Warning("Too many enqueued messages, dropping oldest one.");
|
||||
_rxqueue.Dequeue();
|
||||
}
|
||||
|
||||
//Enqueue the message
|
||||
_rxqueue.Enqueue(message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dequeues a single message from the event stack. Returns null if none are available.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
internal IMessage DequeueMessage()
|
||||
{
|
||||
//Logger.Trace("Deque Message");
|
||||
lock (l_rxqueue)
|
||||
{
|
||||
//We have nothing, so just return null.
|
||||
if (_rxqueue.Count == 0) return null;
|
||||
|
||||
//Get the value and remove it from the list at the same time
|
||||
return _rxqueue.Dequeue();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dequeues all messages from the event stack.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
internal IMessage[] DequeueMessages()
|
||||
{
|
||||
//Logger.Trace("Deque Multiple Messages");
|
||||
lock (l_rxqueue)
|
||||
{
|
||||
//Copy the messages into an array
|
||||
IMessage[] messages = _rxqueue.ToArray();
|
||||
|
||||
//Clear the entire queue
|
||||
_rxqueue.Clear();
|
||||
|
||||
//return the array
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Main thread loop
|
||||
/// </summary>
|
||||
private void MainLoop()
|
||||
{
|
||||
//initialize the pipe
|
||||
Logger.Info("RPC Connection Started");
|
||||
if (Logger.Level <= LogLevel.Trace)
|
||||
{
|
||||
Logger.Trace("============================");
|
||||
Logger.Trace("Assembly: " + System.Reflection.Assembly.GetAssembly(typeof(RichPresence)).FullName);
|
||||
Logger.Trace("Pipe: " + namedPipe.GetType().FullName);
|
||||
Logger.Trace("Platform: " + Environment.OSVersion.ToString());
|
||||
Logger.Trace("applicationID: " + applicationID);
|
||||
Logger.Trace("targetPipe: " + targetPipe);
|
||||
Logger.Trace("POLL_RATE: " + POLL_RATE);
|
||||
Logger.Trace("_maxRtQueueSize: " + _maxRtQueueSize);
|
||||
Logger.Trace("_maxRxQueueSize: " + _maxRxQueueSize);
|
||||
Logger.Trace("============================");
|
||||
}
|
||||
|
||||
//Forever trying to connect unless the abort signal is sent
|
||||
//Keep Alive Loop
|
||||
while (!aborting && !shutdown)
|
||||
{
|
||||
try
|
||||
{
|
||||
//Wrap everything up in a try get
|
||||
//Dispose of the pipe if we have any (could be broken)
|
||||
if (namedPipe == null)
|
||||
{
|
||||
Logger.Error("Something bad has happened with our pipe client!");
|
||||
aborting = true;
|
||||
return;
|
||||
}
|
||||
|
||||
//Connect to a new pipe
|
||||
Logger.Trace("Connecting to the pipe through the {0}", namedPipe.GetType().FullName);
|
||||
if (namedPipe.Connect(targetPipe))
|
||||
{
|
||||
#region Connected
|
||||
//We connected to a pipe! Reset the delay
|
||||
Logger.Trace("Connected to the pipe. Attempting to establish handshake...");
|
||||
EnqueueMessage(new ConnectionEstablishedMessage() { ConnectedPipe = namedPipe.ConnectedPipe });
|
||||
|
||||
//Attempt to establish a handshake
|
||||
EstablishHandshake();
|
||||
Logger.Trace("Connection Established. Starting reading loop...");
|
||||
|
||||
//Continously iterate, waiting for the frame
|
||||
//We want to only stop reading if the inside tells us (mainloop), if we are aborting (abort) or the pipe disconnects
|
||||
// We dont want to exit on a shutdown, as we still have information
|
||||
PipeFrame frame;
|
||||
bool mainloop = true;
|
||||
while (mainloop && !aborting && !shutdown && namedPipe.IsConnected)
|
||||
{
|
||||
#region Read Loop
|
||||
|
||||
//Iterate over every frame we have queued up, processing its contents
|
||||
if (namedPipe.ReadFrame(out frame))
|
||||
{
|
||||
#region Read Payload
|
||||
Logger.Trace("Read Payload: {0}", frame.Opcode);
|
||||
|
||||
//Do some basic processing on the frame
|
||||
switch (frame.Opcode)
|
||||
{
|
||||
//We have been told by discord to close, so we will consider it an abort
|
||||
case Opcode.Close:
|
||||
|
||||
ClosePayload close = frame.GetObject<ClosePayload>();
|
||||
Logger.Warning("We have been told to terminate by discord: ({0}) {1}", close.Code, close.Reason);
|
||||
EnqueueMessage(new CloseMessage() { Code = close.Code, Reason = close.Reason });
|
||||
mainloop = false;
|
||||
break;
|
||||
|
||||
//We have pinged, so we will flip it and respond back with pong
|
||||
case Opcode.Ping:
|
||||
Logger.Trace("PING");
|
||||
frame.Opcode = Opcode.Pong;
|
||||
namedPipe.WriteFrame(frame);
|
||||
break;
|
||||
|
||||
//We have ponged? I have no idea if Discord actually sends ping/pongs.
|
||||
case Opcode.Pong:
|
||||
Logger.Trace("PONG");
|
||||
break;
|
||||
|
||||
//A frame has been sent, we should deal with that
|
||||
case Opcode.Frame:
|
||||
if (shutdown)
|
||||
{
|
||||
//We are shutting down, so skip it
|
||||
Logger.Warning("Skipping frame because we are shutting down.");
|
||||
break;
|
||||
}
|
||||
|
||||
if (frame.Data == null)
|
||||
{
|
||||
//We have invalid data, thats not good.
|
||||
Logger.Error("We received no data from the frame so we cannot get the event payload!");
|
||||
break;
|
||||
}
|
||||
|
||||
//We have a frame, so we are going to process the payload and add it to the stack
|
||||
EventPayload response = null;
|
||||
try { response = frame.GetObject<EventPayload>(); } catch (Exception e)
|
||||
{
|
||||
Logger.Error("Failed to parse event! " + e.Message);
|
||||
Logger.Error("Data: " + frame.Message);
|
||||
}
|
||||
|
||||
if (response != null) ProcessFrame(response);
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
case Opcode.Handshake:
|
||||
//We have a invalid opcode, better terminate to be safe
|
||||
Logger.Error("Invalid opcode: {0}", frame.Opcode);
|
||||
mainloop = false;
|
||||
break;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
if (!aborting && namedPipe.IsConnected)
|
||||
{
|
||||
//Process the entire command queue we have left
|
||||
ProcessCommandQueue();
|
||||
|
||||
//Wait for some time, or until a command has been queued up
|
||||
queueUpdatedEvent.WaitOne(POLL_RATE);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
#endregion
|
||||
|
||||
Logger.Trace("Left main read loop for some reason. Aborting: {0}, Shutting Down: {1}", aborting, shutdown);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Error("Failed to connect for some reason.");
|
||||
EnqueueMessage(new ConnectionFailedMessage() { FailedPipe = targetPipe });
|
||||
}
|
||||
|
||||
//If we are not aborting, we have to wait a bit before trying to connect again
|
||||
if (!aborting && !shutdown)
|
||||
{
|
||||
//We have disconnected for some reason, either a failed pipe or a bad reading,
|
||||
// so we are going to wait a bit before doing it again
|
||||
long sleep = delay.NextDelay();
|
||||
|
||||
Logger.Trace("Waiting {0}ms before attempting to connect again", sleep);
|
||||
Thread.Sleep(delay.NextDelay());
|
||||
}
|
||||
}
|
||||
//catch(InvalidPipeException e)
|
||||
//{
|
||||
// Logger.Error("Invalid Pipe Exception: {0}", e.Message);
|
||||
//}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error("Unhandled Exception: {0}", e.GetType().FullName);
|
||||
Logger.Error(e.Message);
|
||||
Logger.Error(e.StackTrace);
|
||||
}
|
||||
finally
|
||||
{
|
||||
//Disconnect from the pipe because something bad has happened. An exception has been thrown or the main read loop has terminated.
|
||||
if (namedPipe.IsConnected)
|
||||
{
|
||||
//Terminate the pipe
|
||||
Logger.Trace("Closing the named pipe.");
|
||||
namedPipe.Close();
|
||||
}
|
||||
|
||||
//Update our state
|
||||
SetConnectionState(RpcState.Disconnected);
|
||||
}
|
||||
}
|
||||
|
||||
//We have disconnected, so dispose of the thread and the pipe.
|
||||
Logger.Trace("Left Main Loop");
|
||||
if (namedPipe != null)
|
||||
namedPipe.Dispose();
|
||||
|
||||
Logger.Info("Thread Terminated, no longer performing RPC connection.");
|
||||
}
|
||||
|
||||
#region Reading
|
||||
|
||||
/// <summary>Handles the response from the pipe and calls appropriate events and changes states.</summary>
|
||||
/// <param name="response">The response received by the server.</param>
|
||||
private void ProcessFrame(EventPayload response)
|
||||
{
|
||||
Logger.Info("Handling Response. Cmd: {0}, Event: {1}", response.Command, response.Event);
|
||||
|
||||
//Check if it is an error
|
||||
if (response.Event.HasValue && response.Event.Value == ServerEvent.Error)
|
||||
{
|
||||
//We have an error
|
||||
Logger.Error("Error received from the RPC");
|
||||
|
||||
//Create the event objetc and push it to the queue
|
||||
ErrorMessage err = response.GetObject<ErrorMessage>();
|
||||
Logger.Error("Server responded with an error message: ({0}) {1}", err.Code.ToString(), err.Message);
|
||||
|
||||
//Enqueue the messsage and then end
|
||||
EnqueueMessage(err);
|
||||
return;
|
||||
}
|
||||
|
||||
//Check if its a handshake
|
||||
if (State == RpcState.Connecting)
|
||||
{
|
||||
if (response.Command == Command.Dispatch && response.Event.HasValue && response.Event.Value == ServerEvent.Ready)
|
||||
{
|
||||
Logger.Info("Connection established with the RPC");
|
||||
SetConnectionState(RpcState.Connected);
|
||||
delay.Reset();
|
||||
|
||||
//Prepare the object
|
||||
ReadyMessage ready = response.GetObject<ReadyMessage>();
|
||||
lock (l_config)
|
||||
{
|
||||
_configuration = ready.Configuration;
|
||||
ready.User.SetConfiguration(_configuration);
|
||||
}
|
||||
|
||||
//Enqueue the message
|
||||
EnqueueMessage(ready);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (State == RpcState.Connected)
|
||||
{
|
||||
switch(response.Command)
|
||||
{
|
||||
//We were sent a dispatch, better process it
|
||||
case Command.Dispatch:
|
||||
ProcessDispatch(response);
|
||||
break;
|
||||
|
||||
//We were sent a Activity Update, better enqueue it
|
||||
case Command.SetActivity:
|
||||
if (response.Data == null)
|
||||
{
|
||||
EnqueueMessage(new PresenceMessage());
|
||||
}
|
||||
else
|
||||
{
|
||||
RichPresenceResponse rp = response.GetObject<RichPresenceResponse>();
|
||||
EnqueueMessage(new PresenceMessage(rp));
|
||||
}
|
||||
break;
|
||||
|
||||
case Command.Unsubscribe:
|
||||
case Command.Subscribe:
|
||||
|
||||
//Prepare a serializer that can account for snake_case enums.
|
||||
JsonSerializer serializer = new JsonSerializer();
|
||||
serializer.Converters.Add(new Converters.EnumSnakeCaseConverter());
|
||||
|
||||
//Go through the data, looking for the evt property, casting it to a server event
|
||||
var evt = response.GetObject<EventPayload>().Event.Value;
|
||||
|
||||
//Enqueue the appropriate message.
|
||||
if (response.Command == Command.Subscribe)
|
||||
EnqueueMessage(new SubscribeMessage(evt));
|
||||
else
|
||||
EnqueueMessage(new UnsubscribeMessage(evt));
|
||||
|
||||
break;
|
||||
|
||||
|
||||
case Command.SendActivityJoinInvite:
|
||||
Logger.Trace("Got invite response ack.");
|
||||
break;
|
||||
|
||||
case Command.CloseActivityJoinRequest:
|
||||
Logger.Trace("Got invite response reject ack.");
|
||||
break;
|
||||
|
||||
//we have no idea what we were sent
|
||||
default:
|
||||
Logger.Error("Unkown frame was received! {0}", response.Command);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.Trace("Received a frame while we are disconnected. Ignoring. Cmd: {0}, Event: {1}", response.Command, response.Event);
|
||||
}
|
||||
|
||||
private void ProcessDispatch(EventPayload response)
|
||||
{
|
||||
if (response.Command != Command.Dispatch) return;
|
||||
if (!response.Event.HasValue) return;
|
||||
|
||||
switch(response.Event.Value)
|
||||
{
|
||||
//We are to join the server
|
||||
case ServerEvent.ActivitySpectate:
|
||||
var spectate = response.GetObject<SpectateMessage>();
|
||||
EnqueueMessage(spectate);
|
||||
break;
|
||||
|
||||
case ServerEvent.ActivityJoin:
|
||||
var join = response.GetObject<JoinMessage>();
|
||||
EnqueueMessage(join);
|
||||
break;
|
||||
|
||||
case ServerEvent.ActivityJoinRequest:
|
||||
var request = response.GetObject<JoinRequestMessage>();
|
||||
EnqueueMessage(request);
|
||||
break;
|
||||
|
||||
//Unkown dispatch event received. We should just ignore it.
|
||||
default:
|
||||
Logger.Warning("Ignoring {0}", response.Event.Value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Writting
|
||||
|
||||
private void ProcessCommandQueue()
|
||||
{
|
||||
//Logger.Info("Checking command queue");
|
||||
|
||||
//We are not ready yet, dont even try
|
||||
if (State != RpcState.Connected)
|
||||
return;
|
||||
|
||||
//We are aborting, so we will just log a warning so we know this is probably only going to send the CLOSE
|
||||
if (aborting)
|
||||
Logger.Warning("We have been told to write a queue but we have also been aborted.");
|
||||
|
||||
//Prepare some variabels we will clone into with locks
|
||||
bool needsWriting = true;
|
||||
ICommand item = null;
|
||||
|
||||
//Continue looping until we dont need anymore messages
|
||||
while (needsWriting && namedPipe.IsConnected)
|
||||
{
|
||||
lock (l_rtqueue)
|
||||
{
|
||||
//Pull the value and update our writing needs
|
||||
// If we have nothing to write, exit the loop
|
||||
needsWriting = _rtqueue.Count > 0;
|
||||
if (!needsWriting) break;
|
||||
|
||||
//Peek at the item
|
||||
item = _rtqueue.Peek();
|
||||
}
|
||||
|
||||
//BReak out of the loop as soon as we send this item
|
||||
if (shutdown || (!aborting && LOCK_STEP))
|
||||
needsWriting = false;
|
||||
|
||||
//Prepare the payload
|
||||
IPayload payload = item.PreparePayload(GetNextNonce());
|
||||
Logger.Trace("Attempting to send payload: " + payload.Command);
|
||||
|
||||
//Prepare the frame
|
||||
PipeFrame frame = new PipeFrame();
|
||||
if (item is CloseCommand)
|
||||
{
|
||||
//We have been sent a close frame. We better just send a handwave
|
||||
//Send it off to the server
|
||||
SendHandwave();
|
||||
|
||||
//Queue the item
|
||||
Logger.Trace("Handwave sent, ending queue processing.");
|
||||
lock (l_rtqueue) _rtqueue.Dequeue();
|
||||
|
||||
//Stop sending any more messages
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (aborting)
|
||||
{
|
||||
//We are aborting, so just dequeue the message and dont bother sending it
|
||||
Logger.Warning("- skipping frame because of abort.");
|
||||
lock (l_rtqueue) _rtqueue.Dequeue();
|
||||
}
|
||||
else
|
||||
{
|
||||
//Prepare the frame
|
||||
frame.SetObject(Opcode.Frame, payload);
|
||||
|
||||
//Write it and if it wrote perfectly fine, we will dequeue it
|
||||
Logger.Trace("Sending payload: " + payload.Command);
|
||||
if (namedPipe.WriteFrame(frame))
|
||||
{
|
||||
//We sent it, so now dequeue it
|
||||
Logger.Trace("Sent Successfully.");
|
||||
lock (l_rtqueue) _rtqueue.Dequeue();
|
||||
}
|
||||
else
|
||||
{
|
||||
//Something went wrong, so just giveup and wait for the next time around.
|
||||
Logger.Warning("Something went wrong during writing!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Connection
|
||||
|
||||
/// <summary>
|
||||
/// Establishes the handshake with the server.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private void EstablishHandshake()
|
||||
{
|
||||
Logger.Trace("Attempting to establish a handshake...");
|
||||
|
||||
//We are establishing a lock and not releasing it until we sent the handshake message.
|
||||
// We need to set the key, and it would not be nice if someone did things between us setting the key.
|
||||
|
||||
//Check its state
|
||||
if (State != RpcState.Disconnected)
|
||||
{
|
||||
Logger.Error("State must be disconnected in order to start a handshake!");
|
||||
return;
|
||||
}
|
||||
|
||||
//Send it off to the server
|
||||
Logger.Trace("Sending Handshake...");
|
||||
if (!namedPipe.WriteFrame(new PipeFrame(Opcode.Handshake, new Handshake() { Version = VERSION, ClientID = applicationID })))
|
||||
{
|
||||
Logger.Error("Failed to write a handshake.");
|
||||
return;
|
||||
}
|
||||
|
||||
//This has to be done outside the lock
|
||||
SetConnectionState(RpcState.Connecting);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Establishes a fairwell with the server by sending a handwave.
|
||||
/// </summary>
|
||||
private void SendHandwave()
|
||||
{
|
||||
Logger.Info("Attempting to wave goodbye...");
|
||||
|
||||
//Check its state
|
||||
if (State == RpcState.Disconnected)
|
||||
{
|
||||
Logger.Error("State must NOT be disconnected in order to send a handwave!");
|
||||
return;
|
||||
}
|
||||
|
||||
//Send the handwave
|
||||
if (!namedPipe.WriteFrame(new PipeFrame(Opcode.Close, new Handshake() { Version = VERSION, ClientID = applicationID })))
|
||||
{
|
||||
Logger.Error("failed to write a handwave.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to connect to the pipe. Returns true on success
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool AttemptConnection()
|
||||
{
|
||||
Logger.Info("Attempting a new connection");
|
||||
|
||||
//The thread mustn't exist already
|
||||
if (thread != null)
|
||||
{
|
||||
Logger.Error("Cannot attempt a new connection as the previous connection thread is not null!");
|
||||
return false;
|
||||
}
|
||||
|
||||
//We have to be in the disconnected state
|
||||
if (State != RpcState.Disconnected)
|
||||
{
|
||||
Logger.Warning("Cannot attempt a new connection as the previous connection hasn't changed state yet.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (aborting)
|
||||
{
|
||||
Logger.Error("Cannot attempt a new connection while aborting!");
|
||||
return false;
|
||||
}
|
||||
|
||||
//Start the thread up
|
||||
thread = new Thread(MainLoop);
|
||||
thread.Name = "Discord IPC Thread";
|
||||
thread.IsBackground = true;
|
||||
thread.Start();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the current state of the pipe, locking the l_states object for thread saftey.
|
||||
/// </summary>
|
||||
/// <param name="state">The state to set it too.</param>
|
||||
private void SetConnectionState(RpcState state)
|
||||
{
|
||||
Logger.Trace("Setting the connection state to {0}", state.ToString().ToSnakeCase().ToUpperInvariant());
|
||||
lock (l_states)
|
||||
{
|
||||
_state = state;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the connection and disposes of resources. This will not force termination, but instead allow Discord disconnect us after we say goodbye.
|
||||
/// <para>This option helps prevents ghosting in applications where the Process ID is a host and the game is executed within the host (ie: the Unity3D editor). This will tell Discord that we have no presence and we are closing the connection manually, instead of waiting for the process to terminate.</para>
|
||||
/// </summary>
|
||||
public void Shutdown()
|
||||
{
|
||||
//Enable the flag
|
||||
Logger.Trace("Initiated shutdown procedure");
|
||||
shutdown = true;
|
||||
|
||||
//Clear the commands and enqueue the close
|
||||
lock(l_rtqueue)
|
||||
{
|
||||
_rtqueue.Clear();
|
||||
if (CLEAR_ON_SHUTDOWN) _rtqueue.Enqueue(new PresenceCommand() { PID = processID, Presence = null });
|
||||
_rtqueue.Enqueue(new CloseCommand());
|
||||
}
|
||||
|
||||
//Trigger the event
|
||||
queueUpdatedEvent.Set();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the connection and disposes of resources.
|
||||
/// </summary>
|
||||
public void Close()
|
||||
{
|
||||
if (thread == null)
|
||||
{
|
||||
Logger.Error("Cannot close as it is not available!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (aborting)
|
||||
{
|
||||
Logger.Error("Cannot abort as it has already been aborted");
|
||||
return;
|
||||
}
|
||||
|
||||
//Set the abort state
|
||||
if (ShutdownOnly)
|
||||
{
|
||||
Shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
//Terminate
|
||||
Logger.Trace("Updating Abort State...");
|
||||
aborting = true;
|
||||
queueUpdatedEvent.Set();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Closes the connection and disposes resources. Identical to <see cref="Close"/> but ignores the "ShutdownOnly" value.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
ShutdownOnly = false;
|
||||
Close();
|
||||
}
|
||||
#endregion
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// State of the RPC connection
|
||||
/// </summary>
|
||||
internal enum RpcState
|
||||
{
|
||||
/// <summary>
|
||||
/// Disconnected from the discord client
|
||||
/// </summary>
|
||||
Disconnected,
|
||||
|
||||
/// <summary>
|
||||
/// Connecting to the discord client. The handshake has been sent and we are awaiting the ready event
|
||||
/// </summary>
|
||||
Connecting,
|
||||
|
||||
/// <summary>
|
||||
/// We are connect to the client and can send and receive messages.
|
||||
/// </summary>
|
||||
Connected
|
||||
}
|
||||
}
|
14
DiscordAPI/Registry/IUriSchemeCreator.cs
Normal file
14
DiscordAPI/Registry/IUriSchemeCreator.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using DiscordRPC.Logging;
|
||||
|
||||
namespace DiscordRPC.Registry
|
||||
{
|
||||
internal interface IUriSchemeCreator
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers the URI scheme. If Steam ID is passed, the application will be launched through steam instead of directly.
|
||||
/// <para>Additional arguments can be supplied if required.</para>
|
||||
/// </summary>
|
||||
/// <param name="register">The register context.</param>
|
||||
bool RegisterUriScheme(UriSchemeRegister register);
|
||||
}
|
||||
}
|
54
DiscordAPI/Registry/MacUriSchemeCreator.cs
Normal file
54
DiscordAPI/Registry/MacUriSchemeCreator.cs
Normal file
@ -0,0 +1,54 @@
|
||||
using DiscordRPC.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Registry
|
||||
{
|
||||
internal class MacUriSchemeCreator : IUriSchemeCreator
|
||||
{
|
||||
private ILogger logger;
|
||||
public MacUriSchemeCreator(ILogger logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public bool RegisterUriScheme(UriSchemeRegister register)
|
||||
{
|
||||
//var home = Environment.GetEnvironmentVariable("HOME");
|
||||
//if (string.IsNullOrEmpty(home)) return; //TODO: Log Error
|
||||
|
||||
string exe = register.ExecutablePath;
|
||||
if (string.IsNullOrEmpty(exe))
|
||||
{
|
||||
logger.Error("Failed to register because the application could not be located.");
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.Trace("Registering Steam Command");
|
||||
|
||||
//Prepare the command
|
||||
string command = exe;
|
||||
if (register.UsingSteamApp) command = "steam://rungameid/" + register.SteamAppID;
|
||||
else logger.Warning("This library does not fully support MacOS URI Scheme Registration.");
|
||||
|
||||
//get the folder ready
|
||||
string filepath = "~/Library/Application Support/discord/games";
|
||||
var directory = Directory.CreateDirectory(filepath);
|
||||
if (!directory.Exists)
|
||||
{
|
||||
logger.Error("Failed to register because {0} does not exist", filepath);
|
||||
return false;
|
||||
}
|
||||
|
||||
//Write the contents to file
|
||||
File.WriteAllText(filepath + "/" + register.ApplicationID + ".json", "{ \"command\": \"" + command + "\" }");
|
||||
logger.Trace("Registered {0}, {1}", filepath + "/" + register.ApplicationID + ".json", command);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
99
DiscordAPI/Registry/UnixUriSchemeCreator.cs
Normal file
99
DiscordAPI/Registry/UnixUriSchemeCreator.cs
Normal file
@ -0,0 +1,99 @@
|
||||
using DiscordRPC.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC.Registry
|
||||
{
|
||||
internal class UnixUriSchemeCreator : IUriSchemeCreator
|
||||
{
|
||||
private ILogger logger;
|
||||
public UnixUriSchemeCreator(ILogger logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public bool RegisterUriScheme(UriSchemeRegister register)
|
||||
{
|
||||
var home = Environment.GetEnvironmentVariable("HOME");
|
||||
if (string.IsNullOrEmpty(home))
|
||||
{
|
||||
logger.Error("Failed to register because the HOME variable was not set.");
|
||||
return false;
|
||||
}
|
||||
|
||||
string exe = register.ExecutablePath;
|
||||
if (string.IsNullOrEmpty(exe))
|
||||
{
|
||||
logger.Error("Failed to register because the application was not located.");
|
||||
return false;
|
||||
}
|
||||
|
||||
//Prepare the command
|
||||
string command = null;
|
||||
if (register.UsingSteamApp)
|
||||
{
|
||||
//A steam command isntead
|
||||
command = "xdg-open steam://rungameid/" + register.SteamAppID;
|
||||
}
|
||||
else
|
||||
{
|
||||
//Just a regular discord command
|
||||
command = exe;
|
||||
}
|
||||
|
||||
|
||||
//Prepare the file
|
||||
string desktopFileFormat =
|
||||
@"[Desktop Entry]
|
||||
Name=Game {0}
|
||||
Exec={1} %u
|
||||
Type=Application
|
||||
NoDisplay=true
|
||||
Categories=Discord;Games;
|
||||
MimeType=x-scheme-handler/discord-{2}";
|
||||
|
||||
string file = string.Format(desktopFileFormat, register.ApplicationID, command, register.ApplicationID);
|
||||
|
||||
//Prepare the path
|
||||
string filename = "/discord-" + register.ApplicationID + ".desktop";
|
||||
string filepath = home + "/.local/share/applications";
|
||||
var directory = Directory.CreateDirectory(filepath);
|
||||
if (!directory.Exists)
|
||||
{
|
||||
logger.Error("Failed to register because {0} does not exist", filepath);
|
||||
return false;
|
||||
}
|
||||
|
||||
//Write the file
|
||||
File.WriteAllText(filepath + filename, file);
|
||||
|
||||
//Register the Mime type
|
||||
if (!RegisterMime(register.ApplicationID))
|
||||
{
|
||||
logger.Error("Failed to register because the Mime failed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.Trace("Registered {0}, {1}, {2}", filepath + filename, file, command);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool RegisterMime(string appid)
|
||||
{
|
||||
//Format the arguments
|
||||
string format = "default discord-{0}.desktop x-scheme-handler/discord-{0}";
|
||||
string arguments = string.Format(format, appid);
|
||||
|
||||
//Run the process and wait for response
|
||||
Process process = Process.Start("xdg-mime", arguments);
|
||||
process.WaitForExit();
|
||||
|
||||
//Return if succesful
|
||||
return process.ExitCode >= 0;
|
||||
}
|
||||
}
|
||||
}
|
89
DiscordAPI/Registry/UriScheme.cs
Normal file
89
DiscordAPI/Registry/UriScheme.cs
Normal file
@ -0,0 +1,89 @@
|
||||
using DiscordRPC.Logging;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace DiscordRPC.Registry
|
||||
{
|
||||
internal class UriSchemeRegister
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the Discord App to register
|
||||
/// </summary>
|
||||
public string ApplicationID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional Steam App ID to register. If given a value, then the game will launch through steam instead of Discord.
|
||||
/// </summary>
|
||||
public string SteamAppID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is this register using steam?
|
||||
/// </summary>
|
||||
public bool UsingSteamApp { get { return !string.IsNullOrEmpty(SteamAppID) && SteamAppID != ""; } }
|
||||
|
||||
/// <summary>
|
||||
/// The full executable path of the application.
|
||||
/// </summary>
|
||||
public string ExecutablePath { get; set; }
|
||||
|
||||
private ILogger _logger;
|
||||
public UriSchemeRegister(ILogger logger, string applicationID, string steamAppID = null, string executable = null)
|
||||
{
|
||||
_logger = logger;
|
||||
ApplicationID = applicationID.Trim();
|
||||
SteamAppID = steamAppID != null ? steamAppID.Trim() : null;
|
||||
ExecutablePath = executable ?? GetApplicationLocation();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the URI scheme, using the correct creator for the correct platform
|
||||
/// </summary>
|
||||
public bool RegisterUriScheme()
|
||||
{
|
||||
//Get the creator
|
||||
IUriSchemeCreator creator = null;
|
||||
switch(Environment.OSVersion.Platform)
|
||||
{
|
||||
case PlatformID.Win32Windows:
|
||||
case PlatformID.Win32S:
|
||||
case PlatformID.Win32NT:
|
||||
case PlatformID.WinCE:
|
||||
_logger.Trace("Creating Windows Scheme Creator");
|
||||
creator = new WindowsUriSchemeCreator(_logger);
|
||||
break;
|
||||
|
||||
case PlatformID.Unix:
|
||||
_logger.Trace("Creating Unix Scheme Creator");
|
||||
creator = new UnixUriSchemeCreator(_logger);
|
||||
break;
|
||||
|
||||
case PlatformID.MacOSX:
|
||||
_logger.Trace("Creating MacOSX Scheme Creator");
|
||||
creator = new MacUriSchemeCreator(_logger);
|
||||
break;
|
||||
|
||||
default:
|
||||
_logger.Error("Unkown Platform: " + Environment.OSVersion.Platform);
|
||||
throw new PlatformNotSupportedException("Platform does not support registration.");
|
||||
}
|
||||
|
||||
//Regiser the app
|
||||
if (creator.RegisterUriScheme(this))
|
||||
{
|
||||
_logger.Info("URI scheme registered.");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the FileName for the currently executing application
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static string GetApplicationLocation()
|
||||
{
|
||||
return Process.GetCurrentProcess().MainModule.FileName;
|
||||
}
|
||||
}
|
||||
}
|
87
DiscordAPI/Registry/WindowsUriSchemeCreator.cs
Normal file
87
DiscordAPI/Registry/WindowsUriSchemeCreator.cs
Normal file
@ -0,0 +1,87 @@
|
||||
using DiscordRPC.Logging;
|
||||
using System;
|
||||
|
||||
namespace DiscordRPC.Registry
|
||||
{
|
||||
internal class WindowsUriSchemeCreator : IUriSchemeCreator
|
||||
{
|
||||
private ILogger logger;
|
||||
public WindowsUriSchemeCreator(ILogger logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public bool RegisterUriScheme(UriSchemeRegister register)
|
||||
{
|
||||
if (Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX)
|
||||
{
|
||||
throw new PlatformNotSupportedException("URI schemes can only be registered on Windows");
|
||||
}
|
||||
|
||||
//Prepare our location
|
||||
string location = register.ExecutablePath;
|
||||
if (location == null)
|
||||
{
|
||||
logger.Error("Failed to register application because the location was null.");
|
||||
return false;
|
||||
}
|
||||
|
||||
//Prepare the Scheme, Friendly name, default icon and default command
|
||||
string scheme = "discord-" + register.ApplicationID;
|
||||
string friendlyName = "Run game " + register.ApplicationID + " protocol";
|
||||
string defaultIcon = location;
|
||||
string command = location;
|
||||
|
||||
//We have a steam ID, so attempt to replce the command with a steam command
|
||||
if (register.UsingSteamApp)
|
||||
{
|
||||
//Try to get the steam location. If found, set the command to a run steam instead.
|
||||
string steam = GetSteamLocation();
|
||||
if (steam != null)
|
||||
command = string.Format("\"{0}\" steam://rungameid/{1}", steam, register.SteamAppID);
|
||||
|
||||
}
|
||||
|
||||
//Okay, now actually register it
|
||||
CreateUriScheme(scheme, friendlyName, defaultIcon, command);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the actual scheme
|
||||
/// </summary>
|
||||
/// <param name="scheme"></param>
|
||||
/// <param name="friendlyName"></param>
|
||||
/// <param name="defaultIcon"></param>
|
||||
/// <param name="command"></param>
|
||||
private void CreateUriScheme(string scheme, string friendlyName, string defaultIcon, string command)
|
||||
{
|
||||
using (var key = Microsoft.Win32.Registry.CurrentUser.CreateSubKey("SOFTWARE\\Classes\\" + scheme))
|
||||
{
|
||||
key.SetValue("", "URL:" + friendlyName);
|
||||
key.SetValue("URL Protocol", "");
|
||||
|
||||
using (var iconKey = key.CreateSubKey("DefaultIcon"))
|
||||
iconKey.SetValue("", defaultIcon);
|
||||
|
||||
using (var commandKey = key.CreateSubKey("shell\\open\\command"))
|
||||
commandKey.SetValue("", command);
|
||||
}
|
||||
|
||||
logger.Trace("Registered {0}, {1}, {2}", scheme, friendlyName, command);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current location of the steam client
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public string GetSteamLocation()
|
||||
{
|
||||
using (var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey("Software\\Valve\\Steam"))
|
||||
{
|
||||
if (key == null) return null;
|
||||
return key.GetValue("SteamExe") as string;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
836
DiscordAPI/RichPresence.cs
Normal file
836
DiscordAPI/RichPresence.cs
Normal file
@ -0,0 +1,836 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using DiscordRPC.Helper;
|
||||
using System.Text;
|
||||
using DiscordRPC.Exceptions;
|
||||
|
||||
namespace DiscordRPC
|
||||
{
|
||||
/// <summary>
|
||||
/// The Rich Presence structure that will be sent and received by Discord. Use this class to build your presence and update it appropriately.
|
||||
/// </summary>
|
||||
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
|
||||
[Serializable]
|
||||
public class RichPresence
|
||||
{
|
||||
/// <summary>
|
||||
/// The user's current <see cref="Party"/> status. For example, "Playing Solo" or "With Friends".
|
||||
/// <para>Max 128 bytes</para>
|
||||
/// </summary>
|
||||
[JsonProperty("state", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string State
|
||||
{
|
||||
get { return _state; }
|
||||
set
|
||||
{
|
||||
if (!ValidateString(value, out _state, 128, Encoding.UTF8))
|
||||
throw new StringOutOfRangeException("State", 0, 128);
|
||||
}
|
||||
}
|
||||
private string _state;
|
||||
|
||||
/// <summary>
|
||||
/// What the user is currently doing. For example, "Competitive - Total Mayhem".
|
||||
/// <para>Max 128 bytes</para>
|
||||
/// </summary>
|
||||
[JsonProperty("details", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string Details
|
||||
{
|
||||
get { return _details; }
|
||||
set
|
||||
{
|
||||
if (!ValidateString(value, out _details, 128, Encoding.UTF8))
|
||||
throw new StringOutOfRangeException(128);
|
||||
}
|
||||
}
|
||||
private string _details;
|
||||
|
||||
/// <summary>
|
||||
/// The time elapsed / remaining time data.
|
||||
/// </summary>
|
||||
[JsonProperty("timestamps", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public Timestamps Timestamps { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The names of the images to use and the tooltips to give those images.
|
||||
/// </summary>
|
||||
[JsonProperty("assets", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public Assets Assets { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The party the player is currently in. The <see cref="Party.ID"/> must be set for this to be included in the RichPresence update.
|
||||
/// </summary>
|
||||
[JsonProperty("party", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public Party Party { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The secrets used for Join / Spectate. Secrets are obfuscated data of your choosing. They could be match ids, player ids, lobby ids, etc. Make this object null if you do not wish too / unable too implement the Join / Request feature.
|
||||
/// <para>To keep security on the up and up, Discord requires that you properly hash/encode/encrypt/put-a-padlock-on-and-swallow-the-key-but-wait-then-how-would-you-open-it your secrets.</para>
|
||||
/// <para>Visit the <see href="https://discordapp.com/developers/docs/rich-presence/how-to#secrets">Rich Presence How-To</see> for more information.</para>
|
||||
/// </summary>
|
||||
[JsonProperty("secrets", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public Secrets Secrets { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Marks the <see cref="Secrets.MatchSecret"/> as a game session with a specific beginning and end. It was going to be used as a form of notification, but was replaced with the join feature. It may potentially have use in the future, but it currently has no use.
|
||||
/// <para>
|
||||
/// "TLDR it marks the matchSecret field as an instance, that is to say a context in game that’s not like a lobby state/not in game state. It was gonna he used for notify me, but we scrapped that for ask to join. We may put it to another use in the future. For now, don’t worry about it" - Mason (Discord API Server 14 / 03 / 2018)
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[JsonProperty("instance", NullValueHandling = NullValueHandling.Ignore)]
|
||||
[Obsolete("This was going to be used, but was replaced by JoinSecret instead")]
|
||||
private bool Instance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Clones the presence into a new instance. Used for thread safe writing and reading. This function will ignore properties if they are in a invalid state.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public RichPresence Clone()
|
||||
{
|
||||
return new RichPresence
|
||||
{
|
||||
State = this._state != null ? _state.Clone() as string : null,
|
||||
Details = this._details != null ? _details.Clone() as string : null,
|
||||
|
||||
Secrets = !HasSecrets() ? null : new Secrets
|
||||
{
|
||||
//MatchSecret = this.Secrets.MatchSecret?.Clone() as string,
|
||||
JoinSecret = this.Secrets.JoinSecret != null ? this.Secrets.JoinSecret.Clone() as string : null,
|
||||
SpectateSecret = this.Secrets.SpectateSecret != null ? this.Secrets.SpectateSecret.Clone() as string : null
|
||||
},
|
||||
|
||||
Timestamps = !HasTimestamps() ? null : new Timestamps
|
||||
{
|
||||
Start = this.Timestamps.Start,
|
||||
End = this.Timestamps.End
|
||||
},
|
||||
|
||||
Assets = !HasAssets() ? null : new Assets
|
||||
{
|
||||
LargeImageKey = this.Assets.LargeImageKey != null ? this.Assets.LargeImageKey.Clone() as string : null,
|
||||
LargeImageText = this.Assets.LargeImageText != null ? this.Assets.LargeImageText.Clone() as string : null,
|
||||
SmallImageKey = this.Assets.SmallImageKey != null ? this.Assets.SmallImageKey.Clone() as string : null,
|
||||
SmallImageText = this.Assets.SmallImageText != null ? this.Assets.SmallImageText.Clone() as string : null
|
||||
},
|
||||
|
||||
Party = !HasParty() ? null : new Party
|
||||
{
|
||||
ID = this.Party.ID,
|
||||
Size = this.Party.Size,
|
||||
Max = this.Party.Max
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges the passed presence with this one, taking into account the image key to image id annoyance.
|
||||
/// </summary>
|
||||
/// <param name="presence"></param>
|
||||
internal void Merge(RichPresence presence)
|
||||
{
|
||||
this._state = presence._state;
|
||||
this._details = presence._details;
|
||||
this.Party = presence.Party;
|
||||
this.Timestamps = presence.Timestamps;
|
||||
this.Secrets = presence.Secrets;
|
||||
|
||||
//If they have assets, we should merge them
|
||||
if (presence.HasAssets())
|
||||
{
|
||||
//Make sure we actually have assets too
|
||||
if (!this.HasAssets())
|
||||
{
|
||||
//We dont, so we will just use theirs
|
||||
this.Assets = presence.Assets;
|
||||
}
|
||||
else
|
||||
{
|
||||
//We do, so we better merge them!
|
||||
this.Assets.Merge(presence.Assets);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//They dont have assets, so we will just set ours to null
|
||||
this.Assets = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates this presence with any values from the previous one
|
||||
/// </summary>
|
||||
/// <param name="presence"></param>
|
||||
[System.Obsolete("No longer used and probably can be removed.")]
|
||||
internal void Update(RichPresence presence)
|
||||
{
|
||||
if (presence == null) return;
|
||||
|
||||
this._state = presence._state ?? this._state;
|
||||
this._details = presence._details ?? this._details;
|
||||
|
||||
if (presence.Party != null)
|
||||
{
|
||||
if (this.Party != null)
|
||||
{
|
||||
this.Party.ID = presence.Party.ID ?? this.Party.ID;
|
||||
this.Party.Size = presence.Party.Size;
|
||||
this.Party.Max = presence.Party.Max;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Party = presence.Party;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region Has Checks
|
||||
/// <summary>
|
||||
/// Does the Rich Presence have valid timestamps?
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool HasTimestamps()
|
||||
{
|
||||
return this.Timestamps != null && (Timestamps.Start != null || Timestamps.End != null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does the Rich Presence have valid assets?
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool HasAssets()
|
||||
{
|
||||
return this.Assets != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does the Rich Presence have a valid party?
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool HasParty()
|
||||
{
|
||||
return this.Party != null && this.Party.ID != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does the Rich Presence have valid secrets?
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool HasSecrets()
|
||||
{
|
||||
return Secrets != null && (Secrets.JoinSecret != null || Secrets.SpectateSecret != null);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Builder
|
||||
/// <summary>
|
||||
/// Sets the state of the Rich Presence. See also <seealso cref="State"/>.
|
||||
/// </summary>
|
||||
/// <param name="state">The user's current <see cref="Party"/> status.</param>
|
||||
/// <returns>The modified Rich Presence.</returns>
|
||||
public RichPresence WithState(string state)
|
||||
{
|
||||
State = state;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the details of the Rich Presence. See also <seealso cref="Details"/>.
|
||||
/// </summary>
|
||||
/// <param name="details">What the user is currently doing.</param>
|
||||
/// <returns>The modified Rich Presence.</returns>
|
||||
public RichPresence WithDetails(string details)
|
||||
{
|
||||
Details = details;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the timestamp of the Rich Presence. See also <seealso cref="Timestamps"/>.
|
||||
/// </summary>
|
||||
/// <param name="timestamps">The time elapsed / remaining time data.</param>
|
||||
/// <returns>The modified Rich Presence.</returns>
|
||||
public RichPresence WithTimestamps(Timestamps timestamps)
|
||||
{
|
||||
Timestamps = timestamps;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the assets of the Rich Presence. See also <seealso cref="Assets"/>.
|
||||
/// </summary>
|
||||
/// <param name="assets">The names of the images to use and the tooltips to give those images.</param>
|
||||
/// <returns>The modified Rich Presence.</returns>
|
||||
public RichPresence WithAssets(Assets assets)
|
||||
{
|
||||
Assets = assets;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Rich Presence's party. See also <seealso cref="Party"/>.
|
||||
/// </summary>
|
||||
/// <param name="party">The party the player is currently in.</param>
|
||||
/// <returns>The modified Rich Presence.</returns>
|
||||
public RichPresence WithParty(Party party)
|
||||
{
|
||||
Party = party;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Rich Presence's secrets. See also <seealso cref="Secrets"/>.
|
||||
/// </summary>
|
||||
/// <param name="secrets">The secrets used for Join / Spectate.</param>
|
||||
/// <returns>The modified Rich Presence.</returns>
|
||||
public RichPresence WithSecrets(Secrets secrets)
|
||||
{
|
||||
Secrets = secrets;
|
||||
return this;
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to call <see cref="StringTools.GetNullOrString(string)"/> on the string and return the result, if its within a valid length.
|
||||
/// </summary>
|
||||
/// <param name="str">The string to check</param>
|
||||
/// <param name="result">The formatted string result</param>
|
||||
/// <param name="bytes">The maximum number of bytes the string can take up</param>
|
||||
/// <param name="encoding">The encoding to count the bytes with</param>
|
||||
/// <returns>True if the string fits within the number of bytes</returns>
|
||||
internal static bool ValidateString(string str, out string result, int bytes, Encoding encoding)
|
||||
{
|
||||
result = str;
|
||||
if (str == null)
|
||||
return true;
|
||||
|
||||
//Trim the string, for the best chance of fitting
|
||||
var s = str.Trim();
|
||||
|
||||
//Make sure it fits
|
||||
if (!s.WithinLength(bytes, encoding))
|
||||
return false;
|
||||
|
||||
//Make sure its not empty
|
||||
result = s.GetNullOrString();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Operator that converts a presence into a boolean for null checks.
|
||||
/// </summary>
|
||||
/// <param name="presesnce"></param>
|
||||
public static implicit operator bool(RichPresence presesnce)
|
||||
{
|
||||
return presesnce != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the other rich presence differs from the current one
|
||||
/// </summary>
|
||||
/// <param name="other"></param>
|
||||
/// <returns></returns>
|
||||
internal bool Matches(RichPresence other)
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (other == null)
|
||||
return false;
|
||||
|
||||
if (State != other.State || Details != other.Details)
|
||||
return false;
|
||||
|
||||
//Checks if the timestamps are different
|
||||
if (Timestamps != null)
|
||||
{
|
||||
if (other.Timestamps == null ||
|
||||
other.Timestamps.StartUnixMilliseconds != Timestamps.StartUnixMilliseconds ||
|
||||
other.Timestamps.EndUnixMilliseconds != Timestamps.EndUnixMilliseconds)
|
||||
return false;
|
||||
}
|
||||
else if (other.Timestamps != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//Checks if the secrets are different
|
||||
if (Secrets != null)
|
||||
{
|
||||
if (other.Secrets == null ||
|
||||
other.Secrets.JoinSecret != Secrets.JoinSecret ||
|
||||
other.Secrets.MatchSecret != Secrets.MatchSecret ||
|
||||
other.Secrets.SpectateSecret != Secrets.SpectateSecret)
|
||||
return false;
|
||||
}
|
||||
else if (other.Secrets != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//Checks if the timestamps are different
|
||||
if (Party != null)
|
||||
{
|
||||
if (other.Party == null ||
|
||||
other.Party.ID != Party.ID ||
|
||||
other.Party.Max != Party.Max ||
|
||||
other.Party.Size != Party.Size)
|
||||
return false;
|
||||
}
|
||||
else if (other.Party != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//Checks if the assets are different
|
||||
if (Assets != null)
|
||||
{
|
||||
if (other.Assets == null ||
|
||||
other.Assets.LargeImageKey != Assets.LargeImageKey ||
|
||||
other.Assets.LargeImageText != Assets.LargeImageText ||
|
||||
other.Assets.SmallImageKey != Assets.SmallImageKey ||
|
||||
other.Assets.SmallImageText != Assets.SmallImageText)
|
||||
return false;
|
||||
}
|
||||
else if (other.Assets != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Instance == other.Instance;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The secrets used for Join / Spectate. Secrets are obfuscated data of your choosing. They could be match ids, player ids, lobby ids, etc.
|
||||
/// <para>To keep security on the up and up, Discord requires that you properly hash/encode/encrypt/put-a-padlock-on-and-swallow-the-key-but-wait-then-how-would-you-open-it your secrets.</para>
|
||||
/// <para>You should send discord data that someone else's game client would need to join or spectate their friend. If you can't or don't want to support those actions, you don't need to send secrets.</para>
|
||||
/// <para>Visit the <see href="https://discordapp.com/developers/docs/rich-presence/how-to#secrets">Rich Presence How-To</see> for more information.</para>
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class Secrets
|
||||
{
|
||||
/// <summary>
|
||||
/// The unique match code to distinguish different games/lobbies. Use <see cref="Secrets.CreateSecret(Random)"/> to get an appropriately sized secret.
|
||||
/// <para>This cannot be null and must be supplied for the Join / Spectate feature to work.</para>
|
||||
/// <para>Max Length of 128 Bytes</para>
|
||||
/// </summary>
|
||||
[Obsolete("This feature has been deprecated my Mason in issue #152 on the offical library. Was originally used as a Notify Me feature, it has been replaced with Join / Spectate.")]
|
||||
[JsonProperty("match", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string MatchSecret
|
||||
{
|
||||
get { return _matchSecret; }
|
||||
set
|
||||
{
|
||||
if (!RichPresence.ValidateString(value, out _matchSecret, 128, Encoding.UTF8))
|
||||
throw new StringOutOfRangeException(128);
|
||||
}
|
||||
}
|
||||
private string _matchSecret;
|
||||
|
||||
/// <summary>
|
||||
/// The secret data that will tell the client how to connect to the game to play. This could be a unique identifier for a fancy match maker or player id, lobby id, etc.
|
||||
/// <para>It is recommended to encrypt this information so its hard for people to replicate it.
|
||||
/// Do <b>NOT</b> just use the IP address in this. That is a bad practice and can leave your players vulnerable!
|
||||
/// </para>
|
||||
/// <para>Max Length of 128 Bytes</para>
|
||||
/// </summary>
|
||||
[JsonProperty("join", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string JoinSecret
|
||||
{
|
||||
get { return _joinSecret; }
|
||||
set
|
||||
{
|
||||
if (!RichPresence.ValidateString(value, out _joinSecret, 128, Encoding.UTF8))
|
||||
throw new StringOutOfRangeException(128);
|
||||
}
|
||||
}
|
||||
private string _joinSecret;
|
||||
|
||||
/// <summary>
|
||||
/// The secret data that will tell the client how to connect to the game to spectate. This could be a unique identifier for a fancy match maker or player id, lobby id, etc.
|
||||
/// <para>It is recommended to encrypt this information so its hard for people to replicate it.
|
||||
/// Do <b>NOT</b> just use the IP address in this. That is a bad practice and can leave your players vulnerable!
|
||||
/// </para>
|
||||
/// <para>Max Length of 128 Bytes</para>
|
||||
/// </summary>
|
||||
[JsonProperty("spectate", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string SpectateSecret
|
||||
{
|
||||
get { return _spectateSecret; }
|
||||
set
|
||||
{
|
||||
if (!RichPresence.ValidateString(value, out _spectateSecret, 128, Encoding.UTF8))
|
||||
throw new StringOutOfRangeException(128);
|
||||
}
|
||||
}
|
||||
private string _spectateSecret;
|
||||
|
||||
|
||||
#region Statics
|
||||
|
||||
/// <summary>
|
||||
/// The encoding the secret generator is using
|
||||
/// </summary>
|
||||
public static Encoding Encoding { get { return Encoding.UTF8; } }
|
||||
|
||||
/// <summary>
|
||||
/// The length of a secret in bytes.
|
||||
/// </summary>
|
||||
public static int SecretLength { get { return 128; } }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new secret. This is NOT a cryptographic function and should NOT be used for sensitive information. This is mainly provided as a way to generate quick IDs.
|
||||
/// </summary>
|
||||
/// <param name="random">The random to use</param>
|
||||
/// <returns>Returns a <see cref="SecretLength"/> sized string with random characters from <see cref="Encoding"/></returns>
|
||||
public static string CreateSecret(Random random)
|
||||
{
|
||||
//Prepare an array and fill it with random bytes
|
||||
// THIS IS NOT SECURE! DO NOT USE THIS FOR PASSWORDS!
|
||||
byte[] bytes = new byte[SecretLength];
|
||||
random.NextBytes(bytes);
|
||||
|
||||
//Return the encoding. Probably should remove invalid characters but cannot be fucked.
|
||||
return Encoding.GetString(bytes);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a secret word using more readable friendly characters. Useful for debugging purposes. This is not a cryptographic function and should NOT be used for sensitive information.
|
||||
/// </summary>
|
||||
/// <param name="random">The random used to generate the characters</param>
|
||||
/// <returns></returns>
|
||||
public static string CreateFriendlySecret(Random random)
|
||||
{
|
||||
string charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
string secret = "";
|
||||
|
||||
for (int i = 0; i < SecretLength; i++)
|
||||
secret += charset[random.Next(charset.Length)];
|
||||
|
||||
return secret;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Information about the pictures used in the Rich Presence.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class Assets
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the uploaded image for the large profile artwork.
|
||||
/// <para>Max 32 Bytes.</para>
|
||||
/// </summary>
|
||||
[JsonProperty("large_image", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string LargeImageKey
|
||||
{
|
||||
get { return _largeimagekey; }
|
||||
set
|
||||
{
|
||||
if (!RichPresence.ValidateString(value, out _largeimagekey, 32, Encoding.UTF8))
|
||||
throw new StringOutOfRangeException(32);
|
||||
|
||||
//Reset the large image ID
|
||||
_largeimageID = null;
|
||||
}
|
||||
}
|
||||
private string _largeimagekey;
|
||||
|
||||
/// <summary>
|
||||
/// The tooltip for the large square image. For example, "Summoners Rift" or "Horizon Lunar Colony".
|
||||
/// <para>Max 128 Bytes.</para>
|
||||
/// </summary>
|
||||
[JsonProperty("large_text", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string LargeImageText
|
||||
{
|
||||
get { return _largeimagetext; }
|
||||
set
|
||||
{
|
||||
if (!RichPresence.ValidateString(value, out _largeimagetext, 128, Encoding.UTF8))
|
||||
throw new StringOutOfRangeException(128);
|
||||
}
|
||||
}
|
||||
private string _largeimagetext;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Name of the uploaded image for the small profile artwork.
|
||||
/// <para>Max 32 Bytes.</para>
|
||||
/// </summary>
|
||||
[JsonProperty("small_image", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string SmallImageKey
|
||||
{
|
||||
get { return _smallimagekey; }
|
||||
set
|
||||
{
|
||||
if (!RichPresence.ValidateString(value, out _smallimagekey, 32, Encoding.UTF8))
|
||||
throw new StringOutOfRangeException(32);
|
||||
|
||||
//Reset the small image id
|
||||
_smallimageID = null;
|
||||
}
|
||||
}
|
||||
private string _smallimagekey;
|
||||
|
||||
/// <summary>
|
||||
/// The tooltip for the small circle image. For example, "LvL 6" or "Ultimate 85%".
|
||||
/// <para>Max 128 Bytes.</para>
|
||||
/// </summary>
|
||||
[JsonProperty("small_text", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string SmallImageText
|
||||
{
|
||||
get { return _smallimagetext; }
|
||||
set
|
||||
{
|
||||
if (!RichPresence.ValidateString(value, out _smallimagetext, 128, Encoding.UTF8))
|
||||
throw new StringOutOfRangeException(128);
|
||||
}
|
||||
}
|
||||
private string _smallimagetext;
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the large image. This is only set after Update Presence and will automatically become null when <see cref="LargeImageKey"/> is changed.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public ulong? LargeImageID { get { return _largeimageID; } }
|
||||
private ulong? _largeimageID;
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the small image. This is only set after Update Presence and will automatically become null when <see cref="SmallImageKey"/> is changed.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public ulong? SmallImageID { get { return _smallimageID; } }
|
||||
private ulong? _smallimageID;
|
||||
|
||||
/// <summary>
|
||||
/// Merges this asset with the other, taking into account for ID's instead of keys.
|
||||
/// </summary>
|
||||
/// <param name="other"></param>
|
||||
internal void Merge(Assets other)
|
||||
{
|
||||
//Copy over the names
|
||||
_smallimagetext = other._smallimagetext;
|
||||
_largeimagetext = other._largeimagetext;
|
||||
|
||||
//Convert large ID
|
||||
ulong largeID;
|
||||
if (ulong.TryParse(other._largeimagekey, out largeID))
|
||||
{
|
||||
_largeimageID = largeID;
|
||||
}
|
||||
else
|
||||
{
|
||||
_largeimagekey = other._largeimagekey;
|
||||
_largeimageID = null;
|
||||
}
|
||||
|
||||
//Convert the small ID
|
||||
ulong smallID;
|
||||
if (ulong.TryParse(other._smallimagekey, out smallID))
|
||||
{
|
||||
_smallimageID = smallID;
|
||||
}
|
||||
else
|
||||
{
|
||||
_smallimagekey = other._smallimagekey;
|
||||
_smallimageID = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Structure representing the start and endtimes of a match.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class Timestamps
|
||||
{
|
||||
/// <summary>A new timestamp that starts from the current time.</summary>
|
||||
public static Timestamps Now { get { return new Timestamps(DateTime.UtcNow, end: null); } }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new timestamp starting at the current time and ending in the supplied timespan
|
||||
/// </summary>
|
||||
/// <param name="seconds">How long the Timestamp will last for in seconds.</param>
|
||||
/// <returns>Returns a new timestamp with given duration.</returns>
|
||||
public static Timestamps FromTimeSpan(double seconds) { return FromTimeSpan(TimeSpan.FromSeconds(seconds)); }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new timestamp starting at current time and ending in the supplied timespan
|
||||
/// </summary>
|
||||
/// <param name="timespan">How long the Timestamp will last for.</param>
|
||||
/// <returns>Returns a new timestamp with given duration.</returns>
|
||||
public static Timestamps FromTimeSpan(TimeSpan timespan)
|
||||
{
|
||||
return new Timestamps()
|
||||
{
|
||||
Start = DateTime.UtcNow,
|
||||
End = DateTime.UtcNow + timespan
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The time that match started. When included (not-null), the time in the rich presence will be shown as "00:01 elapsed".
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public DateTime? Start { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time the match will end. When included (not-null), the time in the rich presence will be shown as "00:01 remaining". This will override the "elapsed" to "remaining".
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public DateTime? End { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a empty timestamp object
|
||||
/// </summary>
|
||||
public Timestamps()
|
||||
{
|
||||
Start = null;
|
||||
End = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a timestamp with the set start or end time.
|
||||
/// </summary>
|
||||
/// <param name="start">The start time</param>
|
||||
/// <param name="end">The end time</param>
|
||||
public Timestamps(DateTime start, DateTime? end = null)
|
||||
{
|
||||
Start = start;
|
||||
End = end;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts between DateTime and Milliseconds to give the Unix Epoch Time for the <see cref="Timestamps.Start"/>.
|
||||
/// </summary>
|
||||
[JsonProperty("start", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public ulong? StartUnixMilliseconds
|
||||
{
|
||||
get
|
||||
{
|
||||
return Start.HasValue ? ToUnixMilliseconds(Start.Value) : (ulong?)null;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
Start = value.HasValue ? FromUnixMilliseconds(value.Value) : (DateTime?)null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Converts between DateTime and Milliseconds to give the Unix Epoch Time for the <see cref="Timestamps.End"/>.
|
||||
/// <seealso cref="End"/>
|
||||
/// </summary>
|
||||
[JsonProperty("end", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public ulong? EndUnixMilliseconds
|
||||
{
|
||||
get
|
||||
{
|
||||
return End.HasValue ? ToUnixMilliseconds(End.Value) : (ulong?)null;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
End = value.HasValue ? FromUnixMilliseconds(value.Value) : (DateTime?)null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Unix Epoch time into a <see cref="DateTime"/>.
|
||||
/// </summary>
|
||||
/// <param name="unixTime">The time in milliseconds since 1970 / 01 / 01</param>
|
||||
/// <returns></returns>
|
||||
public static DateTime FromUnixMilliseconds(ulong unixTime)
|
||||
{
|
||||
var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
return epoch.AddMilliseconds(Convert.ToDouble(unixTime));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="DateTime"/> into a Unix Epoch time (in milliseconds).
|
||||
/// </summary>
|
||||
/// <param name="date">The datetime to convert</param>
|
||||
/// <returns></returns>
|
||||
public static ulong ToUnixMilliseconds(DateTime date)
|
||||
{
|
||||
var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
return Convert.ToUInt64((date - epoch).TotalMilliseconds);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Structure representing the part the player is in.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class Party
|
||||
{
|
||||
/// <summary>
|
||||
/// A unique ID for the player's current party / lobby / group. If this is not supplied, they player will not be in a party and the rest of the information will not be sent.
|
||||
/// <para>Max 128 Bytes</para>
|
||||
/// </summary>
|
||||
[JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string ID { get { return _partyid; } set { _partyid = value.GetNullOrString(); } }
|
||||
private string _partyid;
|
||||
|
||||
/// <summary>
|
||||
/// The current size of the players party / lobby / group.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public int Size { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maxium size of the party / lobby / group. This is required to be larger than <see cref="Size"/>. If it is smaller than the current party size, it will automatically be set too <see cref="Size"/> when the presence is sent.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public int Max { get; set; }
|
||||
|
||||
[JsonProperty("size", NullValueHandling = NullValueHandling.Ignore)]
|
||||
private int[] _size
|
||||
{
|
||||
get
|
||||
{
|
||||
//see issue https://github.com/discordapp/discord-rpc/issues/111
|
||||
int size = Math.Max(1, Size);
|
||||
return new int[] { size, Math.Max(size, Max) };
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (value.Length != 2)
|
||||
{
|
||||
Size = 0;
|
||||
Max = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
Size = value[0];
|
||||
Max = value[1];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A rich presence that has been parsed from the pipe as a response.
|
||||
/// </summary>
|
||||
internal class RichPresenceResponse : RichPresence
|
||||
{
|
||||
/// <summary>
|
||||
/// ID of the client
|
||||
/// </summary>
|
||||
[JsonProperty("application_id")]
|
||||
public string ClientID { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the bot
|
||||
/// </summary>
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; private set; }
|
||||
|
||||
}
|
||||
}
|
229
DiscordAPI/User.cs
Normal file
229
DiscordAPI/User.cs
Normal file
@ -0,0 +1,229 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordRPC
|
||||
{
|
||||
/// <summary>
|
||||
/// Object representing a Discord user. This is used for join requests.
|
||||
/// </summary>
|
||||
public class User
|
||||
{
|
||||
/// <summary>
|
||||
/// Possible formats for avatars
|
||||
/// </summary>
|
||||
public enum AvatarFormat
|
||||
{
|
||||
/// <summary>
|
||||
/// Portable Network Graphics format (.png)
|
||||
/// <para>Losses format that supports transparent avatars. Most recommended for stationary formats with wide support from many libraries.</para>
|
||||
/// </summary>
|
||||
PNG,
|
||||
|
||||
/// <summary>
|
||||
/// Joint Photographic Experts Group format (.jpeg)
|
||||
/// <para>The format most cameras use. Lossy and does not support transparent avatars.</para>
|
||||
/// </summary>
|
||||
JPEG,
|
||||
|
||||
/// <summary>
|
||||
/// WebP format (.webp)
|
||||
/// <para>Picture only version of WebM. Pronounced "weeb p".</para>
|
||||
/// </summary>
|
||||
WebP,
|
||||
|
||||
/// <summary>
|
||||
/// Graphics Interchange Format (.gif)
|
||||
/// <para>Animated avatars that Discord Nitro users are able to use. If the user doesn't have an animated avatar, then it will just be a single frame gif.</para>
|
||||
/// </summary>
|
||||
GIF //Gif, as in gift.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Possible square sizes of avatars.
|
||||
/// </summary>
|
||||
public enum AvatarSize
|
||||
{
|
||||
/// <summary> 16 x 16 pixels.</summary>
|
||||
x16 = 16,
|
||||
/// <summary> 32 x 32 pixels.</summary>
|
||||
x32 = 32,
|
||||
/// <summary> 64 x 64 pixels.</summary>
|
||||
x64 = 64,
|
||||
/// <summary> 128 x 128 pixels.</summary>
|
||||
x128 = 128,
|
||||
/// <summary> 256 x 256 pixels.</summary>
|
||||
x256 = 256,
|
||||
/// <summary> 512 x 512 pixels.</summary>
|
||||
x512 = 512,
|
||||
/// <summary> 1024 x 1024 pixels.</summary>
|
||||
x1024 = 1024,
|
||||
/// <summary> 2048 x 2048 pixels.</summary>
|
||||
x2048 = 2048
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The snowflake ID of the user.
|
||||
/// </summary>
|
||||
[JsonProperty("id")]
|
||||
public ulong ID { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The username of the player.
|
||||
/// </summary>
|
||||
[JsonProperty("username")]
|
||||
public string Username { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The discriminator of the user.
|
||||
/// </summary>
|
||||
[JsonProperty("discriminator")]
|
||||
public int Discriminator { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The avatar hash of the user. Too get a URL for the avatar, use the <see cref="GetAvatarURL(AvatarFormat, AvatarSize)"/>. This can be null if the user has no avatar. The <see cref="GetAvatarURL(AvatarFormat, AvatarSize)"/> will account for this and return the discord default.
|
||||
/// </summary>
|
||||
[JsonProperty("avatar")]
|
||||
public string Avatar { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The flags on a users account, often represented as a badge.
|
||||
/// </summary>
|
||||
[JsonProperty("flags")]
|
||||
public Flag Flags { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// A flag on the user account
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum Flag
|
||||
{
|
||||
/// <summary>No flag</summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>Staff of Discord.</summary>
|
||||
Employee = 1 << 0,
|
||||
|
||||
/// <summary>Partners of Discord.</summary>
|
||||
Partner = 1 << 1,
|
||||
|
||||
/// <summary>Original HypeSquad which organise events.</summary>
|
||||
HypeSquad = 1 << 2,
|
||||
|
||||
/// <summary>Bug Hunters that found and reported bugs in Discord.</summary>
|
||||
BugHunter = 1 << 3,
|
||||
|
||||
//These 2 are mistery types
|
||||
//A = 1 << 4,
|
||||
//B = 1 << 5,
|
||||
|
||||
/// <summary>The HypeSquad House of Bravery.</summary>
|
||||
HouseBravery = 1 << 6,
|
||||
|
||||
/// <summary>The HypeSquad House of Brilliance.</summary>
|
||||
HouseBrilliance = 1 << 7,
|
||||
|
||||
/// <summary>The HypeSquad House of Balance (the best one).</summary>
|
||||
HouseBalance = 1 << 8,
|
||||
|
||||
/// <summary>Early Supporter of Discord and had Nitro before the store was released.</summary>
|
||||
EarlySupporter = 1 << 9,
|
||||
|
||||
/// <summary>Apart of a team.
|
||||
/// <para>Unclear if it is reserved for members that share a team with the current application.</para>
|
||||
/// </summary>
|
||||
TeamUser = 1 << 10
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The premium type of the user.
|
||||
/// </summary>
|
||||
[JsonProperty("premium_type")]
|
||||
public PremiumType Premium { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of premium
|
||||
/// </summary>
|
||||
public enum PremiumType
|
||||
{
|
||||
/// <summary>No subscription to any forms of Nitro.</summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>Nitro Classic subscription. Has chat perks and animated avatars.</summary>
|
||||
NitroClassic = 1,
|
||||
|
||||
/// <summary>Nitro subscription. Has chat perks, animated avatars, server boosting, and access to free Nitro Games.</summary>
|
||||
Nitro = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The endpoint for the CDN. Normally cdn.discordapp.com.
|
||||
/// </summary>
|
||||
public string CdnEndpoint { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new User instance.
|
||||
/// </summary>
|
||||
internal User()
|
||||
{
|
||||
CdnEndpoint = "cdn.discordapp.com";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the URL paths to the appropriate configuration
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration received by the OnReady event.</param>
|
||||
internal void SetConfiguration(Configuration configuration)
|
||||
{
|
||||
this.CdnEndpoint = configuration.CdnHost;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a URL that can be used to download the user's avatar. If the user has not yet set their avatar, it will return the default one that discord is using. The default avatar only supports the <see cref="AvatarFormat.PNG"/> format.
|
||||
/// </summary>
|
||||
/// <param name="format">The format of the target avatar</param>
|
||||
/// <param name="size">The optional size of the avatar you wish for. Defaults to x128.</param>
|
||||
/// <returns></returns>
|
||||
public string GetAvatarURL(AvatarFormat format, AvatarSize size = AvatarSize.x128)
|
||||
{
|
||||
//Prepare the endpoint
|
||||
string endpoint = "/avatars/" + ID + "/" + Avatar;
|
||||
|
||||
//The user has no avatar, so we better replace it with the default
|
||||
if (string.IsNullOrEmpty(Avatar))
|
||||
{
|
||||
//Make sure we are only using PNG
|
||||
if (format != AvatarFormat.PNG)
|
||||
throw new BadImageFormatException("The user has no avatar and the requested format " + format.ToString() + " is not supported. (Only supports PNG).");
|
||||
|
||||
//Get the endpoint
|
||||
int descrim = Discriminator % 5;
|
||||
endpoint = "/embed/avatars/" + descrim;
|
||||
}
|
||||
|
||||
//Finish of the endpoint
|
||||
return string.Format("https://{0}{1}{2}?size={3}", this.CdnEndpoint, endpoint, GetAvatarExtension(format), (int)size);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the file extension of the specified format.
|
||||
/// </summary>
|
||||
/// <param name="format">The format to get the extention off</param>
|
||||
/// <returns>Returns a period prefixed file extension.</returns>
|
||||
public string GetAvatarExtension(AvatarFormat format)
|
||||
{
|
||||
return "." + format.ToString().ToLowerInvariant();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats the user into username#discriminator
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return Username + "#" + Discriminator.ToString("D4");
|
||||
}
|
||||
}
|
||||
}
|
191
DiscordAPI/Web/WebRPC.cs
Normal file
191
DiscordAPI/Web/WebRPC.cs
Normal file
@ -0,0 +1,191 @@
|
||||
using DiscordRPC.Exceptions;
|
||||
using DiscordRPC.RPC;
|
||||
using DiscordRPC.RPC.Commands;
|
||||
using DiscordRPC.RPC.Payload;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
|
||||
#if INCLUDE_WEB_RPC
|
||||
namespace DiscordRPC.Web
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles HTTP Rich Presence Requests
|
||||
/// </summary>
|
||||
[System.Obsolete("Rich Presence over HTTP is no longer supported by Discord. See offical Rich Presence github for more information.")]
|
||||
public static class WebRPC
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets the Rich Presence over the HTTP protocol. Does not support Join / Spectate and by default is blocking.
|
||||
/// </summary>
|
||||
/// <param name="presence">The presence to send to discord</param>
|
||||
/// <param name="applicationID">The ID of the application</param>
|
||||
/// <param name="port">The port the discord client is currently on. Specify this for testing. Will start scanning from supplied port.</param>
|
||||
/// <returns>Returns the rich presence result from the server. This can be null if presence was set to be null, or if there was no valid response from the client.</returns>
|
||||
[System.Obsolete("Setting Rich Presence over HTTP is no longer supported by Discord. See offical Rich Presence github for more information.")]
|
||||
public static RichPresence SetRichPresence(RichPresence presence, string applicationID, int port = 6463)
|
||||
{
|
||||
try
|
||||
{
|
||||
RichPresence response;
|
||||
if (TrySetRichPresence(presence, out response, applicationID, port))
|
||||
return response;
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to set the Rich Presence over the HTTP protocol. Does not support Join / Specate and by default is blocking.
|
||||
/// </summary>
|
||||
/// <param name="presence">The presence to send to discord</param>
|
||||
/// <param name="response">The response object from the client</param>
|
||||
/// <param name="applicationID">The ID of the application</param>
|
||||
/// <param name="port">The port the discord client is currently on. Specify this for testing. Will start scanning from supplied port.</param>
|
||||
/// <returns>True if the response was valid from the server, otherwise false.</returns>
|
||||
[System.Obsolete("Setting Rich Presence over HTTP is no longer supported by Discord. See offical Rich Presence github for more information.")]
|
||||
public static bool TrySetRichPresence(RichPresence presence, out RichPresence response, string applicationID, int port = 6463)
|
||||
{
|
||||
//Validate the presence
|
||||
if (presence != null)
|
||||
{
|
||||
//Send valid presence
|
||||
//Validate the presence with our settings
|
||||
if (presence.HasSecrets())
|
||||
throw new BadPresenceException("Cannot send a presence with secrets as HTTP endpoint does not suppport events.");
|
||||
|
||||
if (presence.HasParty() && presence.Party.Max < presence.Party.Size)
|
||||
throw new BadPresenceException("Presence maximum party size cannot be smaller than the current size.");
|
||||
}
|
||||
|
||||
//Iterate over the ports until the first succesfull one
|
||||
for (int p = port; p < 6472; p++)
|
||||
{
|
||||
//Prepare the url and json
|
||||
using (WebClient client = new WebClient())
|
||||
{
|
||||
try
|
||||
{
|
||||
WebRequest request = PrepareRequest(presence, applicationID, p);
|
||||
client.Headers.Add("content-type", "application/json");
|
||||
|
||||
var result = client.UploadString(request.URL, request.Data);
|
||||
if (TryParseResponse(result, out response))
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
//Something went wrong, but we are just going to ignore it and try the next port.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//we failed, return null
|
||||
response = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse the response of a Web Request to a rich presence
|
||||
/// </summary>
|
||||
/// <param name="json">The json data received by the client</param>
|
||||
/// <param name="response">The parsed rich presence</param>
|
||||
/// <returns>True if the parse was succesfull</returns>
|
||||
public static bool TryParseResponse(string json, out RichPresence response)
|
||||
{
|
||||
try
|
||||
{
|
||||
//Try to parse the JSON into a event
|
||||
EventPayload ev = JsonConvert.DeserializeObject<EventPayload>(json);
|
||||
|
||||
//We have a result, so parse the rich presence response and return it.
|
||||
if (ev != null)
|
||||
{
|
||||
//Parse the response into a rich presence response
|
||||
response = ev.GetObject<RichPresenceResponse>();
|
||||
return true;
|
||||
}
|
||||
|
||||
}catch(Exception) { }
|
||||
|
||||
//We failed.
|
||||
response = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares a struct containing data requried to make a succesful web client request to set the rich presence.
|
||||
/// </summary>
|
||||
/// <param name="presence">The rich presence to set.</param>
|
||||
/// <param name="applicationID">The ID of the application the presence belongs too.</param>
|
||||
/// <param name="port">The port the client is located on. The default port for the discord client is 6463, but it may move iteratively upto 6473 if the ports are unavailable.</param>
|
||||
/// <returns>Returns a web request containing nessary data to make a POST request</returns>
|
||||
[System.Obsolete("WebRequests are no longer supported because of the removed HTTP functionality by Discord. See offical Rich Presence github for more information.")]
|
||||
public static WebRequest PrepareRequest(RichPresence presence, string applicationID, int port = 6463)
|
||||
{
|
||||
//Validate the presence
|
||||
if (presence != null)
|
||||
{
|
||||
//Send valid presence
|
||||
//Validate the presence with our settings
|
||||
if (presence.HasSecrets())
|
||||
throw new BadPresenceException("Cannot send a presence with secrets as HTTP endpoint does not suppport events.");
|
||||
|
||||
if (presence.HasParty() && presence.Party.Max < presence.Party.Size)
|
||||
throw new BadPresenceException("Presence maximum party size cannot be smaller than the current size.");
|
||||
}
|
||||
|
||||
//Prepare some params
|
||||
int pid = System.Diagnostics.Process.GetCurrentProcess().Id;
|
||||
|
||||
//Prepare the payload
|
||||
PresenceCommand command = new PresenceCommand() { PID = pid, Presence = presence };
|
||||
var payload = command.PreparePayload(DateTime.UtcNow.ToFileTime());
|
||||
|
||||
string json = JsonConvert.SerializeObject(payload);
|
||||
|
||||
string url = "http://127.0.0.1:" + port + "/rpc?v=" + RpcConnection.VERSION + "&client_id=" + applicationID;
|
||||
return new WebRequest(url, json);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Details of a HTTP Post request that will set the rich presence.
|
||||
/// </summary>
|
||||
[System.Obsolete("Web Requests is no longer supported as Discord removed HTTP Rich Presence support. See offical Rich Presence github for more information.")]
|
||||
public struct WebRequest
|
||||
{
|
||||
private string _url;
|
||||
private string _json;
|
||||
private Dictionary<string, string> _headers;
|
||||
|
||||
/// <summary>
|
||||
/// The URL to send the POST request too
|
||||
/// </summary>
|
||||
public string URL { get { return _url; } }
|
||||
|
||||
/// <summary>
|
||||
/// The JSON formatted body to send with the POST request
|
||||
/// </summary>
|
||||
public string Data { get { return _json; } }
|
||||
|
||||
/// <summary>
|
||||
/// The headers to send with the body
|
||||
/// </summary>
|
||||
public Dictionary<string, string> Headers { get { return _headers; } }
|
||||
|
||||
internal WebRequest(string url, string json)
|
||||
{
|
||||
_url = url;
|
||||
_json = json;
|
||||
_headers = new Dictionary<string, string>();
|
||||
_headers.Add("content-type", "application/json");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
674
LICENSE
Normal file
674
LICENSE
Normal file
@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
22
LogWriter.cs
Normal file
22
LogWriter.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace EnderIce2.SDRSharpPlugin
|
||||
{
|
||||
public static class LogWriter
|
||||
{
|
||||
public static void WriteToFile(string Message)
|
||||
{
|
||||
if (SDRSharp.Radio.Utils.GetBooleanSetting("LogRPC", false))
|
||||
{
|
||||
string path = AppDomain.CurrentDomain.BaseDirectory + "\\RPCLogs\\";
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
Directory.CreateDirectory(path);
|
||||
}
|
||||
using StreamWriter sw = File.AppendText(AppDomain.CurrentDomain.BaseDirectory + "\\RPCLogs\\DiscordRPCLog_" + DateTime.Now.Date.ToShortDateString().Replace('/', '_') + ".log");
|
||||
sw.WriteLine($"[{DateTime.Now}] {Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
217
MainPlugin.cs
Normal file
217
MainPlugin.cs
Normal file
@ -0,0 +1,217 @@
|
||||
using DiscordRPC;
|
||||
using DiscordRPC.Logging;
|
||||
using DiscordRPC.Message;
|
||||
using SDRSharp.Common;
|
||||
using SDRSharp.Radio;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace EnderIce2.SDRSharpPlugin
|
||||
{
|
||||
public class MainPlugin : ISharpPlugin
|
||||
{
|
||||
private SettingsPanel _controlPanel;
|
||||
private const LogLevel logLevel = LogLevel.Trace;
|
||||
private const int discordPipe = 0;
|
||||
|
||||
private ISharpControl _control;
|
||||
private bool playedBefore;
|
||||
|
||||
private DiscordRpcClient client;
|
||||
private bool isConnected;
|
||||
public bool HasGui => true;
|
||||
public string DisplayName => "Discord RPC";
|
||||
public UserControl Gui => _controlPanel;
|
||||
|
||||
private readonly RichPresence presence = new RichPresence
|
||||
{
|
||||
Details = "Loading...",
|
||||
State = "Loading...",
|
||||
Assets = new Assets
|
||||
{
|
||||
LargeImageKey = "image_large",
|
||||
LargeImageText = "SDRSharp",
|
||||
SmallImageKey = "image_small",
|
||||
SmallImageText = $"SDR-RPC Plugin v{Assembly.LoadFrom("SDR-RPC.dll").GetName().Version}"
|
||||
}
|
||||
};
|
||||
|
||||
public void Initialize(ISharpControl control)
|
||||
{
|
||||
_controlPanel = new SettingsPanel();
|
||||
_control = control;
|
||||
if (Utils.GetBooleanSetting("EnableRPC", true))
|
||||
{
|
||||
if (Utils.GetStringSetting("ClientID").Replace(" ", "").Length != 18)
|
||||
{
|
||||
Utils.SaveSetting("ClientID", "765213507321856078");
|
||||
}
|
||||
client = new DiscordRpcClient(Utils.GetStringSetting("ClientID"), pipe: discordPipe)
|
||||
{
|
||||
Logger = new ConsoleLogger(logLevel, true)
|
||||
};
|
||||
|
||||
client.RegisterUriScheme();
|
||||
client.OnRpcMessage += Client_OnRpcMessage;
|
||||
client.OnPresenceUpdate += Client_OnPresenceUpdate;
|
||||
client.OnReady += OnReady;
|
||||
client.OnClose += OnClose;
|
||||
client.OnError += OnError;
|
||||
client.OnConnectionEstablished += OnConnectionEstablished;
|
||||
client.OnConnectionFailed += OnConnectionFailed;
|
||||
presence.Timestamps = new Timestamps
|
||||
{
|
||||
Start = DateTime.UtcNow
|
||||
};
|
||||
client.SkipIdenticalPresence = true;
|
||||
client.SetPresence(presence);
|
||||
client.Initialize();
|
||||
_ = MainLoop();
|
||||
}
|
||||
else
|
||||
{
|
||||
_controlPanel.ChangeStatus = "RPC is disabled";
|
||||
}
|
||||
LogWriter.WriteToFile("EOM Initialize");
|
||||
}
|
||||
|
||||
private async Task MainLoop()
|
||||
{
|
||||
loop_start:
|
||||
try
|
||||
{
|
||||
await Task.Delay(2000).ConfigureAwait(false);
|
||||
LogWriter.WriteToFile($"MainLoop called {client.IsInitialized}");
|
||||
while (true)
|
||||
{
|
||||
if (client != null && isConnected)
|
||||
{
|
||||
LogWriter.WriteToFile("Waiting 1000ms in loop...");
|
||||
await Task.Delay(1000).ConfigureAwait(false); // 1 second delay
|
||||
if (_control.IsPlaying)
|
||||
{
|
||||
presence.Assets.SmallImageKey = "play";
|
||||
playedBefore = true;
|
||||
}
|
||||
else if (!_control.IsPlaying && playedBefore)
|
||||
{
|
||||
presence.Assets.SmallImageKey = "pause";
|
||||
}
|
||||
|
||||
if (!playedBefore)
|
||||
{
|
||||
presence.Details = "Frequency: Not playing";
|
||||
presence.State = "Not playing";
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO: Check BandPlan.xml file and set in the status
|
||||
LogWriter.WriteToFile("Setting presence...");
|
||||
string frequency_text = $"Frequency: {$"{_control.Frequency:#,0,,0 Hz}"} | Bandwidth: {$"{_control.FilterBandwidth:#,0,,0 Hz}"} | {Enum.GetName(typeof(DetectorType), _control.DetectorType)}";
|
||||
presence.Details = frequency_text;
|
||||
switch (_control.DetectorType)
|
||||
{
|
||||
case DetectorType.WFM:
|
||||
if (!string.IsNullOrWhiteSpace(_control.RdsRadioText + _control.RdsProgramService))
|
||||
{
|
||||
string radio_text = string.IsNullOrWhiteSpace(_control.RdsRadioText) ? "" : $" - {_control.RdsRadioText}";
|
||||
presence.State = _control.FmStereo
|
||||
? $"RDS: ((( {_control.RdsProgramService} ))){radio_text}"
|
||||
: $"RDS: {_control.RdsProgramService}{radio_text}";
|
||||
}
|
||||
else
|
||||
{
|
||||
presence.State = "RDS: unknown";
|
||||
}
|
||||
break;
|
||||
default:
|
||||
presence.State = ""; // TODO: implement for every type; right now I don't really know what to add
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWriter.WriteToFile("Set rpc exception\n" + ex);
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
client.SetPresence(presence);
|
||||
}
|
||||
catch (ObjectDisposedException ex)
|
||||
{
|
||||
LogWriter.WriteToFile($"ObjectDisposedException exception for SetPresence\n{ex}");
|
||||
}
|
||||
LogWriter.WriteToFile("SetPresence");
|
||||
_controlPanel.ChangeStatus = $"Presence Updated {DateTime.UtcNow}";
|
||||
}
|
||||
}
|
||||
if (client == null)
|
||||
{
|
||||
_controlPanel.ChangeStatus = "Client is null";
|
||||
}
|
||||
else
|
||||
{
|
||||
_controlPanel.ChangeStatus = "Presence stopped";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex.Message.Contains("The process cannot access the file")) // not important exception
|
||||
{
|
||||
goto loop_start;
|
||||
}
|
||||
_controlPanel.ChangeStatus = $"RPC Update Error\n{ex.Message}";
|
||||
LogWriter.WriteToFile(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
LogWriter.WriteToFile("Close called");
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
private void Client_OnPresenceUpdate(object sender, PresenceMessage args)
|
||||
{
|
||||
LogWriter.WriteToFile($"[RpcMessage] | Presence state: {args.Presence.State}");
|
||||
}
|
||||
|
||||
private void Client_OnRpcMessage(object sender, IMessage msg)
|
||||
{
|
||||
LogWriter.WriteToFile($"[RpcMessage] | {msg.Type} | {msg}");
|
||||
}
|
||||
|
||||
private void OnConnectionFailed(object sender, ConnectionFailedMessage args)
|
||||
{
|
||||
_controlPanel.ChangeStatus = $"RPC Connection Failed!\n{args.Type} | {args.FailedPipe}";
|
||||
isConnected = false;
|
||||
}
|
||||
|
||||
private void OnConnectionEstablished(object sender, ConnectionEstablishedMessage args)
|
||||
{
|
||||
_controlPanel.ChangeStatus = "RPC Connection Established!";
|
||||
isConnected = true;
|
||||
}
|
||||
|
||||
private void OnError(object sender, ErrorMessage args)
|
||||
{
|
||||
_controlPanel.ChangeStatus = $"RPC Error:\n{args.Message}";
|
||||
}
|
||||
|
||||
private void OnClose(object sender, CloseMessage args)
|
||||
{
|
||||
_controlPanel.ChangeStatus = "RPC Closed";
|
||||
Close();
|
||||
}
|
||||
|
||||
private void OnReady(object sender, ReadyMessage args)
|
||||
{
|
||||
_controlPanel.ChangeStatus = "RPC Ready";
|
||||
}
|
||||
}
|
||||
}
|
36
Properties/AssemblyInfo.cs
Normal file
36
Properties/AssemblyInfo.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("Show on Discord what are you listening on AIRSPY SDR#")]
|
||||
[assembly: AssemblyDescription("Show on Discord what are you listening on AIRSPY SDR#")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("SDR# Discord RPC Plugin")]
|
||||
[assembly: AssemblyCopyright("Copyright © EnderIce2 2023")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("72e8628f-ba39-4915-bf3c-dd48bf477d30")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.2.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.2.0.0")]
|
73
Properties/Resources.Designer.cs
generated
Normal file
73
Properties/Resources.Designer.cs
generated
Normal file
@ -0,0 +1,73 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace DiscordRPC.Properties {
|
||||
using System;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// </summary>
|
||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class Resources {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal Resources() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DiscordRPC.Properties.Resources", typeof(Resources).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the current thread's CurrentUICulture property for all
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized resource of type System.Drawing.Bitmap.
|
||||
/// </summary>
|
||||
internal static System.Drawing.Bitmap gear {
|
||||
get {
|
||||
object obj = ResourceManager.GetObject("gear", resourceCulture);
|
||||
return ((System.Drawing.Bitmap)(obj));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
124
Properties/Resources.resx
Normal file
124
Properties/Resources.resx
Normal file
@ -0,0 +1,124 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
|
||||
<data name="gear" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\Resources\gear.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||
</data>
|
||||
</root>
|
84
README.md
Normal file
84
README.md
Normal file
@ -0,0 +1,84 @@
|
||||

|
||||

|
||||

|
||||
[](https://app.codacy.com/gh/EnderIce2/SDR-RPC?utm_source=github.com&utm_medium=referral&utm_content=EnderIce2/SDR-RPC&utm_campaign=Badge_Grade_Settings)
|
||||
[](https://www.codefactor.io/repository/github/enderice2/sdr-rpc)
|
||||
[](https://twitter.com/intent/follow?screen_name=enderice22)
|
||||
# SDR-RPC
|
||||
|
||||
SDR-RPC is an [SDRSharp](https://airspy.com/download/) plugin that adds Discord RPC feature in it
|
||||
|
||||
---
|
||||
|
||||
### ✨Features
|
||||
- 🎛Enable / Disable
|
||||
- ✏Showing frequency, play state and RDS almost in realtime
|
||||
- 🔌Most of the code is made asynchronous
|
||||
- 📖Logging for troubleshooting problems
|
||||
- 🔨Easy to install
|
||||
|
||||
---
|
||||
|
||||
### 🎁How to install
|
||||
|
||||
📽Video:
|
||||
|
||||
[](http://www.youtube.com/watch?v=Otn-xSn_ioI "")
|
||||
|
||||
---
|
||||
|
||||
### 🎫Example
|
||||
|
||||
📽Video:
|
||||
|
||||
[](http://www.youtube.com/watch?v=7k02dPqAjBA "")
|
||||
|
||||
---
|
||||
|
||||
### 💻Building by yourself
|
||||
|
||||
1. You need to download this stuff before compiling:
|
||||
- [Visual Studio 2019](https://visualstudio.microsoft.com/vs/)
|
||||
- [.NET 4.6](https://dotnet.microsoft.com/download/dotnet-framework/thank-you/net46-developer-pack-offline-installer)
|
||||
- [SDRSharp Plugin SDK](https://airspy.com/?ddownload=5944)
|
||||
- SDRSharp.Common.dll
|
||||
- SDRSharp.PanView.dll
|
||||
- SDRSharp.Radio.dll
|
||||
|
||||
2. Click "Code" and select "Open with Visual Studio" or "Download ZIP"
|
||||
|
||||

|
||||
|
||||
3. If you downloaded as ZIP, unzip the archive and double click on "SDRSharpPlugin.DiscordRPC.sln"
|
||||
4. Build it and move files to SDR# location (if the compile fails try copying Reference files into /bin/Debug or /bin/Release folder)
|
||||
|
||||
---
|
||||
|
||||
### 👀Setting your custom images on RPC
|
||||
|
||||
1. Go to https://discord.com/developers/applications and create your own application
|
||||
2. Name it "SDRSharp" or something similar
|
||||
|
||||

|
||||
|
||||
3. Go to Rich Presence > Rich Presence Assets and add your own images with these names:
|
||||
|
||||

|
||||
|
||||
4. Go back to General Information and copy Client ID
|
||||
|
||||

|
||||
|
||||
5. Paste the Client ID you copied earlier in plugin textbox from SDRSharp and press the ENTER key
|
||||
|
||||

|
||||
|
||||
6. Restart the SDRSharp and it should be changed with your custom images
|
||||
|
||||
---
|
||||
|
||||
### 📕TODO List
|
||||
|
||||
- Invite people to get Spy Server Address or connecting via voice chat system to listen and having the ability to change the frequency
|
||||
- Change Settings Panel to match SDR# theme
|
||||
- Show "Listening" instead of "Playing"
|
1
Register.txt
Normal file
1
Register.txt
Normal file
@ -0,0 +1 @@
|
||||
<add key="Discord RPC" value="EnderIce2.SDRSharpPlugin.MainPlugin,SDR-RPC" />
|
BIN
Resources/gear.png
Normal file
BIN
Resources/gear.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
175
SDRSharpPlugin.DiscordRPC.csproj
Normal file
175
SDRSharpPlugin.DiscordRPC.csproj
Normal file
@ -0,0 +1,175 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{72E8628F-BA39-4915-BF3C-DD48BF477D30}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>DiscordRPC</RootNamespace>
|
||||
<AssemblyName>SDR-RPC</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.6</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<Deterministic>true</Deterministic>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<CodeAnalysisRuleSet />
|
||||
<LangVersion>8.0</LangVersion>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<CodeAnalysisRuleSet />
|
||||
<LangVersion>8.0</LangVersion>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<SignAssembly>false</SignAssembly>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="SDRSharp.Common, Version=0.0.0.0, Culture=neutral, processorArchitecture=x86">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\sdrsharp-x86\SDRSharp.Common.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="SDRSharp.PanView, Version=0.0.0.0, Culture=neutral, processorArchitecture=x86">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\sdrsharp-x86\SDRSharp.PanView.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="SDRSharp.Radio, Version=0.0.0.0, Culture=neutral, processorArchitecture=x86">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\sdrsharp-x86\SDRSharp.Radio.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Runtime" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="DiscordAPI\Configuration.cs" />
|
||||
<Compile Include="DiscordAPI\Converters\EnumSnakeCaseConverter.cs" />
|
||||
<Compile Include="DiscordAPI\Converters\EnumValueAttribute.cs" />
|
||||
<Compile Include="DiscordAPI\DiscordRpcClient.cs" />
|
||||
<Compile Include="DiscordAPI\Events.cs" />
|
||||
<Compile Include="DiscordAPI\EventType.cs" />
|
||||
<Compile Include="DiscordAPI\Exceptions\BadPresenceException.cs" />
|
||||
<Compile Include="DiscordAPI\Exceptions\InvalidConfigurationException.cs" />
|
||||
<Compile Include="DiscordAPI\Exceptions\InvalidPipeException.cs" />
|
||||
<Compile Include="DiscordAPI\Exceptions\StringOutOfRangeException.cs" />
|
||||
<Compile Include="DiscordAPI\Exceptions\UninitializedException.cs" />
|
||||
<Compile Include="DiscordAPI\Helper\BackoffDelay.cs" />
|
||||
<Compile Include="DiscordAPI\Helper\StringTools.cs" />
|
||||
<Compile Include="DiscordAPI\IO\Handshake.cs" />
|
||||
<Compile Include="DiscordAPI\IO\INamedPipeClient.cs" />
|
||||
<Compile Include="DiscordAPI\IO\ManagedNamedPipeClient.cs" />
|
||||
<Compile Include="DiscordAPI\IO\Opcode.cs" />
|
||||
<Compile Include="DiscordAPI\IO\PipeFrame.cs" />
|
||||
<Compile Include="DiscordAPI\Logging\ConsoleLogger.cs" />
|
||||
<Compile Include="DiscordAPI\Logging\FileLogger.cs" />
|
||||
<Compile Include="DiscordAPI\Logging\ILogger.cs" />
|
||||
<Compile Include="DiscordAPI\Logging\LogLevel.cs" />
|
||||
<Compile Include="DiscordAPI\Logging\NullLogger.cs" />
|
||||
<Compile Include="DiscordAPI\Message\CloseMessage.cs" />
|
||||
<Compile Include="DiscordAPI\Message\ConnectionEstablishedMessage.cs" />
|
||||
<Compile Include="DiscordAPI\Message\ConnectionFailedMessage.cs" />
|
||||
<Compile Include="DiscordAPI\Message\ErrorMessage.cs" />
|
||||
<Compile Include="DiscordAPI\Message\IMessage.cs" />
|
||||
<Compile Include="DiscordAPI\Message\JoinMessage.cs" />
|
||||
<Compile Include="DiscordAPI\Message\JoinRequestMessage.cs" />
|
||||
<Compile Include="DiscordAPI\Message\MessageType.cs" />
|
||||
<Compile Include="DiscordAPI\Message\PresenceMessage.cs" />
|
||||
<Compile Include="DiscordAPI\Message\ReadyMessage.cs" />
|
||||
<Compile Include="DiscordAPI\Message\SpectateMessage.cs" />
|
||||
<Compile Include="DiscordAPI\Message\SubscribeMessage.cs" />
|
||||
<Compile Include="DiscordAPI\Message\UnsubscribeMsesage.cs" />
|
||||
<Compile Include="DiscordAPI\Registry\IUriSchemeCreator.cs" />
|
||||
<Compile Include="DiscordAPI\Registry\MacUriSchemeCreator.cs" />
|
||||
<Compile Include="DiscordAPI\Registry\UnixUriSchemeCreator.cs" />
|
||||
<Compile Include="DiscordAPI\Registry\UriScheme.cs" />
|
||||
<Compile Include="DiscordAPI\Registry\WindowsUriSchemeCreator.cs" />
|
||||
<Compile Include="DiscordAPI\RichPresence.cs" />
|
||||
<Compile Include="DiscordAPI\RPC\Commands\CloseCommand.cs" />
|
||||
<Compile Include="DiscordAPI\RPC\Commands\ICommand.cs" />
|
||||
<Compile Include="DiscordAPI\RPC\Commands\PresenceCommand.cs" />
|
||||
<Compile Include="DiscordAPI\RPC\Commands\RespondCommand.cs" />
|
||||
<Compile Include="DiscordAPI\RPC\Commands\SubscribeCommand.cs" />
|
||||
<Compile Include="DiscordAPI\RPC\Payload\ClosePayload.cs" />
|
||||
<Compile Include="DiscordAPI\RPC\Payload\Command.cs" />
|
||||
<Compile Include="DiscordAPI\RPC\Payload\IPayload.cs" />
|
||||
<Compile Include="DiscordAPI\RPC\Payload\PayloadArgument.cs" />
|
||||
<Compile Include="DiscordAPI\RPC\Payload\PayloadEvent.cs" />
|
||||
<Compile Include="DiscordAPI\RPC\Payload\ServerEvent.cs" />
|
||||
<Compile Include="DiscordAPI\RPC\RpcConnection.cs" />
|
||||
<Compile Include="DiscordAPI\User.cs" />
|
||||
<Compile Include="DiscordAPI\Web\WebRPC.cs" />
|
||||
<Compile Include="LogWriter.cs" />
|
||||
<Compile Include="MainPlugin.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Properties\Resources.Designer.cs">
|
||||
<AutoGen>True</AutoGen>
|
||||
<DesignTime>True</DesignTime>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="SettingsPanel.cs">
|
||||
<SubType>UserControl</SubType>
|
||||
</Compile>
|
||||
<Compile Include="SettingsPanel.Designer.cs">
|
||||
<DependentUpon>SettingsPanel.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="SettingsForm.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
<Compile Include="SettingsForm.Designer.cs">
|
||||
<DependentUpon>SettingsForm.cs</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Properties\Resources.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="SettingsPanel.resx">
|
||||
<DependentUpon>SettingsPanel.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="SettingsForm.resx">
|
||||
<DependentUpon>SettingsForm.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="DiscordAPI\LICENSE.txt" />
|
||||
<Content Include="Register.txt">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<None Include="packages.config" />
|
||||
<None Include="Resources\gear.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include=".editorconfig" />
|
||||
<None Include="app.config" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
30
SDRSharpPlugin.DiscordRPC.sln
Normal file
30
SDRSharpPlugin.DiscordRPC.sln
Normal file
@ -0,0 +1,30 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.30517.126
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SDRSharpPlugin.DiscordRPC", "SDRSharpPlugin.DiscordRPC.csproj", "{72E8628F-BA39-4915-BF3C-DD48BF477D30}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{446ABA84-7848-4219-AE53-4EE2132D0710}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{72E8628F-BA39-4915-BF3C-DD48BF477D30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{72E8628F-BA39-4915-BF3C-DD48BF477D30}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{72E8628F-BA39-4915-BF3C-DD48BF477D30}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{72E8628F-BA39-4915-BF3C-DD48BF477D30}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {DA82DCCB-E6B0-4649-9A0C-CECB0AF41CDD}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
188
SettingsForm.Designer.cs
generated
Normal file
188
SettingsForm.Designer.cs
generated
Normal file
@ -0,0 +1,188 @@
|
||||
namespace EnderIce2.SDRSharpPlugin
|
||||
{
|
||||
partial class SettingsForm
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.button1 = new System.Windows.Forms.Button();
|
||||
this.label1 = new System.Windows.Forms.Label();
|
||||
this.button2 = new System.Windows.Forms.Button();
|
||||
this.checkBox1 = new System.Windows.Forms.CheckBox();
|
||||
this.textBox1 = new System.Windows.Forms.TextBox();
|
||||
this.label2 = new System.Windows.Forms.Label();
|
||||
this.button3 = new System.Windows.Forms.Button();
|
||||
this.button4 = new System.Windows.Forms.Button();
|
||||
this.button5 = new System.Windows.Forms.Button();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// button1
|
||||
//
|
||||
this.button1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.button1.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(15)))), ((int)(((byte)(15)))), ((int)(((byte)(15)))));
|
||||
this.button1.FlatStyle = System.Windows.Forms.FlatStyle.Popup;
|
||||
this.button1.Location = new System.Drawing.Point(200, 126);
|
||||
this.button1.Name = "button1";
|
||||
this.button1.Size = new System.Drawing.Size(75, 23);
|
||||
this.button1.TabIndex = 1;
|
||||
this.button1.Text = "&OK";
|
||||
this.button1.UseVisualStyleBackColor = false;
|
||||
this.button1.Click += new System.EventHandler(this.Button1_Click);
|
||||
//
|
||||
// label1
|
||||
//
|
||||
this.label1.AutoSize = true;
|
||||
this.label1.Font = new System.Drawing.Font("Microsoft Sans Serif", 14.25F);
|
||||
this.label1.Location = new System.Drawing.Point(6, 6);
|
||||
this.label1.Name = "label1";
|
||||
this.label1.Size = new System.Drawing.Size(241, 24);
|
||||
this.label1.TabIndex = 3;
|
||||
this.label1.Text = "DiscordRPC Plugin Settings";
|
||||
//
|
||||
// button2
|
||||
//
|
||||
this.button2.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.button2.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(15)))), ((int)(((byte)(15)))), ((int)(((byte)(15)))));
|
||||
this.button2.FlatStyle = System.Windows.Forms.FlatStyle.Popup;
|
||||
this.button2.Location = new System.Drawing.Point(12, 126);
|
||||
this.button2.Name = "button2";
|
||||
this.button2.Size = new System.Drawing.Size(93, 23);
|
||||
this.button2.TabIndex = 5;
|
||||
this.button2.Text = "Support Me";
|
||||
this.button2.UseVisualStyleBackColor = false;
|
||||
this.button2.Click += new System.EventHandler(this.Button2_Click);
|
||||
//
|
||||
// checkBox1
|
||||
//
|
||||
this.checkBox1.AutoSize = true;
|
||||
this.checkBox1.Location = new System.Drawing.Point(12, 105);
|
||||
this.checkBox1.Name = "checkBox1";
|
||||
this.checkBox1.Size = new System.Drawing.Size(205, 17);
|
||||
this.checkBox1.TabIndex = 6;
|
||||
this.checkBox1.Text = "Enable Logging (for debugging)";
|
||||
this.checkBox1.UseVisualStyleBackColor = true;
|
||||
this.checkBox1.CheckedChanged += new System.EventHandler(this.CheckBox1_CheckedChanged_1);
|
||||
//
|
||||
// textBox1
|
||||
//
|
||||
this.textBox1.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(25)))), ((int)(((byte)(25)))), ((int)(((byte)(25)))));
|
||||
this.textBox1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
|
||||
this.textBox1.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(244)))), ((int)(((byte)(244)))), ((int)(((byte)(244)))));
|
||||
this.textBox1.Location = new System.Drawing.Point(12, 49);
|
||||
this.textBox1.Name = "textBox1";
|
||||
this.textBox1.Size = new System.Drawing.Size(263, 21);
|
||||
this.textBox1.TabIndex = 7;
|
||||
//
|
||||
// label2
|
||||
//
|
||||
this.label2.AutoSize = true;
|
||||
this.label2.Location = new System.Drawing.Point(13, 33);
|
||||
this.label2.Name = "label2";
|
||||
this.label2.Size = new System.Drawing.Size(63, 13);
|
||||
this.label2.TabIndex = 8;
|
||||
this.label2.Text = "Client ID:";
|
||||
//
|
||||
// button3
|
||||
//
|
||||
this.button3.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(15)))), ((int)(((byte)(15)))), ((int)(((byte)(15)))));
|
||||
this.button3.FlatStyle = System.Windows.Forms.FlatStyle.Popup;
|
||||
this.button3.Location = new System.Drawing.Point(141, 76);
|
||||
this.button3.Name = "button3";
|
||||
this.button3.Size = new System.Drawing.Size(64, 23);
|
||||
this.button3.TabIndex = 9;
|
||||
this.button3.Text = "Reset";
|
||||
this.button3.UseVisualStyleBackColor = false;
|
||||
this.button3.Click += new System.EventHandler(this.Button3_Click);
|
||||
//
|
||||
// button4
|
||||
//
|
||||
this.button4.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(15)))), ((int)(((byte)(15)))), ((int)(((byte)(15)))));
|
||||
this.button4.FlatStyle = System.Windows.Forms.FlatStyle.Popup;
|
||||
this.button4.Location = new System.Drawing.Point(211, 76);
|
||||
this.button4.Name = "button4";
|
||||
this.button4.Size = new System.Drawing.Size(64, 23);
|
||||
this.button4.TabIndex = 10;
|
||||
this.button4.Text = "Apply";
|
||||
this.button4.UseVisualStyleBackColor = false;
|
||||
this.button4.Click += new System.EventHandler(this.Button4_Click);
|
||||
//
|
||||
// button5
|
||||
//
|
||||
this.button5.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.button5.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(15)))), ((int)(((byte)(15)))), ((int)(((byte)(15)))));
|
||||
this.button5.FlatStyle = System.Windows.Forms.FlatStyle.Popup;
|
||||
this.button5.Location = new System.Drawing.Point(111, 126);
|
||||
this.button5.Name = "button5";
|
||||
this.button5.Size = new System.Drawing.Size(83, 23);
|
||||
this.button5.TabIndex = 11;
|
||||
this.button5.Text = "Licenses";
|
||||
this.button5.UseVisualStyleBackColor = false;
|
||||
this.button5.Click += new System.EventHandler(this.Button5_Click);
|
||||
//
|
||||
// SettingsForm
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
this.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(28)))), ((int)(((byte)(28)))), ((int)(((byte)(28)))));
|
||||
this.ClientSize = new System.Drawing.Size(287, 161);
|
||||
this.Controls.Add(this.button5);
|
||||
this.Controls.Add(this.button4);
|
||||
this.Controls.Add(this.button3);
|
||||
this.Controls.Add(this.label2);
|
||||
this.Controls.Add(this.textBox1);
|
||||
this.Controls.Add(this.checkBox1);
|
||||
this.Controls.Add(this.button2);
|
||||
this.Controls.Add(this.label1);
|
||||
this.Controls.Add(this.button1);
|
||||
this.Font = new System.Drawing.Font("Verdana", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(238)));
|
||||
this.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(244)))), ((int)(((byte)(244)))), ((int)(((byte)(244)))));
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||
this.MaximizeBox = false;
|
||||
this.MinimizeBox = false;
|
||||
this.Name = "SettingsForm";
|
||||
this.ShowIcon = false;
|
||||
this.ShowInTaskbar = false;
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
|
||||
this.Text = "SDRSharp Discord RPC Settings";
|
||||
this.TopMost = true;
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
private System.Windows.Forms.Button button1;
|
||||
private System.Windows.Forms.Label label1;
|
||||
private System.Windows.Forms.Button button2;
|
||||
private System.Windows.Forms.CheckBox checkBox1;
|
||||
private System.Windows.Forms.TextBox textBox1;
|
||||
private System.Windows.Forms.Label label2;
|
||||
private System.Windows.Forms.Button button3;
|
||||
private System.Windows.Forms.Button button4;
|
||||
private System.Windows.Forms.Button button5;
|
||||
}
|
||||
}
|
52
SettingsForm.cs
Normal file
52
SettingsForm.cs
Normal file
@ -0,0 +1,52 @@
|
||||
using SDRSharp.Radio;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace EnderIce2.SDRSharpPlugin
|
||||
{
|
||||
public partial class SettingsForm : Form
|
||||
{
|
||||
public SettingsForm()
|
||||
{
|
||||
InitializeComponent();
|
||||
textBox1.Text = Utils.GetStringSetting("ClientID");
|
||||
checkBox1.Checked = Utils.GetBooleanSetting("LogRPC", false);
|
||||
}
|
||||
|
||||
private void Button2_Click(object sender, EventArgs e)
|
||||
{
|
||||
Process.Start(new ProcessStartInfo("https://ko-fi.com/enderice2") { UseShellExecute = true });
|
||||
Close();
|
||||
}
|
||||
|
||||
private void Button1_Click(object sender, EventArgs e) => Close();
|
||||
|
||||
private void CheckBox1_CheckedChanged(object sender, EventArgs e) => Utils.SaveSetting("ShowWelcomePage", !checkBox1.Checked);
|
||||
|
||||
private void Button5_Click(object sender, EventArgs e)
|
||||
{
|
||||
MessageBox.Show("MIT License\n\nCopyright (c) 2018 Lachee\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.", "discord-rpc-csharp");
|
||||
MessageBox.Show("The MIT License (MIT)\n\nCopyright(c) 2007 James Newton-King\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.", "Newtonsoft.Json");
|
||||
}
|
||||
|
||||
private void CheckBox1_CheckedChanged_1(object sender, EventArgs e) => Utils.SaveSetting("LogRPC", checkBox1.Checked);
|
||||
|
||||
private void Button4_Click(object sender, EventArgs e)
|
||||
{
|
||||
textBox1.Text.Replace(" ", "").Replace("\n", "").Replace("\r", "");
|
||||
if (!int.TryParse(textBox1.Text, out _) || textBox1.Text.Length != 18)
|
||||
{
|
||||
MessageBox.Show("Invalid Client ID!");
|
||||
}
|
||||
Utils.SaveSetting("ClientID", textBox1.Text);
|
||||
label1.Text = $"Configuration Updated.\nNew ID: {Utils.GetStringSetting("ClientID")}";
|
||||
}
|
||||
|
||||
private void Button3_Click(object sender, EventArgs e)
|
||||
{
|
||||
Utils.SaveSetting("ClientID", "765213507321856078");
|
||||
textBox1.Text = "765213507321856078";
|
||||
}
|
||||
}
|
||||
}
|
120
SettingsForm.resx
Normal file
120
SettingsForm.resx
Normal file
@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
107
SettingsPanel.Designer.cs
generated
Normal file
107
SettingsPanel.Designer.cs
generated
Normal file
@ -0,0 +1,107 @@
|
||||
namespace EnderIce2.SDRSharpPlugin
|
||||
{
|
||||
partial class SettingsPanel
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Component Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.checkBox1 = new System.Windows.Forms.CheckBox();
|
||||
this.label1 = new System.Windows.Forms.Label();
|
||||
this.button1 = new System.Windows.Forms.Button();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// checkBox1
|
||||
//
|
||||
this.checkBox1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.checkBox1.Checked = true;
|
||||
this.checkBox1.CheckState = System.Windows.Forms.CheckState.Checked;
|
||||
this.checkBox1.Font = new System.Drawing.Font("Verdana", 7F);
|
||||
this.checkBox1.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(244)))), ((int)(((byte)(244)))), ((int)(((byte)(244)))));
|
||||
this.checkBox1.Location = new System.Drawing.Point(3, 3);
|
||||
this.checkBox1.Name = "checkBox1";
|
||||
this.checkBox1.Size = new System.Drawing.Size(153, 21);
|
||||
this.checkBox1.TabIndex = 0;
|
||||
this.checkBox1.Text = "Enable Discord RPC";
|
||||
this.checkBox1.UseVisualStyleBackColor = true;
|
||||
this.checkBox1.CheckedChanged += new System.EventHandler(this.CheckBox1_CheckedChanged);
|
||||
//
|
||||
// label1
|
||||
//
|
||||
this.label1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
| System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.label1.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
||||
this.label1.Font = new System.Drawing.Font("Verdana", 7F);
|
||||
this.label1.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(244)))), ((int)(((byte)(244)))), ((int)(((byte)(244)))));
|
||||
this.label1.Location = new System.Drawing.Point(0, 28);
|
||||
this.label1.Name = "label1";
|
||||
this.label1.Size = new System.Drawing.Size(200, 22);
|
||||
this.label1.TabIndex = 1;
|
||||
this.label1.Text = "Loading status...";
|
||||
this.label1.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
|
||||
//
|
||||
// button1
|
||||
//
|
||||
this.button1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.button1.BackgroundImage = global::DiscordRPC.Properties.Resources.gear;
|
||||
this.button1.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom;
|
||||
this.button1.FlatAppearance.BorderSize = 0;
|
||||
this.button1.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
||||
this.button1.Font = new System.Drawing.Font("Verdana", 7F);
|
||||
this.button1.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(244)))), ((int)(((byte)(244)))), ((int)(((byte)(244)))));
|
||||
this.button1.Location = new System.Drawing.Point(175, 0);
|
||||
this.button1.Name = "button1";
|
||||
this.button1.Size = new System.Drawing.Size(25, 25);
|
||||
this.button1.TabIndex = 4;
|
||||
this.button1.UseVisualStyleBackColor = true;
|
||||
this.button1.Click += new System.EventHandler(this.Button1_Click);
|
||||
//
|
||||
// SettingsPanel
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
this.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(15)))), ((int)(((byte)(15)))), ((int)(((byte)(15)))));
|
||||
this.Controls.Add(this.button1);
|
||||
this.Controls.Add(this.label1);
|
||||
this.Controls.Add(this.checkBox1);
|
||||
this.Font = new System.Drawing.Font("Verdana", 7F);
|
||||
this.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(244)))), ((int)(((byte)(244)))), ((int)(((byte)(244)))));
|
||||
this.MinimumSize = new System.Drawing.Size(200, 50);
|
||||
this.Name = "SettingsPanel";
|
||||
this.Size = new System.Drawing.Size(200, 50);
|
||||
this.Load += new System.EventHandler(this.SettingsPanel_Load);
|
||||
this.ResumeLayout(false);
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.CheckBox checkBox1;
|
||||
private System.Windows.Forms.Label label1;
|
||||
private System.Windows.Forms.Button button1;
|
||||
}
|
||||
}
|
47
SettingsPanel.cs
Normal file
47
SettingsPanel.cs
Normal file
@ -0,0 +1,47 @@
|
||||
using SDRSharp.Radio;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace EnderIce2.SDRSharpPlugin
|
||||
{
|
||||
public partial class SettingsPanel : UserControl
|
||||
{
|
||||
private string _ChangeStatus;
|
||||
public string ChangeStatus
|
||||
{
|
||||
get => _ChangeStatus;
|
||||
set
|
||||
{
|
||||
_ChangeStatus = value;
|
||||
label1.Text = value;
|
||||
LogWriter.WriteToFile(value);
|
||||
}
|
||||
}
|
||||
public SettingsPanel()
|
||||
{
|
||||
InitializeComponent();
|
||||
checkBox1.Checked = Utils.GetBooleanSetting("EnableRPC", true);
|
||||
LogWriter.WriteToFile("User Control Loaded");
|
||||
}
|
||||
|
||||
private void Button1_Click(object sender, EventArgs e) => new SettingsForm().Show();
|
||||
|
||||
private void CheckBox1_CheckedChanged(object sender, EventArgs e)
|
||||
{
|
||||
Utils.SaveSetting("EnableRPC", checkBox1.Checked);
|
||||
label1.Text = "Restart required";
|
||||
LogWriter.WriteToFile($"checkbox on SettingsPanel clicked {checkBox1.Checked}");
|
||||
}
|
||||
|
||||
private void SettingsPanel_Load(object sender, EventArgs e)
|
||||
{
|
||||
// can't use bcz System.Drawing.Color is from dotnet core 5
|
||||
//MainPlugin._control.ThemeForeColor;
|
||||
//BackColor = MainPlugin._control.ThemePanelColor;
|
||||
//MainPlugin._control.ThemeBackColor;
|
||||
BackColor = System.Drawing.Color.FromArgb(15, 15, 15);
|
||||
ForeColor = System.Drawing.Color.FromArgb(244, 244, 244);
|
||||
}
|
||||
}
|
||||
}
|
120
SettingsPanel.resx
Normal file
120
SettingsPanel.resx
Normal file
@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
11
app.config
Normal file
11
app.config
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Drawing.Primitives" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0" />
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
</configuration>
|
4
packages.config
Normal file
4
packages.config
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net46" />
|
||||
</packages>
|
Loading…
x
Reference in New Issue
Block a user