Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Audio and voice client support #10

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Wumpus.Net.sln
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wumpus.Net.Tests.Server", "
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wumpus.Net.Bot", "src\Wumpus.Net.Bot\Wumpus.Net.Bot.csproj", "{ED977313-7BC8-4A5E-8A24-1BF42635D293}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wumpus.Net.Audio", "src\Wumpus.Net.Audio\Wumpus.Net.Audio.csproj", "{5ABF3E1B-45E9-4CA7-A455-8923C54B480F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -150,6 +152,18 @@ Global
{ED977313-7BC8-4A5E-8A24-1BF42635D293}.Release|x64.Build.0 = Release|Any CPU
{ED977313-7BC8-4A5E-8A24-1BF42635D293}.Release|x86.ActiveCfg = Release|Any CPU
{ED977313-7BC8-4A5E-8A24-1BF42635D293}.Release|x86.Build.0 = Release|Any CPU
{5ABF3E1B-45E9-4CA7-A455-8923C54B480F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5ABF3E1B-45E9-4CA7-A455-8923C54B480F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5ABF3E1B-45E9-4CA7-A455-8923C54B480F}.Debug|x64.ActiveCfg = Debug|Any CPU
{5ABF3E1B-45E9-4CA7-A455-8923C54B480F}.Debug|x64.Build.0 = Debug|Any CPU
{5ABF3E1B-45E9-4CA7-A455-8923C54B480F}.Debug|x86.ActiveCfg = Debug|Any CPU
{5ABF3E1B-45E9-4CA7-A455-8923C54B480F}.Debug|x86.Build.0 = Debug|Any CPU
{5ABF3E1B-45E9-4CA7-A455-8923C54B480F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5ABF3E1B-45E9-4CA7-A455-8923C54B480F}.Release|Any CPU.Build.0 = Release|Any CPU
{5ABF3E1B-45E9-4CA7-A455-8923C54B480F}.Release|x64.ActiveCfg = Release|Any CPU
{5ABF3E1B-45E9-4CA7-A455-8923C54B480F}.Release|x64.Build.0 = Release|Any CPU
{5ABF3E1B-45E9-4CA7-A455-8923C54B480F}.Release|x86.ActiveCfg = Release|Any CPU
{5ABF3E1B-45E9-4CA7-A455-8923C54B480F}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -164,6 +178,7 @@ Global
{0967FF88-E435-4132-9524-B252A3D9BCD6} = {9806905E-1B09-4AC2-BF4F-731C9E473F94}
{C6BE9630-1E1F-4DD1-9205-045A50181AA7} = {9806905E-1B09-4AC2-BF4F-731C9E473F94}
{ED977313-7BC8-4A5E-8A24-1BF42635D293} = {F7B9BFB1-C836-4432-BE64-719A38E0BBEF}
{5ABF3E1B-45E9-4CA7-A455-8923C54B480F} = {F7B9BFB1-C836-4432-BE64-719A38E0BBEF}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B5B02039-F4C2-44C6-91B8-340E44472AED}
Expand Down
15 changes: 15 additions & 0 deletions src/Wumpus.Net.Audio/Events/VoiceHelloEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Voltaic.Serialization;

namespace Wumpus.Events
{
public class VoiceHelloEvent
{
// Given as float because discord returns json of the form
// {"op":8,"d":{"v":4,"heartbeat_interval":13750.0}}
[ModelProperty("heartbeat_interval")]
public float HeartbeatInterval { get; set; }

[ModelProperty("v")]
public int GatewayVersion { get; set; }
}
}
20 changes: 20 additions & 0 deletions src/Wumpus.Net.Audio/Events/VoiceReadyEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Voltaic;
using Voltaic.Serialization;

namespace Wumpus.Events
{
public class VoiceReadyEvent
{
[ModelProperty("ssrc")]
public uint Ssrc { get; set; }

[ModelProperty("port")]
public int Port { get; set; }

[ModelProperty("modes")]
public Utf8String[] EncryptionSchemes { get; set; }

[ModelProperty("ip")]
public Utf8String IpAddress { get; set; }
}
}
23 changes: 23 additions & 0 deletions src/Wumpus.Net.Audio/Events/VoiceSessionDescriptionEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Voltaic;
using Voltaic.Serialization;

namespace Wumpus.Events
{
public class VoiceSessionDescriptionEvent
{
[ModelProperty("video_codec")]
public Utf8String VideoCodec { get; set; }

[ModelProperty("secret_key")]
public byte[] SecretKey { get; set; }

[ModelProperty("mode")]
public Utf8String EncryptionScheme { get; set; }

[ModelProperty("media_session_id")]
public Utf8String VideoSessionId { get; set; }

[ModelProperty("audio_codec")]
public Utf8String AudioCodec { get; set; }
}
}
38 changes: 38 additions & 0 deletions src/Wumpus.Net.Audio/IPUtilities.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using System.Net;
using Voltaic.Serialization.Utf8;

namespace Wumpus
{
public static class IPUtilities
{
public static bool TryParseIPv4Address(ReadOnlySpan<byte> buffer, out IPAddress address)
=> TryParseIPv4Address(ref buffer, out address);

public static bool TryParseIPv4Address(ref ReadOnlySpan<byte> buffer, out IPAddress address)
{
address = default;
ulong value = 0;

for (int i = 0; i < 4; i++)
{
if (!Utf8Reader.TryReadUInt8(ref buffer, out byte section, 'g'))
return false;

value |= (ulong)section << (i * 8);

// last value does not have a dot following it
if (i != 3)
{
if (buffer[0] != '.')
return false;

buffer = buffer.Slice(1);
}
}

address = new IPAddress((long)value);
return true;
}
}
}
17 changes: 17 additions & 0 deletions src/Wumpus.Net.Audio/Requests/VoiceIdentifyParams.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Voltaic;
using Voltaic.Serialization;

namespace Wumpus.Requests
{
public class VoiceIdentifyParams
{
[ModelProperty("user_id")]
public Snowflake UserId { get; set; }
FiniteReality marked this conversation as resolved.
Show resolved Hide resolved
[ModelProperty("server_id")]
public Snowflake GuildId { get; set; }
[ModelProperty("session_id")]
public Utf8String SessionId { get; set; }
[ModelProperty("token")]
public Utf8String Token { get; set; }
}
}
15 changes: 15 additions & 0 deletions src/Wumpus.Net.Audio/Requests/VoiceResumeParams.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Voltaic;
using Voltaic.Serialization;

namespace Wumpus.Requests
{
public class VoiceResumeParams
{
[ModelProperty("server_id")]
public Snowflake GuildId { get; set; }
FiniteReality marked this conversation as resolved.
Show resolved Hide resolved
[ModelProperty("session_id")]
public Utf8String SessionId { get; set; }
[ModelProperty("token")]
public Utf8String Token { get; set; }
}
}
26 changes: 26 additions & 0 deletions src/Wumpus.Net.Audio/Requests/VoiceSelectProtocolParams.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Voltaic;
using Voltaic.Serialization;

namespace Wumpus.Requests
{
public class VoiceSelectProtocolParams
{
[ModelProperty("protocol")]
public Utf8String TransportProtocol { get; set; }

[ModelProperty("data")]
public VoiceSelectProtocolConnectionProperties Properties { get; set; }
}

public class VoiceSelectProtocolConnectionProperties
{
[ModelProperty("ip")]
public Utf8String Ip { get; set; }

[ModelProperty("port")]
public int Port { get; set; }

[ModelProperty("mode")]
public Utf8String EncryptionScheme { get; set; }
}
}
16 changes: 16 additions & 0 deletions src/Wumpus.Net.Audio/Requests/VoiceSpeakingParams.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Voltaic.Serialization;

namespace Wumpus.Requests
{
public class VoiceSpeakingParams
{
[ModelProperty("speaking")]
public int Speaking { get; set; }

[ModelProperty("delay")]
public uint DelayMilliseconds { get; set; }

[ModelProperty("ssrc")]
public uint Ssrc { get; set; }
}
}
56 changes: 56 additions & 0 deletions src/Wumpus.Net.Audio/Sodium/SodiumPrimitives.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System;
using System.Runtime.InteropServices;

namespace Wumpus
{
internal class SodiumPrimitives
{
// sodium.dll on windows, libsodium.so on linux
[DllImport("sodium")]
extern static int crypto_secretbox_macbytes();
[DllImport("sodium")]
extern static int crypto_secretbox_keybytes();
[DllImport("sodium")]
extern static int crypto_secretbox_noncebytes();

[DllImport("sodium")]
extern static unsafe int crypto_secretbox_easy(byte* c, byte* m, int mlen, byte* n, byte* k);
FiniteReality marked this conversation as resolved.
Show resolved Hide resolved
[DllImport("sodium")]
extern static unsafe int crypto_secretbox_open_easy(byte* m, byte* c, int clen, byte* n, byte* k);

[DllImport("sodium")]
extern static unsafe void randombytes_buf(byte* buf, int size);

// use the functions to grab the info so it's not hardcoded in the lib
private static readonly int MACBYTES = crypto_secretbox_macbytes();
private static readonly int KEYBYTES = crypto_secretbox_keybytes();
private static readonly int NONCEBYTES = crypto_secretbox_noncebytes();

public static int NonceSize => NONCEBYTES;

public static int ComputeMessageLength(int messageLength)
=> messageLength + MACBYTES;

public static unsafe bool TryEncryptInPlace(Span<byte> ciphertext, ReadOnlySpan<byte> message, ReadOnlySpan<byte> nonce, ReadOnlySpan<byte> secret)
{
if (ciphertext.Length < message.Length + MACBYTES)
return false;
if (secret.Length < KEYBYTES)
return false;
if (nonce.Length < NONCEBYTES)
return false;

fixed(byte* c = &ciphertext.GetPinnableReference())
fixed(byte* m = &message.GetPinnableReference())
fixed(byte* n = &nonce.GetPinnableReference())
fixed(byte* k = &secret.GetPinnableReference())
return crypto_secretbox_easy(c, m, message.Length, n, k) == 0;
}

public static unsafe void GenerateRandomBytes(Span<byte> buffer)
{
fixed(byte* buf = &buffer.GetPinnableReference())
randombytes_buf(buf, buffer.Length);
}
}
}
15 changes: 15 additions & 0 deletions src/Wumpus.Net.Audio/SpeakingState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;

namespace Wumpus
{
[Flags]
public enum SpeakingState : byte
{
NotSpeaking = 0b0,
Speaking = 0b1,

Priority = 0b100,

PrioritySpeaking = Speaking | Priority
}
}
41 changes: 41 additions & 0 deletions src/Wumpus.Net.Audio/VoiceGatewayOperation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace Wumpus.Events
{
/// <summary>
/// Voice connections operate in a similar fashion to the Gateway connection.
/// However, they use a different set of payloads and a separate UDP-based connection for voice data transmission.
/// https://discordapp.com/developers/docs/topics/voice-connections#voice
/// </summary>
public enum VoiceGatewayOperation : byte
{
/// <summary> C→S - Used to begin a voice websocket connection. </summary>
Identify = 0,
/// <summary> C→S - Used to select the voice protocol. </summary>
SelectProtocol = 1,
/// <summary> C←S - Used to complete the websocket handshake. </summary>
Ready = 2,
/// <summary> C→S - Used to keep the websocket connection alive. </summary>
Heartbeat = 3,
/// <summary> C→S - Used to describe the session. </summary>
SessionDescription = 4,
/// <summary> C↔S - Used to indicate which users are speaking. </summary>
Speaking = 5,
/// <summary> C←S - Used to reply to a heartbeat. </summary>
HeartbeatAck = 6,
/// <summary> C→S - Used to resume a connection. </summary>
Resume = 7,
/// <summary> C→S - Used to begin the websocket handshake. </summary>
Hello = 8,
/// <summary> C←S - Used to complete the websocket handshake with an existing session. </summary>
Resumed = 9

//NOTE: these do not have official names!
//They are documented here for future expansion purposes

//ssrc update, occurs when a user connects or changes screenshare settings
//SsrcUpdate = 12,
//user disconnected, occurs when a user disconnects
//UserDisconnected = 13,
//change channel, occurs whenever the client gets moved into another channel
//ChangeChannel = 14
}
}
33 changes: 33 additions & 0 deletions src/Wumpus.Net.Audio/VoiceGatewayPayload.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using Voltaic.Serialization;
using Wumpus.Entities;
using Wumpus.Requests;

namespace Wumpus.Events
{
public class VoiceGatewayPayload
{
[ModelProperty("op")]
public VoiceGatewayOperation Operation { get; set; }

[ModelProperty("d"),
FiniteReality marked this conversation as resolved.
Show resolved Hide resolved
ModelTypeSelector(nameof(Operation), nameof(OpCodeTypeSelector))]
FiniteReality marked this conversation as resolved.
Show resolved Hide resolved
public object Data { get; set; }

private static Dictionary<VoiceGatewayOperation, Type> OpCodeTypeSelector => new Dictionary<VoiceGatewayOperation, Type>()
{
[VoiceGatewayOperation.Hello] = typeof(VoiceHelloEvent),
[VoiceGatewayOperation.Ready] = typeof(VoiceReadyEvent),
[VoiceGatewayOperation.HeartbeatAck] = typeof(int),

[VoiceGatewayOperation.Identify] = typeof(VoiceIdentifyParams),
[VoiceGatewayOperation.SelectProtocol] = typeof(VoiceSelectProtocolParams),
[VoiceGatewayOperation.SessionDescription] = typeof(VoiceSessionDescriptionEvent),
[VoiceGatewayOperation.Resume] = typeof(VoiceResumeParams),
[VoiceGatewayOperation.Heartbeat] = typeof(int),

[VoiceGatewayOperation.Speaking] = typeof(VoiceSpeakingParams),
};
}
}
16 changes: 16 additions & 0 deletions src/Wumpus.Net.Audio/Wumpus.Net.Audio.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../Wumpus.Net.targets" />
<PropertyGroup>
<RootNamespace>Wumpus</RootNamespace>
<AssemblyName>Wumpus.Net.Audio</AssemblyName>
<PackageTags>discord;discordapp;wumpus;audio;websocket;api;rogueexception</PackageTags>
<Description>Provides a low-level audio client for the Discord API</Description>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Net.WebSockets.Client" Version="4.3.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../Wumpus.Net.Core/Wumpus.Net.Core.csproj" />
</ItemGroup>
</Project>
Loading