Concrete class prototype

This commit is contained in:
RogueException
2016-09-22 21:15:37 -03:00
parent ab42129eb9
commit 6319933ed0
394 changed files with 3648 additions and 3224 deletions

View File

@@ -0,0 +1,246 @@
#pragma warning disable CS1591
using Discord.API.Gateway;
using Discord.API.Rest;
using Discord.Net.Queue;
using Discord.Net.Rest;
using Discord.Net.WebSockets;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Discord.API
{
public class DiscordSocketApiClient : DiscordRestApiClient
{
public event Func<GatewayOpCode, Task> SentGatewayMessage { add { _sentGatewayMessageEvent.Add(value); } remove { _sentGatewayMessageEvent.Remove(value); } }
private readonly AsyncEvent<Func<GatewayOpCode, Task>> _sentGatewayMessageEvent = new AsyncEvent<Func<GatewayOpCode, Task>>();
public event Func<GatewayOpCode, int?, string, object, Task> ReceivedGatewayEvent { add { _receivedGatewayEvent.Add(value); } remove { _receivedGatewayEvent.Remove(value); } }
private readonly AsyncEvent<Func<GatewayOpCode, int?, string, object, Task>> _receivedGatewayEvent = new AsyncEvent<Func<GatewayOpCode, int?, string, object, Task>>();
public event Func<Exception, Task> Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } }
private readonly AsyncEvent<Func<Exception, Task>> _disconnectedEvent = new AsyncEvent<Func<Exception, Task>>();
private readonly IWebSocketClient _gatewayClient;
private CancellationTokenSource _connectCancelToken;
private string _gatewayUrl;
public ConnectionState ConnectionState { get; private set; }
public DiscordSocketApiClient(RestClientProvider restClientProvider, WebSocketProvider webSocketProvider, JsonSerializer serializer = null, RequestQueue requestQueue = null)
: base(restClientProvider, serializer, requestQueue)
{
_gatewayClient = webSocketProvider();
//_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .Net 4.6+)
_gatewayClient.BinaryMessage += async (data, index, count) =>
{
using (var compressed = new MemoryStream(data, index + 2, count - 2))
using (var decompressed = new MemoryStream())
{
using (var zlib = new DeflateStream(compressed, CompressionMode.Decompress))
zlib.CopyTo(decompressed);
decompressed.Position = 0;
using (var reader = new StreamReader(decompressed))
using (var jsonReader = new JsonTextReader(reader))
{
var msg = _serializer.Deserialize<WebSocketMessage>(jsonReader);
await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false);
}
}
};
_gatewayClient.TextMessage += async text =>
{
using (var reader = new StringReader(text))
using (var jsonReader = new JsonTextReader(reader))
{
var msg = _serializer.Deserialize<WebSocketMessage>(jsonReader);
await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false);
}
};
_gatewayClient.Closed += async ex =>
{
await DisconnectAsync().ConfigureAwait(false);
await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false);
};
}
internal override void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing)
{
_connectCancelToken?.Dispose();
(_gatewayClient as IDisposable)?.Dispose();
}
_isDisposed = true;
}
}
public async Task ConnectAsync()
{
await _stateLock.WaitAsync().ConfigureAwait(false);
try
{
await ConnectInternalAsync().ConfigureAwait(false);
}
finally { _stateLock.Release(); }
}
internal override async Task ConnectInternalAsync()
{
if (LoginState != LoginState.LoggedIn)
throw new InvalidOperationException("You must log in before connecting.");
if (_gatewayClient == null)
throw new NotSupportedException("This client is not configured with websocket support.");
ConnectionState = ConnectionState.Connecting;
try
{
_connectCancelToken = new CancellationTokenSource();
if (_gatewayClient != null)
_gatewayClient.SetCancelToken(_connectCancelToken.Token);
if (_gatewayUrl == null)
{
var gatewayResponse = await GetGatewayAsync().ConfigureAwait(false);
_gatewayUrl = $"{gatewayResponse.Url}?v={DiscordConfig.APIVersion}&encoding={DiscordSocketConfig.GatewayEncoding}";
}
await _gatewayClient.ConnectAsync(_gatewayUrl).ConfigureAwait(false);
ConnectionState = ConnectionState.Connected;
}
catch (Exception)
{
_gatewayUrl = null; //Uncache in case the gateway url changed
await DisconnectInternalAsync().ConfigureAwait(false);
throw;
}
}
public async Task DisconnectAsync()
{
await _stateLock.WaitAsync().ConfigureAwait(false);
try
{
await DisconnectInternalAsync().ConfigureAwait(false);
}
finally { _stateLock.Release(); }
}
public async Task DisconnectAsync(Exception ex)
{
await _stateLock.WaitAsync().ConfigureAwait(false);
try
{
await DisconnectInternalAsync().ConfigureAwait(false);
}
finally { _stateLock.Release(); }
}
internal override async Task DisconnectInternalAsync()
{
if (_gatewayClient == null)
throw new NotSupportedException("This client is not configured with websocket support.");
if (ConnectionState == ConnectionState.Disconnected) return;
ConnectionState = ConnectionState.Disconnecting;
try { _connectCancelToken?.Cancel(false); }
catch { }
await _gatewayClient.DisconnectAsync().ConfigureAwait(false);
ConnectionState = ConnectionState.Disconnected;
}
//Core
private async Task SendGatewayInternalAsync(GatewayOpCode opCode, object payload,
BucketGroup group, int bucketId, ulong guildId, RequestOptions options)
{
CheckState();
//TODO: Add ETF
byte[] bytes = null;
payload = new WebSocketMessage { Operation = (int)opCode, Payload = payload };
if (payload != null)
bytes = Encoding.UTF8.GetBytes(SerializeJson(payload));
await RequestQueue.SendAsync(new WebSocketRequest(_gatewayClient, bytes, true, options), group, bucketId, guildId).ConfigureAwait(false);
await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false);
}
//Gateway
public Task SendGatewayAsync(GatewayOpCode opCode, object payload,
GlobalBucket bucket = GlobalBucket.GeneralGateway, RequestOptions options = null)
=> SendGatewayInternalAsync(opCode, payload, BucketGroup.Global, (int)bucket, 0, options);
public Task SendGatewayAsync(GatewayOpCode opCode, object payload,
GuildBucket bucket, ulong guildId, RequestOptions options = null)
=> SendGatewayInternalAsync(opCode, payload, BucketGroup.Guild, (int)bucket, guildId, options);
public async Task<GetGatewayResponse> GetGatewayAsync(RequestOptions options = null)
{
return await SendAsync<GetGatewayResponse>("GET", "gateway", options: options).ConfigureAwait(false);
}
public async Task SendIdentifyAsync(int largeThreshold = 100, bool useCompression = true, int shardID = 0, int totalShards = 1, RequestOptions options = null)
{
var props = new Dictionary<string, string>
{
["$device"] = "Discord.Net"
};
var msg = new IdentifyParams()
{
Token = _authToken,
Properties = props,
LargeThreshold = largeThreshold,
UseCompression = useCompression,
};
if (totalShards > 1)
msg.ShardingParams = new int[] { shardID, totalShards };
await SendGatewayAsync(GatewayOpCode.Identify, msg, options: options).ConfigureAwait(false);
}
public async Task SendResumeAsync(string sessionId, int lastSeq, RequestOptions options = null)
{
var msg = new ResumeParams()
{
Token = _authToken,
SessionId = sessionId,
Sequence = lastSeq
};
await SendGatewayAsync(GatewayOpCode.Resume, msg, options: options).ConfigureAwait(false);
}
public async Task SendHeartbeatAsync(int lastSeq, RequestOptions options = null)
{
await SendGatewayAsync(GatewayOpCode.Heartbeat, lastSeq, options: options).ConfigureAwait(false);
}
public async Task SendStatusUpdateAsync(long? idleSince, Game game, RequestOptions options = null)
{
var args = new StatusUpdateParams
{
IdleSince = idleSince,
Game = game
};
await SendGatewayAsync(GatewayOpCode.StatusUpdate, args, options: options).ConfigureAwait(false);
}
public async Task SendRequestMembersAsync(IEnumerable<ulong> guildIds, RequestOptions options = null)
{
await SendGatewayAsync(GatewayOpCode.RequestGuildMembers, new RequestMembersParams { GuildIds = guildIds, Query = "", Limit = 0 }, options: options).ConfigureAwait(false);
}
public async Task SendVoiceStateUpdateAsync(ulong guildId, ulong? channelId, bool selfDeaf, bool selfMute, RequestOptions options = null)
{
var payload = new VoiceStateUpdateParams
{
GuildId = guildId,
ChannelId = channelId,
SelfDeaf = selfDeaf,
SelfMute = selfMute
};
await SendGatewayAsync(GatewayOpCode.VoiceStateUpdate, payload, options: options).ConfigureAwait(false);
}
public async Task SendGuildSyncAsync(IEnumerable<ulong> guildIds, RequestOptions options = null)
{
await SendGatewayAsync(GatewayOpCode.GuildSync, guildIds, options: options).ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,259 @@
#pragma warning disable CS1591
using Discord.API;
using Discord.API.Voice;
using Discord.Net.Converters;
using Discord.Net.WebSockets;
using Newtonsoft.Json;
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Net.Sockets;
using System.Net;
namespace Discord.Audio
{
public class DiscordVoiceAPIClient
{
public const int MaxBitrate = 128;
public const string Mode = "xsalsa20_poly1305";
public event Func<string, string, double, Task> SentRequest { add { _sentRequestEvent.Add(value); } remove { _sentRequestEvent.Remove(value); } }
private readonly AsyncEvent<Func<string, string, double, Task>> _sentRequestEvent = new AsyncEvent<Func<string, string, double, Task>>();
public event Func<VoiceOpCode, Task> SentGatewayMessage { add { _sentGatewayMessageEvent.Add(value); } remove { _sentGatewayMessageEvent.Remove(value); } }
private readonly AsyncEvent<Func<VoiceOpCode, Task>> _sentGatewayMessageEvent = new AsyncEvent<Func<VoiceOpCode, Task>>();
public event Func<Task> SentDiscovery { add { _sentDiscoveryEvent.Add(value); } remove { _sentDiscoveryEvent.Remove(value); } }
private readonly AsyncEvent<Func<Task>> _sentDiscoveryEvent = new AsyncEvent<Func<Task>>();
public event Func<int, Task> SentData { add { _sentDataEvent.Add(value); } remove { _sentDataEvent.Remove(value); } }
private readonly AsyncEvent<Func<int, Task>> _sentDataEvent = new AsyncEvent<Func<int, Task>>();
public event Func<VoiceOpCode, object, Task> ReceivedEvent { add { _receivedEvent.Add(value); } remove { _receivedEvent.Remove(value); } }
private readonly AsyncEvent<Func<VoiceOpCode, object, Task>> _receivedEvent = new AsyncEvent<Func<VoiceOpCode, object, Task>>();
public event Func<byte[], Task> ReceivedPacket { add { _receivedPacketEvent.Add(value); } remove { _receivedPacketEvent.Remove(value); } }
private readonly AsyncEvent<Func<byte[], Task>> _receivedPacketEvent = new AsyncEvent<Func<byte[], Task>>();
public event Func<Exception, Task> Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } }
private readonly AsyncEvent<Func<Exception, Task>> _disconnectedEvent = new AsyncEvent<Func<Exception, Task>>();
private readonly JsonSerializer _serializer;
private readonly IWebSocketClient _webSocketClient;
private readonly SemaphoreSlim _connectionLock;
private CancellationTokenSource _connectCancelToken;
private UdpClient _udp;
private IPEndPoint _udpEndpoint;
private Task _udpRecieveTask;
private bool _isDisposed;
public ulong GuildId { get; }
public ConnectionState ConnectionState { get; private set; }
internal DiscordVoiceAPIClient(ulong guildId, WebSocketProvider webSocketProvider, JsonSerializer serializer = null)
{
GuildId = guildId;
_connectionLock = new SemaphoreSlim(1, 1);
_udp = new UdpClient(new IPEndPoint(IPAddress.Any, 0));
_webSocketClient = webSocketProvider();
//_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .Net 4.6+)
_webSocketClient.BinaryMessage += async (data, index, count) =>
{
using (var compressed = new MemoryStream(data, index + 2, count - 2))
using (var decompressed = new MemoryStream())
{
using (var zlib = new DeflateStream(compressed, CompressionMode.Decompress))
zlib.CopyTo(decompressed);
decompressed.Position = 0;
using (var reader = new StreamReader(decompressed))
{
var msg = JsonConvert.DeserializeObject<WebSocketMessage>(reader.ReadToEnd());
await _receivedEvent.InvokeAsync((VoiceOpCode)msg.Operation, msg.Payload).ConfigureAwait(false);
}
}
};
_webSocketClient.TextMessage += async text =>
{
var msg = JsonConvert.DeserializeObject<WebSocketMessage>(text);
await _receivedEvent.InvokeAsync((VoiceOpCode)msg.Operation, msg.Payload).ConfigureAwait(false);
};
_webSocketClient.Closed += async ex =>
{
await DisconnectAsync().ConfigureAwait(false);
await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false);
};
_serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() };
}
private void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing)
{
_connectCancelToken?.Dispose();
(_webSocketClient as IDisposable)?.Dispose();
}
_isDisposed = true;
}
}
public void Dispose() => Dispose(true);
public async Task SendAsync(VoiceOpCode opCode, object payload, RequestOptions options = null)
{
byte[] bytes = null;
payload = new WebSocketMessage { Operation = (int)opCode, Payload = payload };
if (payload != null)
bytes = Encoding.UTF8.GetBytes(SerializeJson(payload));
await _webSocketClient.SendAsync(bytes, 0, bytes.Length, true).ConfigureAwait(false);
await _sentGatewayMessageEvent.InvokeAsync(opCode);
}
public async Task SendAsync(byte[] data, int bytes)
{
if (_udpEndpoint != null)
{
await _udp.SendAsync(data, bytes, _udpEndpoint).ConfigureAwait(false);
await _sentDataEvent.InvokeAsync(bytes).ConfigureAwait(false);
}
}
//WebSocket
public async Task SendHeartbeatAsync(RequestOptions options = null)
{
await SendAsync(VoiceOpCode.Heartbeat, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), options: options).ConfigureAwait(false);
}
public async Task SendIdentityAsync(ulong userId, string sessionId, string token)
{
await SendAsync(VoiceOpCode.Identify, new IdentifyParams
{
GuildId = GuildId,
UserId = userId,
SessionId = sessionId,
Token = token
});
}
public async Task SendSelectProtocol(string externalIp, int externalPort)
{
await SendAsync(VoiceOpCode.SelectProtocol, new SelectProtocolParams
{
Protocol = "udp",
Data = new UdpProtocolInfo
{
Address = externalIp,
Port = externalPort,
Mode = Mode
}
});
}
public async Task SendSetSpeaking(bool value)
{
await SendAsync(VoiceOpCode.Speaking, new SpeakingParams
{
IsSpeaking = value,
Delay = 0
});
}
public async Task ConnectAsync(string url)
{
await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
await ConnectInternalAsync(url).ConfigureAwait(false);
}
finally { _connectionLock.Release(); }
}
private async Task ConnectInternalAsync(string url)
{
ConnectionState = ConnectionState.Connecting;
try
{
_connectCancelToken = new CancellationTokenSource();
_webSocketClient.SetCancelToken(_connectCancelToken.Token);
await _webSocketClient.ConnectAsync(url).ConfigureAwait(false);
_udpRecieveTask = ReceiveAsync(_connectCancelToken.Token);
ConnectionState = ConnectionState.Connected;
}
catch (Exception)
{
await DisconnectInternalAsync().ConfigureAwait(false);
throw;
}
}
public async Task DisconnectAsync()
{
await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
await DisconnectInternalAsync().ConfigureAwait(false);
}
finally { _connectionLock.Release(); }
}
private async Task DisconnectInternalAsync()
{
if (ConnectionState == ConnectionState.Disconnected) return;
ConnectionState = ConnectionState.Disconnecting;
try { _connectCancelToken?.Cancel(false); }
catch { }
//Wait for tasks to complete
await _udpRecieveTask.ConfigureAwait(false);
await _webSocketClient.DisconnectAsync().ConfigureAwait(false);
ConnectionState = ConnectionState.Disconnected;
}
//Udp
public async Task SendDiscoveryAsync(uint ssrc)
{
var packet = new byte[70];
packet[0] = (byte)(ssrc >> 24);
packet[1] = (byte)(ssrc >> 16);
packet[2] = (byte)(ssrc >> 8);
packet[3] = (byte)(ssrc >> 0);
await SendAsync(packet, 70).ConfigureAwait(false);
await _sentDiscoveryEvent.InvokeAsync().ConfigureAwait(false);
}
public void SetUdpEndpoint(IPEndPoint endpoint)
{
_udpEndpoint = endpoint;
}
private async Task ReceiveAsync(CancellationToken cancelToken)
{
var closeTask = Task.Delay(-1, cancelToken);
while (!cancelToken.IsCancellationRequested)
{
var receiveTask = _udp.ReceiveAsync();
var task = await Task.WhenAny(closeTask, receiveTask).ConfigureAwait(false);
if (task == closeTask)
break;
await _receivedPacketEvent.InvokeAsync(receiveTask.Result.Buffer).ConfigureAwait(false);
}
}
//Helpers
private static double ToMilliseconds(Stopwatch stopwatch) => Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2);
private string SerializeJson(object value)
{
var sb = new StringBuilder(256);
using (TextWriter text = new StringWriter(sb, CultureInfo.InvariantCulture))
using (JsonWriter writer = new JsonTextWriter(text))
_serializer.Serialize(writer, value);
return sb.ToString();
}
private T DeserializeJson<T>(Stream jsonStream)
{
using (TextReader text = new StreamReader(jsonStream))
using (JsonReader reader = new JsonTextReader(text))
return _serializer.Deserialize<T>(reader);
}
}
}

View File

@@ -0,0 +1,25 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
using System;
namespace Discord.API.Gateway
{
public class ExtendedGuild : Guild
{
[JsonProperty("unavailable")]
public bool? Unavailable { get; set; }
[JsonProperty("member_count")]
public int MemberCount { get; set; }
[JsonProperty("large")]
public bool Large { get; set; }
[JsonProperty("presences")]
public Presence[] Presences { get; set; }
[JsonProperty("members")]
public GuildMember[] Members { get; set; }
[JsonProperty("channels")]
public Channel[] Channels { get; set; }
[JsonProperty("joined_at")]
public DateTimeOffset JoinedAt { get; set; }
}
}

View File

@@ -0,0 +1,33 @@
#pragma warning disable CS1591
namespace Discord.API.Gateway
{
public enum GatewayOpCode : byte
{
/// <summary> C←S - Used to send most events. </summary>
Dispatch = 0,
/// <summary> C↔S - Used to keep the connection alive and measure latency. </summary>
Heartbeat = 1,
/// <summary> C→S - Used to associate a connection with a token and specify configuration. </summary>
Identify = 2,
/// <summary> C→S - Used to update client's status and current game id. </summary>
StatusUpdate = 3,
/// <summary> C→S - Used to join a particular voice channel. </summary>
VoiceStateUpdate = 4,
/// <summary> C→S - Used to ensure the guild's voice server is alive. </summary>
VoiceServerPing = 5,
/// <summary> C→S - Used to resume a connection after a redirect occurs. </summary>
Resume = 6,
/// <summary> C←S - Used to notify a client that they must reconnect to another gateway. </summary>
Reconnect = 7,
/// <summary> C→S - Used to request members that were withheld by large_threshold </summary>
RequestGuildMembers = 8,
/// <summary> C←S - Used to notify the client that their session has expired and cannot be resumed. </summary>
InvalidSession = 9,
/// <summary> C←S - Used to provide information to the client immediately on connection. </summary>
Hello = 10,
/// <summary> C←S - Used to reply to a client's heartbeat. </summary>
HeartbeatAck = 11,
/// <summary> C→S - Used to request presence updates from particular guilds. </summary>
GuildSync = 12
}
}

View File

@@ -0,0 +1,13 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway
{
public class GuildBanEvent
{
[JsonProperty("guild_id")]
public ulong GuildId { get; set; }
[JsonProperty("user")]
public User User { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway
{
public class GuildEmojiUpdateEvent
{
[JsonProperty("guild_id")]
public ulong GuildId { get; set; }
[JsonProperty("emojis")]
public Emoji[] Emojis { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway
{
public class GuildMemberAddEvent : GuildMember
{
[JsonProperty("guild_id")]
public ulong GuildId { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway
{
public class GuildMemberRemoveEvent
{
[JsonProperty("guild_id")]
public ulong GuildId { get; set; }
[JsonProperty("user")]
public User User { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway
{
public class GuildMemberUpdateEvent : GuildMember
{
[JsonProperty("guild_id")]
public ulong GuildId { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway
{
public class GuildMembersChunkEvent
{
[JsonProperty("guild_id")]
public ulong GuildId { get; set; }
[JsonProperty("members")]
public GuildMember[] Members { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway
{
public class GuildRoleCreateEvent
{
[JsonProperty("guild_id")]
public ulong GuildId { get; set; }
[JsonProperty("role")]
public Role Role { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway
{
public class GuildRoleDeleteEvent
{
[JsonProperty("guild_id")]
public ulong GuildId { get; set; }
[JsonProperty("role_id")]
public ulong RoleId { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway
{
public class GuildRoleUpdateEvent
{
[JsonProperty("guild_id")]
public ulong GuildId { get; set; }
[JsonProperty("role")]
public Role Role { get; set; }
}
}

View File

@@ -0,0 +1,18 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway
{
public class GuildSyncEvent
{
[JsonProperty("id")]
public ulong Id { get; set; }
[JsonProperty("large")]
public bool Large { get; set; }
[JsonProperty("presences")]
public Presence[] Presences { get; set; }
[JsonProperty("members")]
public GuildMember[] Members { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway
{
public class HelloEvent
{
[JsonProperty("heartbeat_interval")]
public int HeartbeatInterval { get; set; }
}
}

View File

@@ -0,0 +1,21 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
using System.Collections.Generic;
namespace Discord.API.Gateway
{
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
public class IdentifyParams
{
[JsonProperty("token")]
public string Token { get; set; }
[JsonProperty("properties")]
public IDictionary<string, string> Properties { get; set; }
[JsonProperty("large_threshold")]
public int LargeThreshold { get; set; }
[JsonProperty("compress")]
public bool UseCompression { get; set; }
[JsonProperty("shard")]
public Optional<int[]> ShardingParams { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
using System.Collections.Generic;
namespace Discord.API.Gateway
{
public class MessageDeleteBulkEvent
{
[JsonProperty("channel_id")]
public ulong ChannelId { get; set; }
[JsonProperty("ids")]
public IEnumerable<ulong> Ids { get; set; }
}
}

View File

@@ -0,0 +1,38 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway
{
public class ReadyEvent
{
public class ReadState
{
[JsonProperty("id")]
public string ChannelId { get; set; }
[JsonProperty("mention_count")]
public int MentionCount { get; set; }
[JsonProperty("last_message_id")]
public string LastMessageId { get; set; }
}
[JsonProperty("v")]
public int Version { get; set; }
[JsonProperty("user")]
public User User { get; set; }
[JsonProperty("session_id")]
public string SessionId { get; set; }
[JsonProperty("read_state")]
public ReadState[] ReadStates { get; set; }
[JsonProperty("guilds")]
public ExtendedGuild[] Guilds { get; set; }
[JsonProperty("private_channels")]
public Channel[] PrivateChannels { get; set; }
[JsonProperty("relationships")]
public Relationship[] Relationships { get; set; }
//Ignored
/*[JsonProperty("user_settings")]
[JsonProperty("user_guild_settings")]
[JsonProperty("tutorial")]*/
}
}

View File

@@ -0,0 +1,13 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway
{
public class RecipientEvent
{
[JsonProperty("user")]
public User User { get; set; }
[JsonProperty("channel_id")]
public ulong ChannelId { get; set; }
}
}

View File

@@ -0,0 +1,21 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Linq;
namespace Discord.API.Gateway
{
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
public class RequestMembersParams
{
[JsonProperty("query")]
public string Query { get; set; }
[JsonProperty("limit")]
public int Limit { get; set; }
[JsonProperty("guild_id")]
private ulong[] _guildIds { get; set; }
public IEnumerable<ulong> GuildIds { set { _guildIds = value.ToArray(); } }
public IEnumerable<IGuild> Guilds { set { _guildIds = value.Select(x => x.Id).ToArray(); } }
}
}

View File

@@ -0,0 +1,16 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway
{
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
public class ResumeParams
{
[JsonProperty("token")]
public string Token { get; set; }
[JsonProperty("session_id")]
public string SessionId { get; set; }
[JsonProperty("seq")]
public int Sequence { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway
{
public class ResumedEvent
{
[JsonProperty("heartbeat_interval")]
public int HeartbeatInterval { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway
{
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
public class StatusUpdateParams
{
[JsonProperty("idle_since"), Int53]
public long? IdleSince { get; set; }
[JsonProperty("game")]
public Game Game { get; set; }
}
}

View File

@@ -0,0 +1,15 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway
{
public class TypingStartEvent
{
[JsonProperty("user_id")]
public ulong UserId { get; set; }
[JsonProperty("channel_id")]
public ulong ChannelId { get; set; }
[JsonProperty("timestamp")]
public int Timestamp { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway
{
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
public class UpdateStatusParams
{
[JsonProperty("idle_since")]
public long? IdleSince { get; set; }
[JsonProperty("game")]
public Game Game { get; set; }
}
}

View File

@@ -0,0 +1,15 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway
{
public class VoiceServerUpdateEvent
{
[JsonProperty("guild_id")]
public ulong GuildId { get; set; }
[JsonProperty("endpoint")]
public string Endpoint { get; set; }
[JsonProperty("token")]
public string Token { get; set; }
}
}

View File

@@ -0,0 +1,22 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Gateway
{
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
public class VoiceStateUpdateParams
{
[JsonProperty("self_mute")]
public bool SelfMute { get; set; }
[JsonProperty("self_deaf")]
public bool SelfDeaf { get; set; }
[JsonProperty("guild_id")]
public ulong? GuildId { get; set; }
public IGuild Guild { set { GuildId = value?.Id; } }
[JsonProperty("channel_id")]
public ulong? ChannelId { get; set; }
public IChannel Channel { set { ChannelId = value?.Id; } }
}
}

View File

@@ -0,0 +1,17 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Voice
{
public class IdentifyParams
{
[JsonProperty("server_id")]
public ulong GuildId { get; set; }
[JsonProperty("user_id")]
public ulong UserId { get; set; }
[JsonProperty("session_id")]
public string SessionId { get; set; }
[JsonProperty("token")]
public string Token { get; set; }
}
}

View File

@@ -0,0 +1,17 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Voice
{
public class ReadyEvent
{
[JsonProperty("ssrc")]
public uint SSRC { get; set; }
[JsonProperty("port")]
public ushort Port { get; set; }
[JsonProperty("modes")]
public string[] Modes { get; set; }
[JsonProperty("heartbeat_interval")]
public int HeartbeatInterval { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Voice
{
public class SelectProtocolParams
{
[JsonProperty("protocol")]
public string Protocol { get; set; }
[JsonProperty("data")]
public UdpProtocolInfo Data { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Voice
{
public class SessionDescriptionEvent
{
[JsonProperty("secret_key")]
public byte[] SecretKey { get; set; }
[JsonProperty("mode")]
public string Mode { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Voice
{
public class SpeakingParams
{
[JsonProperty("speaking")]
public bool IsSpeaking { get; set; }
[JsonProperty("delay")]
public int Delay { get; set; }
}
}

View File

@@ -0,0 +1,15 @@
#pragma warning disable CS1591
using Newtonsoft.Json;
namespace Discord.API.Voice
{
public class UdpProtocolInfo
{
[JsonProperty("address")]
public string Address { get; set; }
[JsonProperty("port")]
public int Port { get; set; }
[JsonProperty("mode")]
public string Mode { get; set; }
}
}

View File

@@ -0,0 +1,21 @@
#pragma warning disable CS1591
namespace Discord.API.Voice
{
public enum VoiceOpCode : byte
{
/// <summary> C→S - Used to associate a connection with a token. </summary>
Identify = 0,
/// <summary> C→S - Used to specify configuration. </summary>
SelectProtocol = 1,
/// <summary> C←S - Used to notify that the voice connection was successful and informs the client of available protocols. </summary>
Ready = 2,
/// <summary> C→S - Used to keep the connection alive and measure latency. </summary>
Heartbeat = 3,
/// <summary> C←S - Used to reply to a client's heartbeat. </summary>
HeartbeatAck = 3,
/// <summary> C←S - Used to provide an encryption key to the client. </summary>
SessionDescription = 4,
/// <summary> C↔S - Used to inform that a certain user is speaking. </summary>
Speaking = 5
}
}

View File

@@ -0,0 +1,331 @@
using Discord.API.Voice;
using Discord.Logging;
using Discord.Net.Converters;
using Discord.WebSocket;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Discord.Audio
{
internal class AudioClient : IAudioClient, IDisposable
{
public event Func<Task> Connected
{
add { _connectedEvent.Add(value); }
remove { _connectedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<Task>> _connectedEvent = new AsyncEvent<Func<Task>>();
public event Func<Exception, Task> Disconnected
{
add { _disconnectedEvent.Add(value); }
remove { _disconnectedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<Exception, Task>> _disconnectedEvent = new AsyncEvent<Func<Exception, Task>>();
public event Func<int, int, Task> LatencyUpdated
{
add { _latencyUpdatedEvent.Add(value); }
remove { _latencyUpdatedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<int, int, Task>> _latencyUpdatedEvent = new AsyncEvent<Func<int, int, Task>>();
private readonly ILogger _audioLogger;
#if BENCHMARK
private readonly ILogger _benchmarkLogger;
#endif
internal readonly SemaphoreSlim _connectionLock;
private readonly JsonSerializer _serializer;
private TaskCompletionSource<bool> _connectTask;
private CancellationTokenSource _cancelToken;
private Task _heartbeatTask;
private long _heartbeatTime;
private string _url;
private bool _isDisposed;
private uint _ssrc;
private byte[] _secretKey;
public SocketGuild Guild { get; }
public DiscordVoiceAPIClient ApiClient { get; private set; }
public ConnectionState ConnectionState { get; private set; }
public int Latency { get; private set; }
private DiscordSocketClient Discord => Guild.Discord;
/// <summary> Creates a new REST/WebSocket discord client. </summary>
public AudioClient(SocketGuild guild, int id)
{
Guild = guild;
_audioLogger = Discord.LogManager.CreateLogger($"Audio #{id}");
#if BENCHMARK
_benchmarkLogger = logManager.CreateLogger("Benchmark");
#endif
_connectionLock = new SemaphoreSlim(1, 1);
_serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() };
_serializer.Error += (s, e) =>
{
_audioLogger.WarningAsync(e.ErrorContext.Error).GetAwaiter().GetResult();
e.ErrorContext.Handled = true;
};
ApiClient = new DiscordVoiceAPIClient(guild.Id, Discord.WebSocketProvider);
ApiClient.SentGatewayMessage += async opCode => await _audioLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false);
ApiClient.SentDiscovery += async () => await _audioLogger.DebugAsync($"Sent Discovery").ConfigureAwait(false);
ApiClient.SentData += async bytes => await _audioLogger.DebugAsync($"Sent {bytes} Bytes").ConfigureAwait(false);
ApiClient.ReceivedEvent += ProcessMessageAsync;
ApiClient.ReceivedPacket += ProcessPacketAsync;
ApiClient.Disconnected += async ex =>
{
if (ex != null)
await _audioLogger.WarningAsync($"Connection Closed", ex).ConfigureAwait(false);
else
await _audioLogger.WarningAsync($"Connection Closed").ConfigureAwait(false);
};
LatencyUpdated += async (old, val) => await _audioLogger.VerboseAsync($"Latency = {val} ms").ConfigureAwait(false);
}
/// <inheritdoc />
public async Task ConnectAsync(string url, ulong userId, string sessionId, string token)
{
await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
await ConnectInternalAsync(url, userId, sessionId, token).ConfigureAwait(false);
}
finally { _connectionLock.Release(); }
}
private async Task ConnectInternalAsync(string url, ulong userId, string sessionId, string token)
{
var state = ConnectionState;
if (state == ConnectionState.Connecting || state == ConnectionState.Connected)
await DisconnectInternalAsync(null).ConfigureAwait(false);
ConnectionState = ConnectionState.Connecting;
await _audioLogger.InfoAsync("Connecting").ConfigureAwait(false);
try
{
_url = url;
_connectTask = new TaskCompletionSource<bool>();
_cancelToken = new CancellationTokenSource();
await ApiClient.ConnectAsync("wss://" + url).ConfigureAwait(false);
await ApiClient.SendIdentityAsync(userId, sessionId, token).ConfigureAwait(false);
await _connectTask.Task.ConfigureAwait(false);
await _connectedEvent.InvokeAsync().ConfigureAwait(false);
ConnectionState = ConnectionState.Connected;
await _audioLogger.InfoAsync("Connected").ConfigureAwait(false);
}
catch (Exception)
{
await DisconnectInternalAsync(null).ConfigureAwait(false);
throw;
}
}
/// <inheritdoc />
public async Task DisconnectAsync()
{
await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
await DisconnectInternalAsync(null).ConfigureAwait(false);
}
finally { _connectionLock.Release(); }
}
private async Task DisconnectAsync(Exception ex)
{
await _connectionLock.WaitAsync().ConfigureAwait(false);
try
{
await DisconnectInternalAsync(ex).ConfigureAwait(false);
}
finally { _connectionLock.Release(); }
}
private async Task DisconnectInternalAsync(Exception ex)
{
if (ConnectionState == ConnectionState.Disconnected) return;
ConnectionState = ConnectionState.Disconnecting;
await _audioLogger.InfoAsync("Disconnecting").ConfigureAwait(false);
//Signal tasks to complete
try { _cancelToken.Cancel(); } catch { }
//Disconnect from server
await ApiClient.DisconnectAsync().ConfigureAwait(false);
//Wait for tasks to complete
var heartbeatTask = _heartbeatTask;
if (heartbeatTask != null)
await heartbeatTask.ConfigureAwait(false);
_heartbeatTask = null;
ConnectionState = ConnectionState.Disconnected;
await _audioLogger.InfoAsync("Disconnected").ConfigureAwait(false);
await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false);
}
public void Send(byte[] data, int count)
{
//TODO: Queue these?
ApiClient.SendAsync(data, count).ConfigureAwait(false);
}
public RTPWriteStream CreateOpusStream(int samplesPerFrame, int bufferSize = 4000)
{
return new RTPWriteStream(this, _secretKey, samplesPerFrame, _ssrc, bufferSize = 4000);
}
public OpusEncodeStream CreatePCMStream(int samplesPerFrame, int? bitrate = null,
OpusApplication application = OpusApplication.MusicOrMixed, int bufferSize = 4000)
{
return new OpusEncodeStream(this, _secretKey, samplesPerFrame, _ssrc, bitrate, application, bufferSize);
}
private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload)
{
#if BENCHMARK
Stopwatch stopwatch = Stopwatch.StartNew();
try
{
#endif
try
{
switch (opCode)
{
case VoiceOpCode.Ready:
{
await _audioLogger.DebugAsync("Received Ready").ConfigureAwait(false);
var data = (payload as JToken).ToObject<ReadyEvent>(_serializer);
_ssrc = data.SSRC;
if (!data.Modes.Contains(DiscordVoiceAPIClient.Mode))
throw new InvalidOperationException($"Discord does not support {DiscordVoiceAPIClient.Mode}");
_heartbeatTime = 0;
_heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _cancelToken.Token);
var entry = await Dns.GetHostEntryAsync(_url).ConfigureAwait(false);
ApiClient.SetUdpEndpoint(new IPEndPoint(entry.AddressList[0], data.Port));
await ApiClient.SendDiscoveryAsync(_ssrc).ConfigureAwait(false);
}
break;
case VoiceOpCode.SessionDescription:
{
await _audioLogger.DebugAsync("Received SessionDescription").ConfigureAwait(false);
var data = (payload as JToken).ToObject<SessionDescriptionEvent>(_serializer);
if (data.Mode != DiscordVoiceAPIClient.Mode)
throw new InvalidOperationException($"Discord selected an unexpected mode: {data.Mode}");
_secretKey = data.SecretKey;
await ApiClient.SendSetSpeaking(true).ConfigureAwait(false);
var _ = _connectTask.TrySetResultAsync(true);
}
break;
case VoiceOpCode.HeartbeatAck:
{
await _audioLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false);
var heartbeatTime = _heartbeatTime;
if (heartbeatTime != 0)
{
int latency = (int)(Environment.TickCount - _heartbeatTime);
_heartbeatTime = 0;
int before = Latency;
Latency = latency;
await _latencyUpdatedEvent.InvokeAsync(before, latency).ConfigureAwait(false);
}
}
break;
default:
await _audioLogger.WarningAsync($"Unknown OpCode ({opCode})").ConfigureAwait(false);
return;
}
}
catch (Exception ex)
{
await _audioLogger.ErrorAsync($"Error handling {opCode}", ex).ConfigureAwait(false);
return;
}
#if BENCHMARK
}
finally
{
stopwatch.Stop();
double millis = Math.Round(stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2);
await _benchmarkLogger.DebugAsync($"{millis} ms").ConfigureAwait(false);
}
#endif
}
private async Task ProcessPacketAsync(byte[] packet)
{
if (!_connectTask.Task.IsCompleted)
{
if (packet.Length == 70)
{
string ip;
int port;
try
{
ip = Encoding.UTF8.GetString(packet, 4, 70 - 6).TrimEnd('\0');
port = packet[69] | (packet[68] << 8);
}
catch { return; }
await _audioLogger.DebugAsync("Received Discovery").ConfigureAwait(false);
await ApiClient.SendSelectProtocol(ip, port);
}
}
}
private async Task RunHeartbeatAsync(int intervalMillis, CancellationToken cancelToken)
{
//Clean this up when Discord's session patch is live
try
{
while (!cancelToken.IsCancellationRequested)
{
await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false);
if (_heartbeatTime != 0) //Server never responded to our last heartbeat
{
if (ConnectionState == ConnectionState.Connected)
{
await _audioLogger.WarningAsync("Server missed last heartbeat").ConfigureAwait(false);
await DisconnectInternalAsync(new Exception("Server missed last heartbeat")).ConfigureAwait(false);
return;
}
}
else
_heartbeatTime = Environment.TickCount;
await ApiClient.SendHeartbeatAsync().ConfigureAwait(false);
}
}
catch (OperationCanceledException) { }
}
internal virtual void Dispose(bool disposing)
{
if (!_isDisposed)
_isDisposed = true;
ApiClient.Dispose();
}
/// <inheritdoc />
public void Dispose() => Dispose(true);
}
}

View File

@@ -0,0 +1,13 @@
using System;
namespace Discord.Audio
{
[Flags]
public enum AudioMode : byte
{
Disabled = 0,
Outgoing = 1,
Incoming = 2,
Both = Outgoing | Incoming
}
}

View File

@@ -0,0 +1,51 @@
using System;
namespace Discord.Audio
{
internal abstract class OpusConverter : IDisposable
{
protected IntPtr _ptr;
/// <summary> Gets the bit rate of this converter. </summary>
public const int BitsPerSample = 16;
/// <summary> Gets the bytes per sample. </summary>
public const int SampleSize = (BitsPerSample / 8) * MaxChannels;
/// <summary> Gets the maximum amount of channels this encoder supports. </summary>
public const int MaxChannels = 2;
/// <summary> Gets the input sampling rate of this converter. </summary>
public int SamplingRate { get; }
/// <summary> Gets the number of samples per second for this stream. </summary>
public int Channels { get; }
protected OpusConverter(int samplingRate, int channels)
{
if (samplingRate != 8000 && samplingRate != 12000 &&
samplingRate != 16000 && samplingRate != 24000 &&
samplingRate != 48000)
throw new ArgumentOutOfRangeException(nameof(samplingRate));
if (channels != 1 && channels != 2)
throw new ArgumentOutOfRangeException(nameof(channels));
SamplingRate = samplingRate;
Channels = channels;
}
private bool disposedValue = false; // To detect redundant calls
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
disposedValue = true;
}
~OpusConverter()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}

View File

@@ -0,0 +1,10 @@
namespace Discord.Audio
{
internal enum OpusCtl : int
{
SetBitrateRequest = 4002,
GetBitrateRequest = 4003,
SetInbandFECRequest = 4012,
GetInbandFECRequest = 4013
}
}

View File

@@ -0,0 +1,49 @@
using System;
using System.Runtime.InteropServices;
namespace Discord.Audio
{
internal unsafe class OpusDecoder : OpusConverter
{
[DllImport("opus", EntryPoint = "opus_decoder_create", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr CreateDecoder(int Fs, int channels, out OpusError error);
[DllImport("opus", EntryPoint = "opus_decoder_destroy", CallingConvention = CallingConvention.Cdecl)]
private static extern void DestroyDecoder(IntPtr decoder);
[DllImport("opus", EntryPoint = "opus_decode", CallingConvention = CallingConvention.Cdecl)]
private static extern int Decode(IntPtr st, byte* data, int len, byte* pcm, int max_frame_size, int decode_fec);
public OpusDecoder(int samplingRate, int channels)
: base(samplingRate, channels)
{
OpusError error;
_ptr = CreateDecoder(samplingRate, channels, out error);
if (error != OpusError.OK)
throw new Exception($"Opus Error: {error}");
}
/// <summary> Produces PCM samples from Opus-encoded audio. </summary>
/// <param name="input">PCM samples to decode.</param>
/// <param name="inputOffset">Offset of the frame in input.</param>
/// <param name="output">Buffer to store the decoded frame.</param>
public unsafe int DecodeFrame(byte[] input, int inputOffset, int inputCount, byte[] output, int outputOffset)
{
int result = 0;
fixed (byte* inPtr = input)
fixed (byte* outPtr = output)
result = Decode(_ptr, inPtr + inputOffset, inputCount, outPtr + outputOffset, (output.Length - outputOffset) / SampleSize / MaxChannels, 0);
if (result < 0)
throw new Exception($"Opus Error: {(OpusError)result}");
return result;
}
protected override void Dispose(bool disposing)
{
if (_ptr != IntPtr.Zero)
{
DestroyDecoder(_ptr);
_ptr = IntPtr.Zero;
}
}
}
}

View File

@@ -0,0 +1,75 @@
using System;
using System.Runtime.InteropServices;
namespace Discord.Audio
{
internal unsafe class OpusEncoder : OpusConverter
{
[DllImport("opus", EntryPoint = "opus_encoder_create", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr CreateEncoder(int Fs, int channels, int application, out OpusError error);
[DllImport("opus", EntryPoint = "opus_encoder_destroy", CallingConvention = CallingConvention.Cdecl)]
private static extern void DestroyEncoder(IntPtr encoder);
[DllImport("opus", EntryPoint = "opus_encode", CallingConvention = CallingConvention.Cdecl)]
private static extern int Encode(IntPtr st, byte* pcm, int frame_size, byte* data, int max_data_bytes);
[DllImport("opus", EntryPoint = "opus_encoder_ctl", CallingConvention = CallingConvention.Cdecl)]
private static extern int EncoderCtl(IntPtr st, OpusCtl request, int value);
/// <summary> Gets the coding mode of the encoder. </summary>
public OpusApplication Application { get; }
public OpusEncoder(int samplingRate, int channels, OpusApplication application = OpusApplication.MusicOrMixed)
: base(samplingRate, channels)
{
Application = application;
OpusError error;
_ptr = CreateEncoder(samplingRate, channels, (int)application, out error);
if (error != OpusError.OK)
throw new Exception($"Opus Error: {error}");
}
/// <summary> Produces Opus encoded audio from PCM samples. </summary>
/// <param name="input">PCM samples to encode.</param>
/// <param name="output">Buffer to store the encoded frame.</param>
/// <returns>Length of the frame contained in outputBuffer.</returns>
public unsafe int EncodeFrame(byte[] input, int inputOffset, int inputCount, byte[] output, int outputOffset)
{
int result = 0;
fixed (byte* inPtr = input)
fixed (byte* outPtr = output)
result = Encode(_ptr, inPtr + inputOffset, inputCount / SampleSize, outPtr + outputOffset, output.Length - outputOffset);
if (result < 0)
throw new Exception($"Opus Error: {(OpusError)result}");
return result;
}
/// <summary> Gets or sets whether Forward Error Correction is enabled. </summary>
public void SetForwardErrorCorrection(bool value)
{
var result = EncoderCtl(_ptr, OpusCtl.SetInbandFECRequest, value ? 1 : 0);
if (result < 0)
throw new Exception($"Opus Error: {(OpusError)result}");
}
/// <summary> Gets or sets whether Forward Error Correction is enabled. </summary>
public void SetBitrate(int value)
{
if (value < 1 || value > DiscordVoiceAPIClient.MaxBitrate)
throw new ArgumentOutOfRangeException(nameof(value));
var result = EncoderCtl(_ptr, OpusCtl.SetBitrateRequest, value * 1000);
if (result < 0)
throw new Exception($"Opus Error: {(OpusError)result}");
}
protected override void Dispose(bool disposing)
{
if (_ptr != IntPtr.Zero)
{
DestroyEncoder(_ptr);
_ptr = IntPtr.Zero;
}
}
}
}

View File

@@ -0,0 +1,14 @@
namespace Discord.Audio
{
internal enum OpusError : int
{
OK = 0,
BadArg = -1,
BufferToSmall = -2,
InternalError = -3,
InvalidPacket = -4,
Unimplemented = -5,
InvalidState = -6,
AllocFail = -7
}
}

View File

@@ -0,0 +1,36 @@
using System;
using System.Runtime.InteropServices;
namespace Discord.Audio
{
public unsafe static class SecretBox
{
[DllImport("libsodium", EntryPoint = "crypto_secretbox_easy", CallingConvention = CallingConvention.Cdecl)]
private static extern int SecretBoxEasy(byte* output, byte* input, long inputLength, byte[] nonce, byte[] secret);
[DllImport("libsodium", EntryPoint = "crypto_secretbox_open_easy", CallingConvention = CallingConvention.Cdecl)]
private static extern int SecretBoxOpenEasy(byte* output, byte* input, long inputLength, byte[] nonce, byte[] secret);
public static int Encrypt(byte[] input, int inputOffset, int inputLength, byte[] output, int outputOffset, byte[] nonce, byte[] secret)
{
fixed (byte* inPtr = input)
fixed (byte* outPtr = output)
{
int error = SecretBoxEasy(outPtr + outputOffset, inPtr + inputOffset, inputLength, nonce, secret);
if (error != 0)
throw new Exception($"Sodium Error: {error}");
return inputLength + 16;
}
}
public static int Decrypt(byte[] input, int inputOffset, int inputLength, byte[] output, int outputOffset, byte[] nonce, byte[] secret)
{
fixed (byte* inPtr = input)
fixed (byte* outPtr = output)
{
int error = SecretBoxOpenEasy(outPtr + outputOffset, inPtr + inputOffset, inputLength, nonce, secret);
if (error != 0)
throw new Exception($"Sodium Error: {error}");
return inputLength - 16;
}
}
}
}

View File

@@ -0,0 +1,30 @@
namespace Discord.Audio
{
public class OpusDecodeStream : RTPReadStream
{
private readonly byte[] _buffer;
private readonly OpusDecoder _decoder;
internal OpusDecodeStream(AudioClient audioClient, byte[] secretKey, int samplingRate,
int channels = OpusConverter.MaxChannels, int bufferSize = 4000)
: base(audioClient, secretKey)
{
_buffer = new byte[bufferSize];
_decoder = new OpusDecoder(samplingRate, channels);
}
public override int Read(byte[] buffer, int offset, int count)
{
count = _decoder.DecodeFrame(buffer, offset, count, _buffer, 0);
return base.Read(_buffer, 0, count);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
_decoder.Dispose();
}
}
}

View File

@@ -0,0 +1,35 @@
namespace Discord.Audio
{
public class OpusEncodeStream : RTPWriteStream
{
public int SampleRate = 48000;
public int Channels = 2;
private readonly OpusEncoder _encoder;
internal OpusEncodeStream(AudioClient audioClient, byte[] secretKey, int samplesPerFrame, uint ssrc, int? bitrate = null,
OpusApplication application = OpusApplication.MusicOrMixed, int bufferSize = 4000)
: base(audioClient, secretKey, samplesPerFrame, ssrc, bufferSize)
{
_encoder = new OpusEncoder(SampleRate, Channels);
_encoder.SetForwardErrorCorrection(true);
if (bitrate != null)
_encoder.SetBitrate(bitrate.Value);
}
public override void Write(byte[] buffer, int offset, int count)
{
count = _encoder.EncodeFrame(buffer, offset, count, _buffer, 0);
base.Write(_buffer, 0, count);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
_encoder.Dispose();
}
}
}

View File

@@ -0,0 +1,53 @@
using System;
using System.Collections.Concurrent;
using System.IO;
namespace Discord.Audio
{
public class RTPReadStream : Stream
{
private readonly BlockingCollection<byte[]> _queuedData; //TODO: Replace with max-length ring buffer
private readonly AudioClient _audioClient;
private readonly byte[] _buffer, _nonce, _secretKey;
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => true;
internal RTPReadStream(AudioClient audioClient, byte[] secretKey, int bufferSize = 4000)
{
_audioClient = audioClient;
_secretKey = secretKey;
_buffer = new byte[bufferSize];
_queuedData = new BlockingCollection<byte[]>(100);
_nonce = new byte[24];
}
public override int Read(byte[] buffer, int offset, int count)
{
var queuedData = _queuedData.Take();
Buffer.BlockCopy(queuedData, 0, buffer, offset, Math.Min(queuedData.Length, count));
return queuedData.Length;
}
public override void Write(byte[] buffer, int offset, int count)
{
Buffer.BlockCopy(buffer, 0, _nonce, 0, 12);
count = SecretBox.Decrypt(buffer, offset, count, _buffer, 0, _nonce, _secretKey);
var newBuffer = new byte[count];
Buffer.BlockCopy(_buffer, 0, newBuffer, 0, count);
_queuedData.Add(newBuffer);
}
public override void Flush() { throw new NotSupportedException(); }
public override long Length { get { throw new NotSupportedException(); } }
public override long Position
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
}
public override void SetLength(long value) { throw new NotSupportedException(); }
public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); }
}
}

View File

@@ -0,0 +1,67 @@
using System;
using System.IO;
namespace Discord.Audio
{
public class RTPWriteStream : Stream
{
private readonly AudioClient _audioClient;
private readonly byte[] _nonce, _secretKey;
private int _samplesPerFrame;
private uint _ssrc, _timestamp = 0;
protected readonly byte[] _buffer;
public override bool CanRead => false;
public override bool CanSeek => false;
public override bool CanWrite => true;
internal RTPWriteStream(AudioClient audioClient, byte[] secretKey, int samplesPerFrame, uint ssrc, int bufferSize = 4000)
{
_audioClient = audioClient;
_secretKey = secretKey;
_samplesPerFrame = samplesPerFrame;
_ssrc = ssrc;
_buffer = new byte[bufferSize];
_nonce = new byte[24];
_nonce[0] = 0x80;
_nonce[1] = 0x78;
_nonce[8] = (byte)(_ssrc >> 24);
_nonce[9] = (byte)(_ssrc >> 16);
_nonce[10] = (byte)(_ssrc >> 8);
_nonce[11] = (byte)(_ssrc >> 0);
}
public override void Write(byte[] buffer, int offset, int count)
{
unchecked
{
if (_nonce[3]++ == byte.MaxValue)
_nonce[2]++;
_timestamp += (uint)_samplesPerFrame;
_nonce[4] = (byte)(_timestamp >> 24);
_nonce[5] = (byte)(_timestamp >> 16);
_nonce[6] = (byte)(_timestamp >> 8);
_nonce[7] = (byte)(_timestamp >> 0);
}
count = SecretBox.Encrypt(buffer, offset, count, _buffer, 12, _nonce, _secretKey);
Buffer.BlockCopy(_nonce, 0, _buffer, 0, 12); //Copy the RTP header from nonce to buffer
_audioClient.Send(_buffer, count + 12);
}
public override void Flush() { }
public override long Length { get { throw new NotSupportedException(); } }
public override long Position
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
}
public override int Read(byte[] buffer, int offset, int count) { throw new NotSupportedException(); }
public override void SetLength(long value) { throw new NotSupportedException(); }
public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); }
}
}

View File

@@ -0,0 +1,131 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
namespace Discord.WebSocket
{
internal class DataStore
{
private const int CollectionConcurrencyLevel = 1; //WebSocket updater/event handler. //TODO: Needs profiling, increase to 2?
private const double AverageChannelsPerGuild = 10.22; //Source: Googie2149
private const double AverageUsersPerGuild = 47.78; //Source: Googie2149
private const double CollectionMultiplier = 1.05; //Add 5% buffer to handle growth
private readonly ConcurrentDictionary<ulong, ISocketChannel> _channels;
private readonly ConcurrentDictionary<ulong, SocketDMChannel> _dmChannels;
private readonly ConcurrentDictionary<ulong, SocketGuild> _guilds;
private readonly ConcurrentDictionary<ulong, SocketGlobalUser> _users;
private readonly ConcurrentHashSet<ulong> _groupChannels;
internal IReadOnlyCollection<ISocketChannel> Channels => _channels.ToReadOnlyCollection();
internal IReadOnlyCollection<SocketDMChannel> DMChannels => _dmChannels.ToReadOnlyCollection();
internal IReadOnlyCollection<SocketGroupChannel> GroupChannels => _groupChannels.Select(x => GetChannel(x) as SocketGroupChannel).ToReadOnlyCollection(_groupChannels);
internal IReadOnlyCollection<SocketGuild> Guilds => _guilds.ToReadOnlyCollection();
internal IReadOnlyCollection<SocketGlobalUser> Users => _users.ToReadOnlyCollection();
internal IReadOnlyCollection<ISocketPrivateChannel> PrivateChannels =>
_dmChannels.Select(x => x.Value as ISocketPrivateChannel).Concat(
_groupChannels.Select(x => GetChannel(x) as ISocketPrivateChannel))
.ToReadOnlyCollection(() => _dmChannels.Count + _groupChannels.Count);
public DataStore(int guildCount, int dmChannelCount)
{
double estimatedChannelCount = guildCount * AverageChannelsPerGuild + dmChannelCount;
double estimatedUsersCount = guildCount * AverageUsersPerGuild;
_channels = new ConcurrentDictionary<ulong, ISocketChannel>(CollectionConcurrencyLevel, (int)(estimatedChannelCount * CollectionMultiplier));
_dmChannels = new ConcurrentDictionary<ulong, SocketDMChannel>(CollectionConcurrencyLevel, (int)(dmChannelCount * CollectionMultiplier));
_guilds = new ConcurrentDictionary<ulong, SocketGuild>(CollectionConcurrencyLevel, (int)(guildCount * CollectionMultiplier));
_users = new ConcurrentDictionary<ulong, SocketGlobalUser>(CollectionConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier));
_groupChannels = new ConcurrentHashSet<ulong>(CollectionConcurrencyLevel, (int)(10 * CollectionMultiplier));
}
internal ISocketChannel GetChannel(ulong id)
{
ISocketChannel channel;
if (_channels.TryGetValue(id, out channel))
return channel;
return null;
}
internal SocketDMChannel GetDMChannel(ulong userId)
{
SocketDMChannel channel;
if (_dmChannels.TryGetValue(userId, out channel))
return channel;
return null;
}
internal void AddChannel(ISocketChannel channel)
{
_channels[channel.Id] = channel;
var dmChannel = channel as SocketDMChannel;
if (dmChannel != null)
_dmChannels[dmChannel.Recipient.Id] = dmChannel;
else
{
var groupChannel = channel as SocketGroupChannel;
if (groupChannel != null)
_groupChannels.TryAdd(groupChannel.Id);
}
}
internal ISocketChannel RemoveChannel(ulong id)
{
ISocketChannel channel;
if (_channels.TryRemove(id, out channel))
{
var dmChannel = channel as SocketDMChannel;
if (dmChannel != null)
{
SocketDMChannel ignored;
_dmChannels.TryRemove(dmChannel.Recipient.Id, out ignored);
}
else
{
var groupChannel = channel as SocketGroupChannel;
if (groupChannel != null)
_groupChannels.TryRemove(id);
}
return channel;
}
return null;
}
internal SocketGuild GetGuild(ulong id)
{
SocketGuild guild;
if (_guilds.TryGetValue(id, out guild))
return guild;
return null;
}
internal void AddGuild(SocketGuild guild)
{
_guilds[guild.Id] = guild;
}
internal SocketGuild RemoveGuild(ulong id)
{
SocketGuild guild;
if (_guilds.TryRemove(id, out guild))
return guild;
return null;
}
internal SocketGlobalUser GetUser(ulong id)
{
SocketGlobalUser user;
if (_users.TryGetValue(id, out user))
return user;
return null;
}
internal SocketGlobalUser GetOrAddUser(ulong id, Func<ulong, SocketGlobalUser> userFactory)
{
return _users.GetOrAdd(id, userFactory);
}
internal SocketGlobalUser RemoveUser(ulong id)
{
SocketGlobalUser user;
if (_users.TryRemove(id, out user))
return user;
return null;
}
}
}

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>22ab6c66-536c-4ac2-bbdb-a8bc4eb6b14d</ProjectGuid>
<RootNamespace>Discord.Net.WebSocket</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@@ -0,0 +1,203 @@
using System;
using System.Threading.Tasks;
namespace Discord.WebSocket
{
//TODO: Add event docstrings
public partial class DiscordSocketClient
{
//General
public event Func<Task> Connected
{
add { _connectedEvent.Add(value); }
remove { _connectedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<Task>> _connectedEvent = new AsyncEvent<Func<Task>>();
public event Func<Exception, Task> Disconnected
{
add { _disconnectedEvent.Add(value); }
remove { _disconnectedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<Exception, Task>> _disconnectedEvent = new AsyncEvent<Func<Exception, Task>>();
public event Func<Task> Ready
{
add { _readyEvent.Add(value); }
remove { _readyEvent.Remove(value); }
}
private readonly AsyncEvent<Func<Task>> _readyEvent = new AsyncEvent<Func<Task>>();
public event Func<int, int, Task> LatencyUpdated
{
add { _latencyUpdatedEvent.Add(value); }
remove { _latencyUpdatedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<int, int, Task>> _latencyUpdatedEvent = new AsyncEvent<Func<int, int, Task>>();
//Channels
public event Func<IChannel, Task> ChannelCreated
{
add { _channelCreatedEvent.Add(value); }
remove { _channelCreatedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<IChannel, Task>> _channelCreatedEvent = new AsyncEvent<Func<IChannel, Task>>();
public event Func<IChannel, Task> ChannelDestroyed
{
add { _channelDestroyedEvent.Add(value); }
remove { _channelDestroyedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<IChannel, Task>> _channelDestroyedEvent = new AsyncEvent<Func<IChannel, Task>>();
public event Func<IChannel, IChannel, Task> ChannelUpdated
{
add { _channelUpdatedEvent.Add(value); }
remove { _channelUpdatedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<IChannel, IChannel, Task>> _channelUpdatedEvent = new AsyncEvent<Func<IChannel, IChannel, Task>>();
//Messages
public event Func<IMessage, Task> MessageReceived
{
add { _messageReceivedEvent.Add(value); }
remove { _messageReceivedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<IMessage, Task>> _messageReceivedEvent = new AsyncEvent<Func<IMessage, Task>>();
public event Func<ulong, Optional<IMessage>, Task> MessageDeleted
{
add { _messageDeletedEvent.Add(value); }
remove { _messageDeletedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<ulong, Optional<IMessage>, Task>> _messageDeletedEvent = new AsyncEvent<Func<ulong, Optional<IMessage>, Task>>();
public event Func<Optional<IMessage>, IMessage, Task> MessageUpdated
{
add { _messageUpdatedEvent.Add(value); }
remove { _messageUpdatedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<Optional<IMessage>, IMessage, Task>> _messageUpdatedEvent = new AsyncEvent<Func<Optional<IMessage>, IMessage, Task>>();
//Roles
public event Func<IRole, Task> RoleCreated
{
add { _roleCreatedEvent.Add(value); }
remove { _roleCreatedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<IRole, Task>> _roleCreatedEvent = new AsyncEvent<Func<IRole, Task>>();
public event Func<IRole, Task> RoleDeleted
{
add { _roleDeletedEvent.Add(value); }
remove { _roleDeletedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<IRole, Task>> _roleDeletedEvent = new AsyncEvent<Func<IRole, Task>>();
public event Func<IRole, IRole, Task> RoleUpdated
{
add { _roleUpdatedEvent.Add(value); }
remove { _roleUpdatedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<IRole, IRole, Task>> _roleUpdatedEvent = new AsyncEvent<Func<IRole, IRole, Task>>();
//Guilds
public event Func<IGuild, Task> JoinedGuild
{
add { _joinedGuildEvent.Add(value); }
remove { _joinedGuildEvent.Remove(value); }
}
private AsyncEvent<Func<IGuild, Task>> _joinedGuildEvent = new AsyncEvent<Func<IGuild, Task>>();
public event Func<IGuild, Task> LeftGuild
{
add { _leftGuildEvent.Add(value); }
remove { _leftGuildEvent.Remove(value); }
}
private AsyncEvent<Func<IGuild, Task>> _leftGuildEvent = new AsyncEvent<Func<IGuild, Task>>();
public event Func<IGuild, Task> GuildAvailable
{
add { _guildAvailableEvent.Add(value); }
remove { _guildAvailableEvent.Remove(value); }
}
private AsyncEvent<Func<IGuild, Task>> _guildAvailableEvent = new AsyncEvent<Func<IGuild, Task>>();
public event Func<IGuild, Task> GuildUnavailable
{
add { _guildUnavailableEvent.Add(value); }
remove { _guildUnavailableEvent.Remove(value); }
}
private AsyncEvent<Func<IGuild, Task>> _guildUnavailableEvent = new AsyncEvent<Func<IGuild, Task>>();
public event Func<IGuild, Task> GuildMembersDownloaded
{
add { _guildMembersDownloadedEvent.Add(value); }
remove { _guildMembersDownloadedEvent.Remove(value); }
}
private AsyncEvent<Func<IGuild, Task>> _guildMembersDownloadedEvent = new AsyncEvent<Func<IGuild, Task>>();
public event Func<IGuild, IGuild, Task> GuildUpdated
{
add { _guildUpdatedEvent.Add(value); }
remove { _guildUpdatedEvent.Remove(value); }
}
private AsyncEvent<Func<IGuild, IGuild, Task>> _guildUpdatedEvent = new AsyncEvent<Func<IGuild, IGuild, Task>>();
//Users
public event Func<IGuildUser, Task> UserJoined
{
add { _userJoinedEvent.Add(value); }
remove { _userJoinedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<IGuildUser, Task>> _userJoinedEvent = new AsyncEvent<Func<IGuildUser, Task>>();
public event Func<IGuildUser, Task> UserLeft
{
add { _userLeftEvent.Add(value); }
remove { _userLeftEvent.Remove(value); }
}
private readonly AsyncEvent<Func<IGuildUser, Task>> _userLeftEvent = new AsyncEvent<Func<IGuildUser, Task>>();
public event Func<IUser, IGuild, Task> UserBanned
{
add { _userBannedEvent.Add(value); }
remove { _userBannedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<IUser, IGuild, Task>> _userBannedEvent = new AsyncEvent<Func<IUser, IGuild, Task>>();
public event Func<IUser, IGuild, Task> UserUnbanned
{
add { _userUnbannedEvent.Add(value); }
remove { _userUnbannedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<IUser, IGuild, Task>> _userUnbannedEvent = new AsyncEvent<Func<IUser, IGuild, Task>>();
public event Func<IGuildUser, IGuildUser, Task> UserUpdated
{
add { _userUpdatedEvent.Add(value); }
remove { _userUpdatedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<IGuildUser, IGuildUser, Task>> _userUpdatedEvent = new AsyncEvent<Func<IGuildUser, IGuildUser, Task>>();
public event Func<IGuildUser, IPresence, IPresence, Task> UserPresenceUpdated
{
add { _userPresenceUpdatedEvent.Add(value); }
remove { _userPresenceUpdatedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<IGuildUser, IPresence, IPresence, Task>> _userPresenceUpdatedEvent = new AsyncEvent<Func<IGuildUser, IPresence, IPresence, Task>>();
public event Func<IUser, IVoiceState, IVoiceState, Task> UserVoiceStateUpdated
{
add { _userVoiceStateUpdatedEvent.Add(value); }
remove { _userVoiceStateUpdatedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<IUser, IVoiceState, IVoiceState, Task>> _userVoiceStateUpdatedEvent = new AsyncEvent<Func<IUser, IVoiceState, IVoiceState, Task>>();
public event Func<ISelfUser, ISelfUser, Task> CurrentUserUpdated
{
add { _selfUpdatedEvent.Add(value); }
remove { _selfUpdatedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<ISelfUser, ISelfUser, Task>> _selfUpdatedEvent = new AsyncEvent<Func<ISelfUser, ISelfUser, Task>>();
public event Func<IUser, IChannel, Task> UserIsTyping
{
add { _userIsTypingEvent.Add(value); }
remove { _userIsTypingEvent.Remove(value); }
}
private readonly AsyncEvent<Func<IUser, IChannel, Task>> _userIsTypingEvent = new AsyncEvent<Func<IUser, IChannel, Task>>();
public event Func<IGroupUser, Task> RecipientAdded
{
add { _recipientAddedEvent.Add(value); }
remove { _recipientAddedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<IGroupUser, Task>> _recipientAddedEvent = new AsyncEvent<Func<IGroupUser, Task>>();
public event Func<IGroupUser, Task> RecipientRemoved
{
add { _recipientRemovedEvent.Add(value); }
remove { _recipientRemovedEvent.Remove(value); }
}
private readonly AsyncEvent<Func<IGroupUser, Task>> _recipientRemovedEvent = new AsyncEvent<Func<IGroupUser, Task>>();
//TODO: Add PresenceUpdated? VoiceStateUpdated?, VoiceConnected, VoiceDisconnected;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,32 @@
using Discord.Audio;
using Discord.Net.WebSockets;
using Discord.Rest;
namespace Discord.WebSocket
{
public class DiscordSocketConfig : DiscordRestConfig
{
public const string GatewayEncoding = "json";
/// <summary> Gets or sets the time, in milliseconds, to wait for a connection to complete before aborting. </summary>
public int ConnectionTimeout { get; set; } = 30000;
/// <summary> Gets or sets the id for this shard. Must be less than TotalShards. </summary>
public int ShardId { get; set; } = 0;
/// <summary> Gets or sets the total number of shards for this application. </summary>
public int TotalShards { get; set; } = 1;
/// <summary> Gets or sets the number of messages per channel that should be kept in cache. Setting this to zero disables the message cache entirely. </summary>
public int MessageCacheSize { get; set; } = 0;
/// <summary>
/// Gets or sets the max number of users a guild may have for offline users to be included in the READY packet. Max is 250.
/// </summary>
public int LargeThreshold { get; set; } = 250;
/// <summary> Gets or sets the type of audio this DiscordClient supports. </summary>
public AudioMode AudioMode { get; set; } = AudioMode.Disabled;
/// <summary> Gets or sets the provider used to generate new websocket connections. </summary>
public WebSocketProvider WebSocketProvider { get; set; } = () => new DefaultWebSocketClient();
}
}

View File

@@ -0,0 +1,77 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading.Tasks;
using MessageModel = Discord.API.Message;
using Model = Discord.API.Channel;
namespace Discord.WebSocket
{
internal class DMChannel : IDMChannel, ISocketChannel, ISocketMessageChannel, ISocketPrivateChannel
{
private readonly MessageManager _messages;
public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient;
public new SocketDMUser Recipient => base.Recipient as SocketDMUser;
public IReadOnlyCollection<ISocketUser> Users => ImmutableArray.Create<ISocketUser>(Discord.CurrentUser, Recipient);
IReadOnlyCollection<ISocketUser> ISocketPrivateChannel.Recipients => ImmutableArray.Create(Recipient);
public SocketDMChannel(DiscordSocketClient discord, SocketDMUser recipient, Model model)
: base(discord, recipient, model)
{
if (Discord.MessageCacheSize > 0)
_messages = new MessageCache(Discord, this);
else
_messages = new MessageManager(Discord, this);
}
public override Task<IUser> GetUserAsync(ulong id) => Task.FromResult<IUser>(GetUser(id));
public override Task<IReadOnlyCollection<IUser>> GetUsersAsync() => Task.FromResult<IReadOnlyCollection<IUser>>(Users);
public ISocketUser GetUser(ulong id)
{
var currentUser = Discord.CurrentUser;
if (id == Recipient.Id)
return Recipient;
else if (id == currentUser.Id)
return currentUser;
else
return null;
}
public override async Task<IMessage> GetMessageAsync(ulong id)
{
return await _messages.DownloadAsync(id).ConfigureAwait(false);
}
public override async Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(int limit)
{
return await _messages.DownloadAsync(null, Direction.Before, limit).ConfigureAwait(false);
}
public override async Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit)
{
return await _messages.DownloadAsync(fromMessageId, dir, limit).ConfigureAwait(false);
}
public ISocketMessage CreateMessage(ISocketUser author, MessageModel model)
{
return _messages.Create(author, model);
}
public ISocketMessage AddMessage(ISocketUser author, MessageModel model)
{
var msg = _messages.Create(author, model);
_messages.Add(msg);
return msg;
}
public ISocketMessage GetMessage(ulong id)
{
return _messages.Get(id);
}
public ISocketMessage RemoveMessage(ulong id)
{
return _messages.Remove(id);
}
public SocketDMChannel Clone() => MemberwiseClone() as SocketDMChannel;
IMessage IMessageChannel.GetCachedMessage(ulong id) => GetMessage(id);
ISocketUser ISocketMessageChannel.GetUser(ulong id, bool skipCheck) => GetUser(id);
ISocketChannel ISocketChannel.Clone() => Clone();
}
}

View File

@@ -0,0 +1,144 @@
using Discord.Rest;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using MessageModel = Discord.API.Message;
using Model = Discord.API.Channel;
using UserModel = Discord.API.User;
using VoiceStateModel = Discord.API.VoiceState;
namespace Discord.WebSocket
{
internal class SocketGroupChannel : IGroupChannel, ISocketChannel, ISocketMessageChannel, ISocketPrivateChannel
{
internal override bool IsAttached => true;
private readonly MessageManager _messages;
private ConcurrentDictionary<ulong, VoiceState> _voiceStates;
public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient;
public IReadOnlyCollection<ISocketUser> Users
=> _users.Select(x => x.Value as ISocketUser).Concat(ImmutableArray.Create(Discord.CurrentUser)).ToReadOnlyCollection(() => _users.Count + 1);
public new IReadOnlyCollection<ISocketUser> Recipients => _users.Select(x => x.Value as ISocketUser).ToReadOnlyCollection(_users);
public SocketGroupChannel(DiscordSocketClient discord, Model model)
: base(discord, model)
{
if (Discord.MessageCacheSize > 0)
_messages = new MessageCache(Discord, this);
else
_messages = new MessageManager(Discord, this);
_voiceStates = new ConcurrentDictionary<ulong, VoiceState>(1, 5);
}
public override void Update(Model model)
{
if (source == UpdateSource.Rest && IsAttached) return;
base.Update(model, source);
}
internal void UpdateUsers(UserModel[] models, DataStore dataStore)
{
var users = new ConcurrentDictionary<ulong, GroupUser>(1, models.Length);
for (int i = 0; i < models.Length; i++)
{
var globalUser = Discord.GetOrAddUser(models[i], dataStore);
users[models[i].Id] = new SocketGroupUser(this, globalUser);
}
_users = users;
}
internal override void UpdateUsers(UserModel[] models)
=> UpdateUsers(models, source, Discord.DataStore);
public SocketGroupUser AddUser(UserModel model, DataStore dataStore)
{
GroupUser user;
if (_users.TryGetValue(model.Id, out user))
return user as SocketGroupUser;
else
{
var globalUser = Discord.GetOrAddUser(model, dataStore);
var privateUser = new SocketGroupUser(this, globalUser);
_users[privateUser.Id] = privateUser;
return privateUser;
}
}
public ISocketUser GetUser(ulong id)
{
GroupUser user;
if (_users.TryGetValue(id, out user))
return user as SocketGroupUser;
if (id == Discord.CurrentUser.Id)
return Discord.CurrentUser;
return null;
}
public SocketGroupUser RemoveUser(ulong id)
{
GroupUser user;
if (_users.TryRemove(id, out user))
return user as SocketGroupUser;
return null;
}
public VoiceState AddOrUpdateVoiceState(VoiceStateModel model, DataStore dataStore, ConcurrentDictionary<ulong, VoiceState> voiceStates = null)
{
var voiceChannel = dataStore.GetChannel(model.ChannelId.Value) as SocketVoiceChannel;
var voiceState = new VoiceState(voiceChannel, model);
(voiceStates ?? _voiceStates)[model.UserId] = voiceState;
return voiceState;
}
public VoiceState? GetVoiceState(ulong id)
{
VoiceState voiceState;
if (_voiceStates.TryGetValue(id, out voiceState))
return voiceState;
return null;
}
public VoiceState? RemoveVoiceState(ulong id)
{
VoiceState voiceState;
if (_voiceStates.TryRemove(id, out voiceState))
return voiceState;
return null;
}
public override async Task<IMessage> GetMessageAsync(ulong id)
{
return await _messages.DownloadAsync(id).ConfigureAwait(false);
}
public override async Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(int limit)
{
return await _messages.DownloadAsync(null, Direction.Before, limit).ConfigureAwait(false);
}
public override async Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit)
{
return await _messages.DownloadAsync(fromMessageId, dir, limit).ConfigureAwait(false);
}
public ISocketMessage CreateMessage(ISocketUser author, MessageModel model)
{
return _messages.Create(author, model);
}
public ISocketMessage AddMessage(ISocketUser author, MessageModel model)
{
var msg = _messages.Create(author, model);
_messages.Add(msg);
return msg;
}
public ISocketMessage GetMessage(ulong id)
{
return _messages.Get(id);
}
public ISocketMessage RemoveMessage(ulong id)
{
return _messages.Remove(id);
}
public SocketDMChannel Clone() => MemberwiseClone() as SocketDMChannel;
IMessage IMessageChannel.GetCachedMessage(ulong id) => GetMessage(id);
ISocketUser ISocketMessageChannel.GetUser(ulong id, bool skipCheck) => GetUser(id);
ISocketChannel ISocketChannel.Clone() => Clone();
}
}

View File

@@ -0,0 +1,157 @@
using Discord.API.Rest;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Model = Discord.API.Channel;
namespace Discord.Rest
{
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
internal abstract class SocketGuildChannel : ISnowflakeEntity, IGuildChannel
{
private List<Overwrite> _overwrites; //TODO: Is maintaining a list here too expensive? Is this threadsafe?
public string Name { get; private set; }
public int Position { get; private set; }
public Guild Guild { get; private set; }
public override DiscordRestClient Discord => Guild.Discord;
public GuildChannel(Guild guild, Model model)
: base(model.Id)
{
Guild = guild;
Update(model);
}
public virtual void Update(Model model)
{
if (source == UpdateSource.Rest && IsAttached) return;
Name = model.Name.Value;
Position = model.Position.Value;
var overwrites = model.PermissionOverwrites.Value;
var newOverwrites = new List<Overwrite>(overwrites.Length);
for (int i = 0; i < overwrites.Length; i++)
newOverwrites.Add(new Overwrite(overwrites[i]));
_overwrites = newOverwrites;
}
public async Task UpdateAsync()
{
if (IsAttached) throw new NotSupportedException();
var model = await Discord.ApiClient.GetChannelAsync(Id).ConfigureAwait(false);
Update(model, UpdateSource.Rest);
}
public async Task ModifyAsync(Action<ModifyGuildChannelParams> func)
{
if (func == null) throw new NullReferenceException(nameof(func));
var args = new ModifyGuildChannelParams();
func(args);
if (!args._name.IsSpecified)
args._name = Name;
var model = await Discord.ApiClient.ModifyGuildChannelAsync(Id, args).ConfigureAwait(false);
Update(model, UpdateSource.Rest);
}
public async Task DeleteAsync()
{
await Discord.ApiClient.DeleteChannelAsync(Id).ConfigureAwait(false);
}
public abstract Task<IGuildUser> GetUserAsync(ulong id);
public abstract Task<IReadOnlyCollection<IGuildUser>> GetUsersAsync();
public async Task<IReadOnlyCollection<IInviteMetadata>> GetInvitesAsync()
{
var models = await Discord.ApiClient.GetChannelInvitesAsync(Id).ConfigureAwait(false);
return models.Select(x => new InviteMetadata(Discord, x)).ToImmutableArray();
}
public async Task<IInviteMetadata> CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary)
{
var args = new CreateChannelInviteParams
{
MaxAge = maxAge ?? 0,
MaxUses = maxUses ?? 0,
Temporary = isTemporary
};
var model = await Discord.ApiClient.CreateChannelInviteAsync(Id, args).ConfigureAwait(false);
return new InviteMetadata(Discord, model);
}
public OverwritePermissions? GetPermissionOverwrite(IUser user)
{
for (int i = 0; i < _overwrites.Count; i++)
{
if (_overwrites[i].TargetId == user.Id)
return _overwrites[i].Permissions;
}
return null;
}
public OverwritePermissions? GetPermissionOverwrite(IRole role)
{
for (int i = 0; i < _overwrites.Count; i++)
{
if (_overwrites[i].TargetId == role.Id)
return _overwrites[i].Permissions;
}
return null;
}
public async Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions perms)
{
var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue, Type = "member" };
await Discord.ApiClient.ModifyChannelPermissionsAsync(Id, user.Id, args).ConfigureAwait(false);
_overwrites.Add(new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = user.Id, TargetType = PermissionTarget.User }));
}
public async Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions perms)
{
var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue, Type = "role" };
await Discord.ApiClient.ModifyChannelPermissionsAsync(Id, role.Id, args).ConfigureAwait(false);
_overwrites.Add(new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = role.Id, TargetType = PermissionTarget.Role }));
}
public async Task RemovePermissionOverwriteAsync(IUser user)
{
await Discord.ApiClient.DeleteChannelPermissionAsync(Id, user.Id).ConfigureAwait(false);
for (int i = 0; i < _overwrites.Count; i++)
{
if (_overwrites[i].TargetId == user.Id)
{
_overwrites.RemoveAt(i);
return;
}
}
}
public async Task RemovePermissionOverwriteAsync(IRole role)
{
await Discord.ApiClient.DeleteChannelPermissionAsync(Id, role.Id).ConfigureAwait(false);
for (int i = 0; i < _overwrites.Count; i++)
{
if (_overwrites[i].TargetId == role.Id)
{
_overwrites.RemoveAt(i);
return;
}
}
}
public override string ToString() => Name;
private string DebuggerDisplay => $"{Name} ({Id})";
IGuild IGuildChannel.Guild => Guild;
IReadOnlyCollection<Overwrite> IGuildChannel.PermissionOverwrites => _overwrites.AsReadOnly();
async Task<IUser> IChannel.GetUserAsync(ulong id) => await GetUserAsync(id).ConfigureAwait(false);
async Task<IReadOnlyCollection<IUser>> IChannel.GetUsersAsync() => await GetUsersAsync().ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,88 @@
using Discord.Rest;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using MessageModel = Discord.API.Message;
using Model = Discord.API.Channel;
namespace Discord.WebSocket
{
internal class SocketTextChannel : TextChannel, ISocketGuildChannel, ISocketMessageChannel
{
internal override bool IsAttached => true;
private readonly MessageManager _messages;
public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient;
public new SocketGuild Guild => base.Guild as SocketGuild;
public IReadOnlyCollection<SocketGuildUser> Members
=> Guild.Members.Where(x => Permissions.GetValue(Permissions.ResolveChannel(x, this, x.GuildPermissions.RawValue), ChannelPermission.ReadMessages)).ToImmutableArray();
public SocketTextChannel(SocketGuild guild, Model model)
: base(guild, model)
{
if (Discord.MessageCacheSize > 0)
_messages = new MessageCache(Discord, this);
else
_messages = new MessageManager(Discord, this);
}
public override Task<IGuildUser> GetUserAsync(ulong id) => Task.FromResult<IGuildUser>(GetUser(id));
public override Task<IReadOnlyCollection<IGuildUser>> GetUsersAsync() => Task.FromResult<IReadOnlyCollection<IGuildUser>>(Members);
public SocketGuildUser GetUser(ulong id, bool skipCheck = false)
{
var user = Guild.GetUser(id);
if (skipCheck) return user;
if (user != null)
{
ulong perms = Permissions.ResolveChannel(user, this, user.GuildPermissions.RawValue);
if (Permissions.GetValue(perms, ChannelPermission.ReadMessages))
return user;
}
return null;
}
public override async Task<IMessage> GetMessageAsync(ulong id)
{
return await _messages.DownloadAsync(id).ConfigureAwait(false);
}
public override async Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch)
{
return await _messages.DownloadAsync(null, Direction.Before, limit).ConfigureAwait(false);
}
public override async Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch)
{
return await _messages.DownloadAsync(fromMessageId, dir, limit).ConfigureAwait(false);
}
public ISocketMessage CreateMessage(ISocketUser author, MessageModel model)
{
return _messages.Create(author, model);
}
public ISocketMessage AddMessage(ISocketUser author, MessageModel model)
{
var msg = _messages.Create(author, model);
_messages.Add(msg);
return msg;
}
public ISocketMessage GetMessage(ulong id)
{
return _messages.Get(id);
}
public ISocketMessage RemoveMessage(ulong id)
{
return _messages.Remove(id);
}
public SocketTextChannel Clone() => MemberwiseClone() as SocketTextChannel;
IReadOnlyCollection<ISocketUser> ISocketMessageChannel.Users => Members;
IMessage IMessageChannel.GetCachedMessage(ulong id) => GetMessage(id);
ISocketUser ISocketMessageChannel.GetUser(ulong id, bool skipCheck) => GetUser(id, skipCheck);
ISocketChannel ISocketChannel.Clone() => Clone();
}
}

View File

@@ -0,0 +1,54 @@
using Discord.Audio;
using Discord.Rest;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Model = Discord.API.Channel;
namespace Discord.WebSocket
{
internal class SocketVoiceChannel : VoiceChannel, ISocketGuildChannel
{
internal override bool IsAttached => true;
public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient;
public new SocketGuild Guild => base.Guild as SocketGuild;
public IReadOnlyCollection<IGuildUser> Members
=> Guild.VoiceStates.Where(x => x.Value.VoiceChannel.Id == Id).Select(x => Guild.GetUser(x.Key)).ToImmutableArray();
public SocketVoiceChannel(SocketGuild guild, Model model)
: base(guild, model)
{
}
public override Task<IGuildUser> GetUserAsync(ulong id)
=> Task.FromResult(GetUser(id));
public override Task<IReadOnlyCollection<IGuildUser>> GetUsersAsync()
=> Task.FromResult(Members);
public IGuildUser GetUser(ulong id)
{
var user = Guild.GetUser(id);
if (user != null && user.VoiceChannel.Id == Id)
return user;
return null;
}
public override async Task<IAudioClient> ConnectAsync()
{
var audioMode = Discord.AudioMode;
if (audioMode == AudioMode.Disabled)
throw new InvalidOperationException($"Audio is not enabled on this client, {nameof(DiscordSocketConfig.AudioMode)} in {nameof(DiscordSocketConfig)} must be set.");
return await Guild.ConnectAudioAsync(Id,
(audioMode & AudioMode.Incoming) == 0,
(audioMode & AudioMode.Outgoing) == 0).ConfigureAwait(false);
}
public SocketVoiceChannel Clone() => MemberwiseClone() as SocketVoiceChannel;
ISocketChannel ISocketChannel.Clone() => Clone();
}
}

View File

@@ -0,0 +1,419 @@
using Discord.Audio;
using Discord.Rest;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ChannelModel = Discord.API.Channel;
using EmojiUpdateModel = Discord.API.Gateway.GuildEmojiUpdateEvent;
using ExtendedModel = Discord.API.Gateway.ExtendedGuild;
using GuildSyncModel = Discord.API.Gateway.GuildSyncEvent;
using MemberModel = Discord.API.GuildMember;
using Model = Discord.API.Guild;
using PresenceModel = Discord.API.Presence;
using RoleModel = Discord.API.Role;
using VoiceStateModel = Discord.API.VoiceState;
namespace Discord.WebSocket
{
internal class SocketGuild : Guild, IGuild, IUserGuild
{
internal override bool IsAttached => true;
private readonly SemaphoreSlim _audioLock;
private TaskCompletionSource<bool> _syncPromise, _downloaderPromise;
private TaskCompletionSource<AudioClient> _audioConnectPromise;
private ConcurrentHashSet<ulong> _channels;
private ConcurrentDictionary<ulong, SocketGuildUser> _members;
private ConcurrentDictionary<ulong, VoiceState> _voiceStates;
internal bool _available;
public bool Available => _available && Discord.ConnectionState == ConnectionState.Connected;
public int MemberCount { get; set; }
public int DownloadedMemberCount { get; private set; }
public AudioClient AudioClient { get; private set; }
public bool HasAllMembers => _downloaderPromise.Task.IsCompleted;
public bool IsSynced => _syncPromise.Task.IsCompleted;
public Task SyncPromise => _syncPromise.Task;
public Task DownloaderPromise => _downloaderPromise.Task;
public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient;
public SocketGuildUser CurrentUser => GetUser(Discord.CurrentUser.Id);
public IReadOnlyCollection<ISocketGuildChannel> Channels
{
get
{
var channels = _channels;
var store = Discord.DataStore;
return channels.Select(x => store.GetChannel(x) as ISocketGuildChannel).Where(x => x != null).ToReadOnlyCollection(channels);
}
}
public IReadOnlyCollection<SocketGuildUser> Members => _members.ToReadOnlyCollection();
public IEnumerable<KeyValuePair<ulong, VoiceState>> VoiceStates => _voiceStates;
public SocketGuild(DiscordSocketClient discord, ExtendedModel model, DataStore dataStore) : base(discord, model)
{
_audioLock = new SemaphoreSlim(1, 1);
_syncPromise = new TaskCompletionSource<bool>();
_downloaderPromise = new TaskCompletionSource<bool>();
Update(model, dataStore);
}
public void Update(ExtendedModel model, DataStore dataStore)
{
if (source == UpdateSource.Rest && IsAttached) return;
_available = !(model.Unavailable ?? false);
if (!_available)
{
if (_channels == null)
_channels = new ConcurrentHashSet<ulong>();
if (_members == null)
_members = new ConcurrentDictionary<ulong, SocketGuildUser>();
if (_roles == null)
_roles = new ConcurrentDictionary<ulong, Role>();
if (Emojis == null)
Emojis = ImmutableArray.Create<Emoji>();
if (Features == null)
Features = ImmutableArray.Create<string>();
return;
}
base.Update(model as Model, source);
var channels = new ConcurrentHashSet<ulong>(1, (int)(model.Channels.Length * 1.05));
{
for (int i = 0; i < model.Channels.Length; i++)
AddChannel(model.Channels[i], dataStore, channels);
}
_channels = channels;
var members = new ConcurrentDictionary<ulong, SocketGuildUser>(1, (int)(model.Presences.Length * 1.05));
{
DownloadedMemberCount = 0;
for (int i = 0; i < model.Members.Length; i++)
AddOrUpdateUser(model.Members[i], dataStore, members);
if (Discord.ApiClient.AuthTokenType != TokenType.User)
{
var _ = _syncPromise.TrySetResultAsync(true);
if (!model.Large)
_ = _downloaderPromise.TrySetResultAsync(true);
}
for (int i = 0; i < model.Presences.Length; i++)
AddOrUpdateUser(model.Presences[i], dataStore, members);
}
_members = members;
MemberCount = model.MemberCount;
var voiceStates = new ConcurrentDictionary<ulong, VoiceState>(1, (int)(model.VoiceStates.Length * 1.05));
{
for (int i = 0; i < model.VoiceStates.Length; i++)
AddOrUpdateVoiceState(model.VoiceStates[i], dataStore, voiceStates);
}
_voiceStates = voiceStates;
}
public void Update(GuildSyncModel model, DataStore dataStore)
{
if (source == UpdateSource.Rest && IsAttached) return;
var members = new ConcurrentDictionary<ulong, SocketGuildUser>(1, (int)(model.Presences.Length * 1.05));
{
DownloadedMemberCount = 0;
for (int i = 0; i < model.Members.Length; i++)
AddOrUpdateUser(model.Members[i], dataStore, members);
var _ = _syncPromise.TrySetResultAsync(true);
if (!model.Large)
_ = _downloaderPromise.TrySetResultAsync(true);
for (int i = 0; i < model.Presences.Length; i++)
AddOrUpdateUser(model.Presences[i], dataStore, members);
}
_members = members;
}
public void Update(EmojiUpdateModel model)
{
if (source == UpdateSource.Rest && IsAttached) return;
var emojis = ImmutableArray.CreateBuilder<Emoji>(model.Emojis.Length);
for (int i = 0; i < model.Emojis.Length; i++)
emojis.Add(new Emoji(model.Emojis[i]));
Emojis = emojis.ToImmutableArray();
}
public override Task<IGuildChannel> GetChannelAsync(ulong id) => Task.FromResult<IGuildChannel>(GetChannel(id));
public override Task<IReadOnlyCollection<IGuildChannel>> GetChannelsAsync() => Task.FromResult<IReadOnlyCollection<IGuildChannel>>(Channels);
public void AddChannel(ChannelModel model, DataStore dataStore, ConcurrentHashSet<ulong> channels = null)
{
var channel = ToChannel(model);
(channels ?? _channels).TryAdd(model.Id);
dataStore.AddChannel(channel);
}
public ISocketGuildChannel GetChannel(ulong id)
{
return Discord.DataStore.GetChannel(id) as ISocketGuildChannel;
}
public ISocketGuildChannel RemoveChannel(ulong id)
{
_channels.TryRemove(id);
return Discord.DataStore.RemoveChannel(id) as ISocketGuildChannel;
}
public Role AddRole(RoleModel model, ConcurrentDictionary<ulong, Role> roles = null)
{
var role = new Role(this, model);
(roles ?? _roles)[model.Id] = role;
return role;
}
public Role RemoveRole(ulong id)
{
Role role;
if (_roles.TryRemove(id, out role))
return role;
return null;
}
public override Task<IGuildUser> GetUserAsync(ulong id) => Task.FromResult<IGuildUser>(GetUser(id));
public override Task<IGuildUser> GetCurrentUserAsync()
=> Task.FromResult<IGuildUser>(CurrentUser);
public override Task<IReadOnlyCollection<IGuildUser>> GetUsersAsync()
=> Task.FromResult<IReadOnlyCollection<IGuildUser>>(Members);
public SocketGuildUser AddOrUpdateUser(MemberModel model, DataStore dataStore, ConcurrentDictionary<ulong, SocketGuildUser> members = null)
{
members = members ?? _members;
SocketGuildUser member;
if (members.TryGetValue(model.User.Id, out member))
member.Update(model, UpdateSource.WebSocket);
else
{
var user = Discord.GetOrAddUser(model.User, dataStore);
member = new SocketGuildUser(this, user, model);
members[user.Id] = member;
DownloadedMemberCount++;
}
return member;
}
public SocketGuildUser AddOrUpdateUser(PresenceModel model, DataStore dataStore, ConcurrentDictionary<ulong, SocketGuildUser> members = null)
{
members = members ?? _members;
SocketGuildUser member;
if (members.TryGetValue(model.User.Id, out member))
member.Update(model, UpdateSource.WebSocket);
else
{
var user = Discord.GetOrAddUser(model.User, dataStore);
member = new SocketGuildUser(this, user, model);
members[user.Id] = member;
DownloadedMemberCount++;
}
return member;
}
public SocketGuildUser GetUser(ulong id)
{
SocketGuildUser member;
if (_members.TryGetValue(id, out member))
return member;
return null;
}
public SocketGuildUser RemoveUser(ulong id)
{
SocketGuildUser member;
if (_members.TryRemove(id, out member))
{
DownloadedMemberCount--;
return member;
}
member.User.RemoveRef(Discord);
return null;
}
public override async Task DownloadUsersAsync()
{
await Discord.DownloadUsersAsync(new [] { this });
}
public void CompleteDownloadMembers()
{
_downloaderPromise.TrySetResultAsync(true);
}
public VoiceState AddOrUpdateVoiceState(VoiceStateModel model, DataStore dataStore, ConcurrentDictionary<ulong, VoiceState> voiceStates = null)
{
var voiceChannel = dataStore.GetChannel(model.ChannelId.Value) as SocketVoiceChannel;
var voiceState = new VoiceState(voiceChannel, model);
(voiceStates ?? _voiceStates)[model.UserId] = voiceState;
return voiceState;
}
public VoiceState? GetVoiceState(ulong id)
{
VoiceState voiceState;
if (_voiceStates.TryGetValue(id, out voiceState))
return voiceState;
return null;
}
public VoiceState? RemoveVoiceState(ulong id)
{
VoiceState voiceState;
if (_voiceStates.TryRemove(id, out voiceState))
return voiceState;
return null;
}
public async Task<IAudioClient> ConnectAudioAsync(ulong channelId, bool selfDeaf, bool selfMute)
{
try
{
TaskCompletionSource<AudioClient> promise;
await _audioLock.WaitAsync().ConfigureAwait(false);
try
{
await DisconnectAudioInternalAsync().ConfigureAwait(false);
promise = new TaskCompletionSource<AudioClient>();
_audioConnectPromise = promise;
await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, channelId, selfDeaf, selfMute).ConfigureAwait(false);
}
finally
{
_audioLock.Release();
}
var timeoutTask = Task.Delay(15000);
if (await Task.WhenAny(promise.Task, timeoutTask) == timeoutTask)
throw new TimeoutException();
return await promise.Task.ConfigureAwait(false);
}
catch (Exception)
{
await DisconnectAudioInternalAsync().ConfigureAwait(false);
throw;
}
}
public async Task DisconnectAudioAsync(AudioClient client = null)
{
await _audioLock.WaitAsync().ConfigureAwait(false);
try
{
await DisconnectAudioInternalAsync(client).ConfigureAwait(false);
}
finally
{
_audioLock.Release();
}
}
private async Task DisconnectAudioInternalAsync(AudioClient client = null)
{
var oldClient = AudioClient;
if (oldClient != null)
{
if (client == null || oldClient == client)
{
_audioConnectPromise?.TrySetCanceledAsync(); //Cancel any previous audio connection
_audioConnectPromise = null;
}
if (oldClient == client)
{
AudioClient = null;
await oldClient.DisconnectAsync().ConfigureAwait(false);
}
}
}
public async Task FinishConnectAudio(int id, string url, string token)
{
var voiceState = GetVoiceState(CurrentUser.Id).Value;
await _audioLock.WaitAsync().ConfigureAwait(false);
try
{
if (AudioClient == null)
{
var audioClient = new AudioClient(this, id);
audioClient.Disconnected += async ex =>
{
await _audioLock.WaitAsync().ConfigureAwait(false);
try
{
if (AudioClient == audioClient) //Only reconnect if we're still assigned as this guild's audio client
{
if (ex != null)
{
//Reconnect if we still have channel info.
//TODO: Is this threadsafe? Could channel data be deleted before we access it?
var voiceState2 = GetVoiceState(CurrentUser.Id);
if (voiceState2.HasValue)
{
var voiceChannelId = voiceState2.Value.VoiceChannel?.Id;
if (voiceChannelId != null)
await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, voiceChannelId, voiceState2.Value.IsSelfDeafened, voiceState2.Value.IsSelfMuted);
}
}
else
{
try { AudioClient.Dispose(); } catch { }
AudioClient = null;
}
}
}
finally
{
_audioLock.Release();
}
};
AudioClient = audioClient;
}
await AudioClient.ConnectAsync(url, CurrentUser.Id, voiceState.VoiceSessionId, token).ConfigureAwait(false);
await _audioConnectPromise.TrySetResultAsync(AudioClient).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
await DisconnectAudioAsync();
}
catch (Exception e)
{
await _audioConnectPromise.SetExceptionAsync(e).ConfigureAwait(false);
await DisconnectAudioAsync();
}
finally
{
_audioLock.Release();
}
}
public async Task FinishJoinAudioChannel()
{
await _audioLock.WaitAsync().ConfigureAwait(false);
try
{
if (AudioClient != null)
await _audioConnectPromise.TrySetResultAsync(AudioClient).ConfigureAwait(false);
}
finally
{
_audioLock.Release();
}
}
public SocketGuild Clone() => MemberwiseClone() as SocketGuild;
new internal ISocketGuildChannel ToChannel(ChannelModel model)
{
switch (model.Type)
{
case ChannelType.Text:
return new SocketTextChannel(this, model);
case ChannelType.Voice:
return new SocketVoiceChannel(this, model);
default:
throw new InvalidOperationException($"Unexpected channel type: {model.Type}");
}
}
bool IUserGuild.IsOwner => OwnerId == Discord.CurrentUser.Id;
GuildPermissions IUserGuild.Permissions => CurrentUser.GuildPermissions;
IAudioClient IGuild.AudioClient => AudioClient;
}
}

View File

@@ -0,0 +1,76 @@
using Discord.API.Rest;
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Model = Discord.API.Integration;
namespace Discord.Rest
{
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
internal class GuildIntegration : IEntity<ulong>, IGuildIntegration
{
private long _syncedAtTicks;
public string Name { get; private set; }
public string Type { get; private set; }
public bool IsEnabled { get; private set; }
public bool IsSyncing { get; private set; }
public ulong ExpireBehavior { get; private set; }
public ulong ExpireGracePeriod { get; private set; }
public Guild Guild { get; private set; }
public Role Role { get; private set; }
public User User { get; private set; }
public IntegrationAccount Account { get; private set; }
public override DiscordRestClient Discord => Guild.Discord;
public DateTimeOffset SyncedAt => DateTimeUtils.FromTicks(_syncedAtTicks);
public GuildIntegration(Guild guild, Model model)
: base(model.Id)
{
Guild = guild;
Update(model);
}
public void Update(Model model)
{
Name = model.Name;
Type = model.Type;
IsEnabled = model.Enabled;
IsSyncing = model.Syncing;
ExpireBehavior = model.ExpireBehavior;
ExpireGracePeriod = model.ExpireGracePeriod;
_syncedAtTicks = model.SyncedAt.UtcTicks;
Role = Guild.GetRole(model.RoleId);
User = new User(model.User);
}
public async Task DeleteAsync()
{
await Discord.ApiClient.DeleteGuildIntegrationAsync(Guild.Id, Id).ConfigureAwait(false);
}
public async Task ModifyAsync(Action<ModifyGuildIntegrationParams> func)
{
if (func == null) throw new NullReferenceException(nameof(func));
var args = new ModifyGuildIntegrationParams();
func(args);
var model = await Discord.ApiClient.ModifyGuildIntegrationAsync(Guild.Id, Id, args).ConfigureAwait(false);
Update(model, UpdateSource.Rest);
}
public async Task SyncAsync()
{
await Discord.ApiClient.SyncGuildIntegrationAsync(Guild.Id, Id).ConfigureAwait(false);
}
public override string ToString() => Name;
private string DebuggerDisplay => $"{Name} ({Id}{(IsEnabled ? ", Enabled" : "")})";
IGuild IGuildIntegration.Guild => Guild;
IUser IGuildIntegration.User => User;
IRole IGuildIntegration.Role => Role;
}
}

View File

@@ -0,0 +1,13 @@
using Model = Discord.API.Message;
namespace Discord.WebSocket
{
internal interface ISocketMessage : IMessage
{
DiscordSocketClient Discord { get; }
new ISocketMessageChannel Channel { get; }
void Update(Model model);
ISocketMessage Clone();
}
}

View File

@@ -0,0 +1,20 @@
using Discord.Rest;
using Model = Discord.API.Message;
namespace Discord.WebSocket
{
internal class SocketSystemMessage : SystemMessage, ISocketMessage
{
internal override bool IsAttached => true;
public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient;
public new ISocketMessageChannel Channel => base.Channel as ISocketMessageChannel;
public SocketSystemMessage(ISocketMessageChannel channel, IUser author, Model model)
: base(channel, author, model)
{
}
public ISocketMessage Clone() => MemberwiseClone() as ISocketMessage;
}
}

View File

@@ -0,0 +1,20 @@
using Discord.Rest;
using Model = Discord.API.Message;
namespace Discord.WebSocket
{
internal class SocketUserMessage : UserMessage, ISocketMessage
{
internal override bool IsAttached => true;
public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient;
public new ISocketMessageChannel Channel => base.Channel as ISocketMessageChannel;
public SocketUserMessage(ISocketMessageChannel channel, IUser author, Model model)
: base(channel, author, model)
{
}
public ISocketMessage Clone() => MemberwiseClone() as ISocketMessage;
}
}

View File

@@ -0,0 +1,9 @@
namespace Discord.WebSocket
{
internal interface ISocketUser : IUser, IEntity<ulong>
{
SocketGlobalUser User { get; }
ISocketUser Clone();
}
}

View File

@@ -0,0 +1,17 @@
namespace Discord.WebSocket
{
//TODO: C#7 Candidate for record type
internal struct Presence : IPresence
{
public Game Game { get; }
public UserStatus Status { get; }
public Presence(Game game, UserStatus status)
{
Game = game;
Status = status;
}
public Presence Clone() => this;
}
}

View File

@@ -0,0 +1,46 @@
using System;
using System.Diagnostics;
using PresenceModel = Discord.API.Presence;
namespace Discord.WebSocket
{
[DebuggerDisplay("{DebuggerDisplay,nq}")]
internal class SocketDMUser : ISocketUser
{
internal bool IsAttached => true;
bool IEntity<ulong>.IsAttached => IsAttached;
public SocketGlobalUser User { get; }
public DiscordSocketClient Discord => User.Discord;
public Game Game => Presence.Game;
public UserStatus Status => Presence.Status;
public Presence Presence => User.Presence; //{ get; private set; }
public ulong Id => User.Id;
public string AvatarUrl => User.AvatarUrl;
public DateTimeOffset CreatedAt => User.CreatedAt;
public string Discriminator => User.Discriminator;
public ushort DiscriminatorValue => User.DiscriminatorValue;
public bool IsBot => User.IsBot;
public string Mention => MentionUtils.Mention(this);
public string Username => User.Username;
public SocketDMUser(SocketGlobalUser user)
{
User = user;
}
public void Update(PresenceModel model)
{
User.Update(model, source);
}
public SocketDMUser Clone() => MemberwiseClone() as SocketDMUser;
ISocketUser ISocketUser.Clone() => Clone();
public override string ToString() => $"{Username}#{Discriminator}";
private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id})";
}
}

View File

@@ -0,0 +1,60 @@
using Discord.Rest;
using System;
using Model = Discord.API.User;
using PresenceModel = Discord.API.Presence;
namespace Discord.WebSocket
{
internal class SocketGlobalUser : User, ISocketUser
{
internal override bool IsAttached => true;
private ushort _references;
public Presence Presence { get; private set; }
public new DiscordSocketClient Discord { get { throw new NotSupportedException(); } }
SocketGlobalUser ISocketUser.User => this;
public SocketGlobalUser(Model model)
: base(model)
{
}
public void AddRef()
{
checked
{
lock (this)
_references++;
}
}
public void RemoveRef(DiscordSocketClient discord)
{
lock (this)
{
if (--_references == 0)
discord.RemoveUser(Id);
}
}
public override void Update(Model model)
{
lock (this)
base.Update(model, source);
}
public void Update(PresenceModel model)
{
//Race conditions are okay here. Multiple shards racing already cant guarantee presence in order.
//lock (this)
//{
var game = model.Game != null ? new Game(model.Game) : null;
Presence = new Presence(game, model.Status);
//}
}
public SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser;
ISocketUser ISocketUser.Clone() => Clone();
}
}

View File

@@ -0,0 +1,36 @@
using Discord.Rest;
using System.Diagnostics;
namespace Discord.WebSocket
{
[DebuggerDisplay("{DebuggerDisplay,nq}")]
internal class SocketGroupUser : GroupUser, ISocketUser
{
internal override bool IsAttached => true;
public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient;
public new SocketGroupChannel Channel => base.Channel as SocketGroupChannel;
public new SocketGlobalUser User => base.User as SocketGlobalUser;
public Presence Presence => User.Presence; //{ get; private set; }
public override Game Game => Presence.Game;
public override UserStatus Status => Presence.Status;
public VoiceState? VoiceState => Channel.GetVoiceState(Id);
public bool IsSelfDeafened => VoiceState?.IsSelfDeafened ?? false;
public bool IsSelfMuted => VoiceState?.IsSelfMuted ?? false;
public bool IsSuppressed => VoiceState?.IsSuppressed ?? false;
public SocketVoiceChannel VoiceChannel => VoiceState?.VoiceChannel;
public SocketGroupUser(SocketGroupChannel channel, SocketGlobalUser user)
: base(channel, user)
{
}
public SocketGroupUser Clone() => MemberwiseClone() as SocketGroupUser;
ISocketUser ISocketUser.Clone() => Clone();
public override string ToString() => $"{Username}#{Discriminator}";
private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id})";
}
}

View File

@@ -0,0 +1,53 @@
using Discord.Rest;
using Model = Discord.API.GuildMember;
using PresenceModel = Discord.API.Presence;
namespace Discord.WebSocket
{
internal class SocketGuildUser : GuildUser, ISocketUser, IVoiceState
{
internal override bool IsAttached => true;
public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient;
public new SocketGuild Guild => base.Guild as SocketGuild;
public new SocketGlobalUser User => base.User as SocketGlobalUser;
public Presence Presence => User.Presence; //{ get; private set; }
public override Game Game => Presence.Game;
public override UserStatus Status => Presence.Status;
public VoiceState? VoiceState => Guild.GetVoiceState(Id);
public bool IsSelfDeafened => VoiceState?.IsSelfDeafened ?? false;
public bool IsSelfMuted => VoiceState?.IsSelfMuted ?? false;
public bool IsSuppressed => VoiceState?.IsSuppressed ?? false;
public SocketVoiceChannel VoiceChannel => VoiceState?.VoiceChannel;
public bool IsDeafened => VoiceState?.IsDeafened ?? false;
public bool IsMuted => VoiceState?.IsMuted ?? false;
public string VoiceSessionId => VoiceState?.VoiceSessionId ?? "";
public SocketGuildUser(SocketGuild guild, SocketGlobalUser user, Model model)
: base(guild, user, model)
{
//Presence = new Presence(null, UserStatus.Offline);
}
public SocketGuildUser(SocketGuild guild, SocketGlobalUser user, PresenceModel model)
: base(guild, user, model)
{
}
public override void Update(PresenceModel model)
{
base.Update(model, source);
var game = model.Game != null ? new Game(model.Game) : null;
//Presence = new Presence(game, model.Status);
User.Update(model, source);
}
IVoiceChannel IVoiceState.VoiceChannel => VoiceState?.VoiceChannel;
public SocketGuildUser Clone() => MemberwiseClone() as SocketGuildUser;
ISocketUser ISocketUser.Clone() => Clone();
}
}

View File

@@ -0,0 +1,47 @@
using Discord.API.Rest;
using Discord.Rest;
using System;
using System.Threading.Tasks;
using Model = Discord.API.User;
namespace Discord.WebSocket
{
internal class SocketSelfUser : SelfUser, ISocketUser, ISelfUser
{
internal override bool IsAttached => true;
public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient;
SocketGlobalUser ISocketUser.User { get { throw new NotSupportedException(); } }
public SocketSelfUser(DiscordSocketClient discord, Model model)
: base(discord, model)
{
}
public async Task ModifyStatusAsync(Action<ModifyPresenceParams> func)
{
if (func == null) throw new NullReferenceException(nameof(func));
var args = new ModifyPresenceParams();
func(args);
var game = args._game.GetValueOrDefault(_game);
var status = args._status.GetValueOrDefault(_status);
long idleSince = _idleSince;
if (status == UserStatus.Idle && _status != UserStatus.Idle)
idleSince = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var apiGame = game != null ? new API.Game { Name = game.Name, StreamType = game.StreamType, StreamUrl = game.StreamUrl } : null;
await Discord.ApiClient.SendStatusUpdateAsync(status == UserStatus.Idle ? _idleSince : (long?)null, apiGame).ConfigureAwait(false);
//Save values
_idleSince = idleSince;
_game = game;
_status = status;
}
public SocketSelfUser Clone() => MemberwiseClone() as SocketSelfUser;
ISocketUser ISocketUser.Clone() => Clone();
}
}

View File

@@ -0,0 +1,52 @@
using System;
using Model = Discord.API.VoiceState;
namespace Discord.WebSocket
{
//TODO: C#7 Candidate for record type
internal struct VoiceState : IVoiceState
{
[Flags]
private enum Flags : byte
{
None = 0x00,
Suppressed = 0x01,
Muted = 0x02,
Deafened = 0x04,
SelfMuted = 0x08,
SelfDeafened = 0x10,
}
private readonly Flags _voiceStates;
public SocketVoiceChannel VoiceChannel { get; }
public string VoiceSessionId { get; }
public bool IsMuted => (_voiceStates & Flags.Muted) != 0;
public bool IsDeafened => (_voiceStates & Flags.Deafened) != 0;
public bool IsSuppressed => (_voiceStates & Flags.Suppressed) != 0;
public bool IsSelfMuted => (_voiceStates & Flags.SelfMuted) != 0;
public bool IsSelfDeafened => (_voiceStates & Flags.SelfDeafened) != 0;
public VoiceState(SocketVoiceChannel voiceChannel, Model model)
: this(voiceChannel, model.SessionId, model.SelfMute, model.SelfDeaf, model.Suppress) { }
public VoiceState(SocketVoiceChannel voiceChannel, string sessionId, bool isSelfMuted, bool isSelfDeafened, bool isSuppressed)
{
VoiceChannel = voiceChannel;
VoiceSessionId = sessionId;
Flags voiceStates = Flags.None;
if (isSelfMuted)
voiceStates |= Flags.SelfMuted;
if (isSelfDeafened)
voiceStates |= Flags.SelfDeafened;
if (isSuppressed)
voiceStates |= Flags.Suppressed;
_voiceStates = voiceStates;
}
public VoiceState Clone() => this;
IVoiceChannel IVoiceState.VoiceChannel => VoiceChannel;
}
}

View File

@@ -0,0 +1,87 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
namespace Discord.WebSocket
{
internal class MessageCache : MessageManager
{
private readonly ConcurrentDictionary<ulong, ISocketMessage> _messages;
private readonly ConcurrentQueue<ulong> _orderedMessages;
private readonly int _size;
public override IReadOnlyCollection<ISocketMessage> Messages => _messages.ToReadOnlyCollection();
public MessageCache(DiscordSocketClient discord, ISocketMessageChannel channel)
: base(discord, channel)
{
_size = discord.MessageCacheSize;
_messages = new ConcurrentDictionary<ulong, ISocketMessage>(1, (int)(_size * 1.05));
_orderedMessages = new ConcurrentQueue<ulong>();
}
public override void Add(ISocketMessage message)
{
if (_messages.TryAdd(message.Id, message))
{
_orderedMessages.Enqueue(message.Id);
ulong msgId;
ISocketMessage msg;
while (_orderedMessages.Count > _size && _orderedMessages.TryDequeue(out msgId))
_messages.TryRemove(msgId, out msg);
}
}
public override ISocketMessage Remove(ulong id)
{
ISocketMessage msg;
_messages.TryRemove(id, out msg);
return msg;
}
public override ISocketMessage Get(ulong id)
{
ISocketMessage result;
if (_messages.TryGetValue(id, out result))
return result;
return null;
}
public override IImmutableList<ISocketMessage> GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch)
{
if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit));
if (limit == 0) return ImmutableArray<ISocketMessage>.Empty;
IEnumerable<ulong> cachedMessageIds;
if (fromMessageId == null)
cachedMessageIds = _orderedMessages;
else if (dir == Direction.Before)
cachedMessageIds = _orderedMessages.Where(x => x < fromMessageId.Value);
else
cachedMessageIds = _orderedMessages.Where(x => x > fromMessageId.Value);
return cachedMessageIds
.Take(limit)
.Select(x =>
{
ISocketMessage msg;
if (_messages.TryGetValue(x, out msg))
return msg;
return null;
})
.Where(x => x != null)
.ToImmutableArray();
}
public override async Task<ISocketMessage> DownloadAsync(ulong id)
{
var msg = Get(id);
if (msg != null)
return msg;
return await base.DownloadAsync(id).ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,92 @@
using Discord.API.Rest;
using Discord.Rest;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Model = Discord.API.Message;
namespace Discord.WebSocket
{
internal class MessageManager
{
private readonly DiscordSocketClient _discord;
private readonly ISocketMessageChannel _channel;
public virtual IReadOnlyCollection<ISocketMessage> Messages
=> ImmutableArray.Create<ISocketMessage>();
public MessageManager(DiscordSocketClient discord, ISocketMessageChannel channel)
{
_discord = discord;
_channel = channel;
}
public virtual void Add(ISocketMessage message) { }
public virtual ISocketMessage Remove(ulong id) => null;
public virtual ISocketMessage Get(ulong id) => null;
public virtual IImmutableList<ISocketMessage> GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch)
=> ImmutableArray.Create<ISocketMessage>();
public virtual async Task<ISocketMessage> DownloadAsync(ulong id)
{
var model = await _discord.ApiClient.GetChannelMessageAsync(_channel.Id, id).ConfigureAwait(false);
if (model != null)
return Create(new User(model.Author.Value), model);
return null;
}
public async Task<IReadOnlyCollection<ISocketMessage>> DownloadAsync(ulong? fromId, Direction dir, int limit)
{
//TODO: Test heavily, especially the ordering of messages
if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit));
if (limit == 0) return ImmutableArray<ISocketMessage>.Empty;
var cachedMessages = GetMany(fromId, dir, limit);
if (cachedMessages.Count == limit)
return cachedMessages;
else if (cachedMessages.Count > limit)
return cachedMessages.Skip(cachedMessages.Count - limit).ToImmutableArray();
else
{
var args = new GetChannelMessagesParams
{
Limit = limit - cachedMessages.Count,
RelativeDirection = dir
};
if (cachedMessages.Count == 0)
{
if (fromId != null)
args.RelativeMessageId = fromId.Value;
}
else
args.RelativeMessageId = dir == Direction.Before ? cachedMessages[0].Id : cachedMessages[cachedMessages.Count - 1].Id;
var downloadedMessages = await _discord.ApiClient.GetChannelMessagesAsync(_channel.Id, args).ConfigureAwait(false);
var guild = (_channel as ISocketGuildChannel)?.Guild;
return cachedMessages.Concat(downloadedMessages.Select(x =>
{
IUser user = _channel.GetUser(x.Author.Value.Id, true);
if (user == null)
{
var newUser = new User(x.Author.Value);
if (guild != null)
user = new GuildUser(guild, newUser);
else
user = newUser;
}
return Create(user, x);
})).ToImmutableArray();
}
}
public ISocketMessage Create(IUser author, Model model)
{
if (model.Type == MessageType.Default)
return new SocketUserMessage(_channel, author, model);
else
return new SocketSystemMessage(_channel, author, model);
}
}
}

View File

@@ -0,0 +1,19 @@
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: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Discord.Net.WebSocket")]
[assembly: AssemblyTrademark("")]
// 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("22ab6c66-536c-4ac2-bbdb-a8bc4eb6b14d")]

View File

@@ -0,0 +1,20 @@
{
"version": "1.0.0-*",
"buildOptions": {
"compile": {
"include": [ "../Discord.Net.Entities/**.cs", "../Discord.Net.Utils/**.cs" ]
},
"define": [ "WEBSOCKET" ]
},
"dependencies": {
"NETStandard.Library": "1.6.0"
},
"frameworks": {
"netstandard1.6": {
"imports": "dnxcore50"
}
}
}