From 2d8412ce2979313bbc2337a48a2686621cdf09d4 Mon Sep 17 00:00:00 2001 From: Anon Date: Mon, 23 Mar 2026 03:06:00 +0100 Subject: [PATCH] Embed Discord RPC pipe client in DiscordRpc bot --- MinecraftClient/ChatBots/DiscordRpc.cs | 394 +++++++++++++++++ .../ChatBots/DiscordRpcPipeClient.cs | 399 ------------------ 2 files changed, 394 insertions(+), 399 deletions(-) delete mode 100644 MinecraftClient/ChatBots/DiscordRpcPipeClient.cs diff --git a/MinecraftClient/ChatBots/DiscordRpc.cs b/MinecraftClient/ChatBots/DiscordRpc.cs index 6ab78ac9..f4be0532 100644 --- a/MinecraftClient/ChatBots/DiscordRpc.cs +++ b/MinecraftClient/ChatBots/DiscordRpc.cs @@ -1,5 +1,10 @@ using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Pipes; +using System.Threading; using DiscordRPC; +using DiscordRPC.IO; using DiscordRPC.Logging; using MinecraftClient.Mapping; using MinecraftClient.Scripting; @@ -329,5 +334,394 @@ namespace MinecraftClient.ChatBots return value.Length <= maxLength ? value : value[..(maxLength - 3)] + "..."; } + + /// + /// Flatpak Discord exposes the RPC socket under app/com.discordapp.Discord, + /// but the currently published DiscordRichPresence package does not probe that path. + /// + private sealed class DiscordRpcPipeClient : INamedPipeClient + { + private const string DiscordPipePrefix = "discord-ipc-"; + private const int MaximumPipeVariations = 10; + + private static readonly string[] s_unixPackageDirectories = + [ + // Official Discord clients + "app/com.discordapp.Discord", + "snap.discord", + + // Community desktop clients / wrappers + "app/dev.vencord.Vesktop", + ".flatpak/dev.vencord.Vesktop/xdg-run", + "app/org.equicord.equibop", + "app/io.github.equicord.equibop", + "app/xyz.armcord.ArmCord", + "app/io.github.spacingbat3.webcord" + ]; + + private readonly byte[] _buffer = new byte[PipeFrame.MAX_SIZE]; + private readonly Queue _frameQueue = new(); + private readonly object _frameQueueLock = new(); + private readonly object _streamLock = new(); + + private int _connectedPipe; + private NamedPipeClientStream? _stream; + private volatile bool _isClosed = true; + private volatile bool _isDisposed; + + public ILogger Logger { get; set; } = new NullLogger(); + + public bool IsConnected + { + get + { + if (_isClosed) + return false; + + lock (_streamLock) + return _stream is { IsConnected: true }; + } + } + + [Obsolete("The connected pipe is not neccessary information.")] + public int ConnectedPipe => _connectedPipe; + + public bool Connect(int pipe) + { + Logger.Trace("DiscordRpcPipeClient.Connect({0})", pipe); + + if (_isDisposed) + throw new ObjectDisposedException(nameof(DiscordRpcPipeClient)); + + if (pipe > 9) + throw new ArgumentOutOfRangeException(nameof(pipe), "Argument cannot be greater than 9"); + + int startPipe = pipe >= 0 ? pipe : 0; + + foreach (string pipeName in GetPipeCandidates(startPipe)) + { + if (AttemptConnection(pipeName)) + { + BeginReadStream(); + return true; + } + } + + return false; + } + + public bool ReadFrame(out PipeFrame frame) + { + if (_isDisposed) + throw new ObjectDisposedException(nameof(DiscordRpcPipeClient)); + + lock (_frameQueueLock) + { + if (_frameQueue.Count == 0) + { + frame = default; + return false; + } + + frame = _frameQueue.Dequeue(); + return true; + } + } + + public bool WriteFrame(PipeFrame frame) + { + if (_isDisposed) + throw new ObjectDisposedException(nameof(DiscordRpcPipeClient)); + + if (_isClosed || !IsConnected) + { + Logger.Error("Failed to write frame because the stream is closed"); + return false; + } + + try + { + 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"); + } + + return false; + } + + public void Close() + { + if (_isClosed) + { + Logger.Warning("Tried to close a already closed pipe."); + return; + } + + try + { + lock (_streamLock) + { + if (_stream is not null) + { + try + { + _stream.Flush(); + _stream.Dispose(); + } + catch + { + } + + _stream = null; + _isClosed = true; + } + else + { + Logger.Warning("Stream was closed, but no stream was available to begin with!"); + } + } + } + catch (ObjectDisposedException) + { + Logger.Warning("Tried to dispose already disposed stream"); + } + finally + { + _isClosed = true; + } + } + + public void Dispose() + { + if (_isDisposed) + return; + + if (!_isClosed) + Close(); + + lock (_streamLock) + { + _stream?.Dispose(); + _stream = null; + } + + _isDisposed = true; + } + + private bool AttemptConnection(string pipeName) + { + if (_isDisposed) + throw new ObjectDisposedException(nameof(DiscordRpcPipeClient)); + + try + { + lock (_streamLock) + { + Logger.Info("Attempting to connect to {0}", pipeName); + _stream = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous); + _stream.Connect(0); + + Logger.Trace("Waiting for connection..."); + while (!_stream.IsConnected) + Thread.Sleep(10); + } + + Logger.Info("Connected to {0}", pipeName); + _connectedPipe = int.Parse(pipeName[(pipeName.LastIndexOf('-') + 1)..], System.Globalization.CultureInfo.InvariantCulture); + _isClosed = false; + } + catch (Exception e) + { + Logger.Error("Failed connection to {0}. {1}", pipeName, e.Message); + Close(); + } + + Logger.Trace("Done. Result: {0}", _isClosed); + return !_isClosed; + } + + private void BeginReadStream() + { + if (_isClosed) + return; + + try + { + lock (_streamLock) + { + if (_stream is not { IsConnected: true }) + return; + + Logger.Trace("Beginning Read of {0} bytes", _buffer.Length); + _stream.BeginRead(_buffer, 0, _buffer.Length, EndReadStream, _stream.IsConnected); + } + } + catch (ObjectDisposedException) + { + Logger.Warning("Attempted to start reading from a disposed pipe"); + } + catch (InvalidOperationException) + { + Logger.Warning("Attempted to start reading from a closed pipe"); + } + catch (Exception e) + { + Logger.Error("An exception occurred while starting to read a stream: {0}", e.Message); + Logger.Error(e.StackTrace); + } + } + + private void EndReadStream(IAsyncResult callback) + { + Logger.Trace("Ending Read"); + int bytes; + + try + { + lock (_streamLock) + { + if (_stream is not { IsConnected: true }) + return; + + 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("Attempted to end reading from a disposed pipe"); + return; + } + catch (Exception e) + { + Logger.Error("An exception occurred while ending a read of a stream: {0}", e.Message); + Logger.Error(e.StackTrace); + return; + } + + Logger.Trace("Read {0} bytes", bytes); + + if (bytes > 0) + { + using MemoryStream memory = new(_buffer, 0, bytes); + try + { + PipeFrame frame = new(); + if (frame.ReadStream(memory)) + { + Logger.Trace("Read a frame: {0}", frame.Opcode); + lock (_frameQueueLock) + _frameQueue.Enqueue(frame); + } + else + { + Logger.Error("Pipe failed to read from the data received by the stream."); + Close(); + } + } + catch (Exception e) + { + Logger.Error("An exception has occurred while trying to parse the pipe data: {0}", e.Message); + Close(); + } + } + else + { + Logger.Error("Empty frame was read on {0}, aborting.", Environment.OSVersion); + Close(); + } + + if (!_isClosed && IsConnected) + { + Logger.Trace("Starting another read"); + BeginReadStream(); + } + } + + private static IEnumerable GetPipeCandidates(int startPipe) + { + if (OperatingSystem.IsWindows()) + { + for (int i = startPipe; i < MaximumPipeVariations; i++) + yield return $"{DiscordPipePrefix}{i}"; + + yield break; + } + + foreach (string runtimeDir in GetUnixRuntimeDirectories()) + { + for (int index = startPipe; index < MaximumPipeVariations; index++) + { + string pipeFileName = $"{DiscordPipePrefix}{index}"; + + foreach (string packageDirectory in s_unixPackageDirectories) + { + string packagePipe = Path.Combine(runtimeDir, packageDirectory, pipeFileName); + if (File.Exists(packagePipe)) + yield return packagePipe; + } + + string defaultPipe = Path.Combine(runtimeDir, pipeFileName); + if (File.Exists(defaultPipe)) + yield return defaultPipe; + + foreach (string packageDirectory in s_unixPackageDirectories) + { + string packagePipe = Path.Combine(runtimeDir, packageDirectory, pipeFileName); + if (!File.Exists(packagePipe)) + yield return packagePipe; + } + + if (!File.Exists(defaultPipe)) + yield return defaultPipe; + } + } + } + + private static IEnumerable GetUnixRuntimeDirectories() + { + HashSet yielded = new(StringComparer.Ordinal); + + string[] candidates = + [ + Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR") ?? string.Empty, + Environment.GetEnvironmentVariable("TMPDIR") ?? string.Empty, + Environment.GetEnvironmentVariable("TMP") ?? string.Empty, + Environment.GetEnvironmentVariable("TEMP") ?? string.Empty, + Path.GetTempPath(), + "/tmp" + ]; + + foreach (string candidate in candidates) + { + if (string.IsNullOrWhiteSpace(candidate)) + continue; + + string normalized = candidate.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + if (yielded.Add(normalized)) + yield return normalized; + } + } + } } } diff --git a/MinecraftClient/ChatBots/DiscordRpcPipeClient.cs b/MinecraftClient/ChatBots/DiscordRpcPipeClient.cs deleted file mode 100644 index 61f2e098..00000000 --- a/MinecraftClient/ChatBots/DiscordRpcPipeClient.cs +++ /dev/null @@ -1,399 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Pipes; -using System.Threading; -using DiscordRPC.IO; -using DiscordRPC.Logging; - -namespace MinecraftClient.ChatBots -{ - /// - /// Flatpak Discord exposes the RPC socket under app/com.discordapp.Discord, - /// but the currently published DiscordRichPresence package does not probe that path. - /// - internal sealed class DiscordRpcPipeClient : INamedPipeClient - { - private const string DiscordPipePrefix = "discord-ipc-"; - private const int MaximumPipeVariations = 10; - - private static readonly string[] s_unixPackageDirectories = - [ - // Official Discord clients - "app/com.discordapp.Discord", - "snap.discord", - - // Community desktop clients / wrappers - "app/dev.vencord.Vesktop", - ".flatpak/dev.vencord.Vesktop/xdg-run", - "app/org.equicord.equibop", - "app/io.github.equicord.equibop", - "app/xyz.armcord.ArmCord", - "app/io.github.spacingbat3.webcord" - ]; - - private readonly byte[] _buffer = new byte[PipeFrame.MAX_SIZE]; - private readonly Queue _frameQueue = new(); - private readonly object _frameQueueLock = new(); - private readonly object _streamLock = new(); - - private int _connectedPipe; - private NamedPipeClientStream? _stream; - private volatile bool _isClosed = true; - private volatile bool _isDisposed; - - public ILogger Logger { get; set; } = new NullLogger(); - - public bool IsConnected - { - get - { - if (_isClosed) - return false; - - lock (_streamLock) - return _stream is { IsConnected: true }; - } - } - - [Obsolete("The connected pipe is not neccessary information.")] - public int ConnectedPipe => _connectedPipe; - - public bool Connect(int pipe) - { - Logger.Trace("DiscordRpcPipeClient.Connect({0})", pipe); - - if (_isDisposed) - throw new ObjectDisposedException(nameof(DiscordRpcPipeClient)); - - if (pipe > 9) - throw new ArgumentOutOfRangeException(nameof(pipe), "Argument cannot be greater than 9"); - - int startPipe = pipe >= 0 ? pipe : 0; - - foreach (string pipeName in GetPipeCandidates(startPipe)) - { - if (AttemptConnection(pipeName)) - { - BeginReadStream(); - return true; - } - } - - return false; - } - - public bool ReadFrame(out PipeFrame frame) - { - if (_isDisposed) - throw new ObjectDisposedException(nameof(DiscordRpcPipeClient)); - - lock (_frameQueueLock) - { - if (_frameQueue.Count == 0) - { - frame = default; - return false; - } - - frame = _frameQueue.Dequeue(); - return true; - } - } - - public bool WriteFrame(PipeFrame frame) - { - if (_isDisposed) - throw new ObjectDisposedException(nameof(DiscordRpcPipeClient)); - - if (_isClosed || !IsConnected) - { - Logger.Error("Failed to write frame because the stream is closed"); - return false; - } - - try - { - 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"); - } - - return false; - } - - public void Close() - { - if (_isClosed) - { - Logger.Warning("Tried to close a already closed pipe."); - return; - } - - try - { - lock (_streamLock) - { - if (_stream is not null) - { - try - { - _stream.Flush(); - _stream.Dispose(); - } - catch - { - } - - _stream = null; - _isClosed = true; - } - else - { - Logger.Warning("Stream was closed, but no stream was available to begin with!"); - } - } - } - catch (ObjectDisposedException) - { - Logger.Warning("Tried to dispose already disposed stream"); - } - finally - { - _isClosed = true; - } - } - - public void Dispose() - { - if (_isDisposed) - return; - - if (!_isClosed) - Close(); - - lock (_streamLock) - { - _stream?.Dispose(); - _stream = null; - } - - _isDisposed = true; - } - - private bool AttemptConnection(string pipeName) - { - if (_isDisposed) - throw new ObjectDisposedException(nameof(DiscordRpcPipeClient)); - - try - { - lock (_streamLock) - { - Logger.Info("Attempting to connect to {0}", pipeName); - _stream = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous); - _stream.Connect(0); - - Logger.Trace("Waiting for connection..."); - while (!_stream.IsConnected) - Thread.Sleep(10); - } - - Logger.Info("Connected to {0}", pipeName); - _connectedPipe = int.Parse(pipeName[(pipeName.LastIndexOf('-') + 1)..], System.Globalization.CultureInfo.InvariantCulture); - _isClosed = false; - } - catch (Exception e) - { - Logger.Error("Failed connection to {0}. {1}", pipeName, e.Message); - Close(); - } - - Logger.Trace("Done. Result: {0}", _isClosed); - return !_isClosed; - } - - private void BeginReadStream() - { - if (_isClosed) - return; - - try - { - lock (_streamLock) - { - if (_stream is not { IsConnected: true }) - return; - - Logger.Trace("Beginning Read of {0} bytes", _buffer.Length); - _stream.BeginRead(_buffer, 0, _buffer.Length, EndReadStream, _stream.IsConnected); - } - } - catch (ObjectDisposedException) - { - Logger.Warning("Attempted to start reading from a disposed pipe"); - } - catch (InvalidOperationException) - { - Logger.Warning("Attempted to start reading from a closed pipe"); - } - catch (Exception e) - { - Logger.Error("An exception occurred while starting to read a stream: {0}", e.Message); - Logger.Error(e.StackTrace); - } - } - - private void EndReadStream(IAsyncResult callback) - { - Logger.Trace("Ending Read"); - int bytes; - - try - { - lock (_streamLock) - { - if (_stream is not { IsConnected: true }) - return; - - 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("Attempted to end reading from a disposed pipe"); - return; - } - catch (Exception e) - { - Logger.Error("An exception occurred while ending a read of a stream: {0}", e.Message); - Logger.Error(e.StackTrace); - return; - } - - Logger.Trace("Read {0} bytes", bytes); - - if (bytes > 0) - { - using MemoryStream memory = new(_buffer, 0, bytes); - try - { - PipeFrame frame = new(); - if (frame.ReadStream(memory)) - { - Logger.Trace("Read a frame: {0}", frame.Opcode); - lock (_frameQueueLock) - _frameQueue.Enqueue(frame); - } - else - { - Logger.Error("Pipe failed to read from the data received by the stream."); - Close(); - } - } - catch (Exception e) - { - Logger.Error("An exception has occurred while trying to parse the pipe data: {0}", e.Message); - Close(); - } - } - else - { - Logger.Error("Empty frame was read on {0}, aborting.", Environment.OSVersion); - Close(); - } - - if (!_isClosed && IsConnected) - { - Logger.Trace("Starting another read"); - BeginReadStream(); - } - } - - private static IEnumerable GetPipeCandidates(int startPipe) - { - if (OperatingSystem.IsWindows()) - { - for (int i = startPipe; i < MaximumPipeVariations; i++) - yield return $"{DiscordPipePrefix}{i}"; - - yield break; - } - - foreach (string runtimeDir in GetUnixRuntimeDirectories()) - { - for (int index = startPipe; index < MaximumPipeVariations; index++) - { - string pipeFileName = $"{DiscordPipePrefix}{index}"; - - foreach (string packageDirectory in s_unixPackageDirectories) - { - string packagePipe = Path.Combine(runtimeDir, packageDirectory, pipeFileName); - if (File.Exists(packagePipe)) - yield return packagePipe; - } - - string defaultPipe = Path.Combine(runtimeDir, pipeFileName); - if (File.Exists(defaultPipe)) - yield return defaultPipe; - - foreach (string packageDirectory in s_unixPackageDirectories) - { - string packagePipe = Path.Combine(runtimeDir, packageDirectory, pipeFileName); - if (!File.Exists(packagePipe)) - yield return packagePipe; - } - - if (!File.Exists(defaultPipe)) - yield return defaultPipe; - } - } - } - - private static IEnumerable GetUnixRuntimeDirectories() - { - HashSet yielded = new(StringComparer.Ordinal); - - string[] candidates = - [ - Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR") ?? string.Empty, - Environment.GetEnvironmentVariable("TMPDIR") ?? string.Empty, - Environment.GetEnvironmentVariable("TMP") ?? string.Empty, - Environment.GetEnvironmentVariable("TEMP") ?? string.Empty, - Path.GetTempPath(), - "/tmp" - ]; - - foreach (string candidate in candidates) - { - if (string.IsNullOrWhiteSpace(candidate)) - continue; - - string normalized = candidate.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); - if (yielded.Add(normalized)) - yield return normalized; - } - } - } -}