Concrete class prototype
This commit is contained in:
246
src/Discord.Net.WebSocket/API/DiscordSocketApiClient.cs
Normal file
246
src/Discord.Net.WebSocket/API/DiscordSocketApiClient.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
259
src/Discord.Net.WebSocket/API/DiscordVoiceApiClient.cs
Normal file
259
src/Discord.Net.WebSocket/API/DiscordVoiceApiClient.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/Discord.Net.WebSocket/API/Gateway/ExtendedGuild.cs
Normal file
25
src/Discord.Net.WebSocket/API/Gateway/ExtendedGuild.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
33
src/Discord.Net.WebSocket/API/Gateway/GatewayOpCode.cs
Normal file
33
src/Discord.Net.WebSocket/API/Gateway/GatewayOpCode.cs
Normal 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
|
||||
}
|
||||
}
|
||||
13
src/Discord.Net.WebSocket/API/Gateway/GuildBanEvent.cs
Normal file
13
src/Discord.Net.WebSocket/API/Gateway/GuildBanEvent.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
11
src/Discord.Net.WebSocket/API/Gateway/GuildMemberAddEvent.cs
Normal file
11
src/Discord.Net.WebSocket/API/Gateway/GuildMemberAddEvent.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
18
src/Discord.Net.WebSocket/API/Gateway/GuildSyncEvent.cs
Normal file
18
src/Discord.Net.WebSocket/API/Gateway/GuildSyncEvent.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
11
src/Discord.Net.WebSocket/API/Gateway/HelloEvent.cs
Normal file
11
src/Discord.Net.WebSocket/API/Gateway/HelloEvent.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
21
src/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs
Normal file
21
src/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
38
src/Discord.Net.WebSocket/API/Gateway/ReadyEvent.cs
Normal file
38
src/Discord.Net.WebSocket/API/Gateway/ReadyEvent.cs
Normal 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")]*/
|
||||
}
|
||||
}
|
||||
13
src/Discord.Net.WebSocket/API/Gateway/RecipientEvent.cs
Normal file
13
src/Discord.Net.WebSocket/API/Gateway/RecipientEvent.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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(); } }
|
||||
}
|
||||
}
|
||||
16
src/Discord.Net.WebSocket/API/Gateway/ResumeParams.cs
Normal file
16
src/Discord.Net.WebSocket/API/Gateway/ResumeParams.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
11
src/Discord.Net.WebSocket/API/Gateway/ResumedEvent.cs
Normal file
11
src/Discord.Net.WebSocket/API/Gateway/ResumedEvent.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
14
src/Discord.Net.WebSocket/API/Gateway/StatusUpdateParams.cs
Normal file
14
src/Discord.Net.WebSocket/API/Gateway/StatusUpdateParams.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
15
src/Discord.Net.WebSocket/API/Gateway/TypingStartEvent.cs
Normal file
15
src/Discord.Net.WebSocket/API/Gateway/TypingStartEvent.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
14
src/Discord.Net.WebSocket/API/Gateway/UpdateStatusParams.cs
Normal file
14
src/Discord.Net.WebSocket/API/Gateway/UpdateStatusParams.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; } }
|
||||
}
|
||||
}
|
||||
17
src/Discord.Net.WebSocket/API/Voice/IdentifyParams.cs
Normal file
17
src/Discord.Net.WebSocket/API/Voice/IdentifyParams.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
17
src/Discord.Net.WebSocket/API/Voice/ReadyEvent.cs
Normal file
17
src/Discord.Net.WebSocket/API/Voice/ReadyEvent.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
13
src/Discord.Net.WebSocket/API/Voice/SelectProtocolParams.cs
Normal file
13
src/Discord.Net.WebSocket/API/Voice/SelectProtocolParams.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
13
src/Discord.Net.WebSocket/API/Voice/SpeakingParams.cs
Normal file
13
src/Discord.Net.WebSocket/API/Voice/SpeakingParams.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
15
src/Discord.Net.WebSocket/API/Voice/UdpProtocolInfo.cs
Normal file
15
src/Discord.Net.WebSocket/API/Voice/UdpProtocolInfo.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
21
src/Discord.Net.WebSocket/API/Voice/VoiceOpCode.cs
Normal file
21
src/Discord.Net.WebSocket/API/Voice/VoiceOpCode.cs
Normal 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
|
||||
}
|
||||
}
|
||||
331
src/Discord.Net.WebSocket/Audio/AudioClient.cs
Normal file
331
src/Discord.Net.WebSocket/Audio/AudioClient.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
13
src/Discord.Net.WebSocket/Audio/AudioMode.cs
Normal file
13
src/Discord.Net.WebSocket/Audio/AudioMode.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
namespace Discord.Audio
|
||||
{
|
||||
[Flags]
|
||||
public enum AudioMode : byte
|
||||
{
|
||||
Disabled = 0,
|
||||
Outgoing = 1,
|
||||
Incoming = 2,
|
||||
Both = Outgoing | Incoming
|
||||
}
|
||||
}
|
||||
51
src/Discord.Net.WebSocket/Audio/Opus/OpusConverter.cs
Normal file
51
src/Discord.Net.WebSocket/Audio/Opus/OpusConverter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/Discord.Net.WebSocket/Audio/Opus/OpusCtl.cs
Normal file
10
src/Discord.Net.WebSocket/Audio/Opus/OpusCtl.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Discord.Audio
|
||||
{
|
||||
internal enum OpusCtl : int
|
||||
{
|
||||
SetBitrateRequest = 4002,
|
||||
GetBitrateRequest = 4003,
|
||||
SetInbandFECRequest = 4012,
|
||||
GetInbandFECRequest = 4013
|
||||
}
|
||||
}
|
||||
49
src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs
Normal file
49
src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
75
src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs
Normal file
75
src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/Discord.Net.WebSocket/Audio/Opus/OpusError.cs
Normal file
14
src/Discord.Net.WebSocket/Audio/Opus/OpusError.cs
Normal 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
|
||||
}
|
||||
}
|
||||
36
src/Discord.Net.WebSocket/Audio/Sodium/SecretBox.cs
Normal file
36
src/Discord.Net.WebSocket/Audio/Sodium/SecretBox.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs
Normal file
30
src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs
Normal file
35
src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
53
src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs
Normal file
53
src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs
Normal 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(); }
|
||||
}
|
||||
}
|
||||
67
src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs
Normal file
67
src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs
Normal 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(); }
|
||||
}
|
||||
}
|
||||
131
src/Discord.Net.WebSocket/DataStore.cs
Normal file
131
src/Discord.Net.WebSocket/DataStore.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src/Discord.Net.WebSocket/Discord.Net.WebSocket.xproj
Normal file
21
src/Discord.Net.WebSocket/Discord.Net.WebSocket.xproj
Normal 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>
|
||||
203
src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs
Normal file
203
src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
1623
src/Discord.Net.WebSocket/DiscordSocketClient.cs
Normal file
1623
src/Discord.Net.WebSocket/DiscordSocketClient.cs
Normal file
File diff suppressed because it is too large
Load Diff
32
src/Discord.Net.WebSocket/DiscordSocketConfig.cs
Normal file
32
src/Discord.Net.WebSocket/DiscordSocketConfig.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
419
src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
Normal file
419
src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
9
src/Discord.Net.WebSocket/Entities/Users/ISocketUser.cs
Normal file
9
src/Discord.Net.WebSocket/Entities/Users/ISocketUser.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Discord.WebSocket
|
||||
{
|
||||
internal interface ISocketUser : IUser, IEntity<ulong>
|
||||
{
|
||||
SocketGlobalUser User { get; }
|
||||
|
||||
ISocketUser Clone();
|
||||
}
|
||||
}
|
||||
17
src/Discord.Net.WebSocket/Entities/Users/Presence.cs
Normal file
17
src/Discord.Net.WebSocket/Entities/Users/Presence.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
46
src/Discord.Net.WebSocket/Entities/Users/SocketDMUser.cs
Normal file
46
src/Discord.Net.WebSocket/Entities/Users/SocketDMUser.cs
Normal 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})";
|
||||
}
|
||||
}
|
||||
60
src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs
Normal file
60
src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
36
src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs
Normal file
36
src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs
Normal 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})";
|
||||
}
|
||||
}
|
||||
53
src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs
Normal file
53
src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
47
src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs
Normal file
47
src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
52
src/Discord.Net.WebSocket/Entities/Users/VoiceState.cs
Normal file
52
src/Discord.Net.WebSocket/Entities/Users/VoiceState.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
87
src/Discord.Net.WebSocket/Entities/Utilities/MessageCache.cs
Normal file
87
src/Discord.Net.WebSocket/Entities/Utilities/MessageCache.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/Discord.Net.WebSocket/Properties/AssemblyInfo.cs
Normal file
19
src/Discord.Net.WebSocket/Properties/AssemblyInfo.cs
Normal 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")]
|
||||
20
src/Discord.Net.WebSocket/project.json
Normal file
20
src/Discord.Net.WebSocket/project.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user