@@ -15,8 +15,6 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.Core", "src\Dis | |||
EndProject | |||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Impls", "Impls", "{288C363D-A636-4EAE-9AC1-4698B641B26E}" | |||
EndProject | |||
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Discord.Net.Utils", "src\Discord.Net.Utils\Discord.Net.Utils.shproj", "{2B75119C-9893-4AAA-8D38-6176EEB09060}" | |||
EndProject | |||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.Rest", "src\Discord.Net.Rest\Discord.Net.Rest.xproj", "{BFC6DC28-0351-4573-926A-D4124244C04F}" | |||
EndProject | |||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.WebSocket", "src\Discord.Net.WebSocket\Discord.Net.WebSocket.xproj", "{22AB6C66-536C-4AC2-BBDB-A8BC4EB6B14D}" | |||
@@ -24,9 +22,6 @@ EndProject | |||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.Rpc", "src\Discord.Net.Rpc\Discord.Net.Rpc.xproj", "{5688A353-121E-40A1-8BFA-B17B91FB48FB}" | |||
EndProject | |||
Global | |||
GlobalSection(SharedMSBuildProjectFiles) = preSolution | |||
src\Discord.Net.Utils\Discord.Net.Utils.projitems*{2b75119c-9893-4aaa-8d38-6176eeb09060}*SharedItemsImports = 13 | |||
EndGlobalSection | |||
GlobalSection(SolutionConfigurationPlatforms) = preSolution | |||
Debug|Any CPU = Debug|Any CPU | |||
Release|Any CPU = Release|Any CPU | |||
@@ -0,0 +1,7 @@ | |||
using System.Runtime.CompilerServices; | |||
[assembly: InternalsVisibleTo("Discord.Net.Rest")] | |||
[assembly: InternalsVisibleTo("Discord.Net.Rpc")] | |||
[assembly: InternalsVisibleTo("Discord.Net.WebSocket")] | |||
[assembly: InternalsVisibleTo("Discord.Net.Commands")] | |||
[assembly: InternalsVisibleTo("Discord.Net.Test")] |
@@ -14,7 +14,7 @@ namespace Discord | |||
public bool RequireColons { get; } | |||
public IReadOnlyList<ulong> RoleIds { get; } | |||
public Emoji(ulong id, string name, bool isManaged, bool requireColons, IReadOnlyList<ulong> roleIds) | |||
private Emoji(ulong id, string name, bool isManaged, bool requireColons, IReadOnlyList<ulong> roleIds) | |||
{ | |||
Id = id; | |||
Name = name; | |||
@@ -22,7 +22,7 @@ namespace Discord | |||
RequireColons = requireColons; | |||
RoleIds = roleIds; | |||
} | |||
public static Emoji Create(Model model) | |||
internal static Emoji Create(Model model) | |||
{ | |||
return new Emoji(model.Id, model.Name, model.Managed, model.RequireColons, ImmutableArray.Create(model.Roles)); | |||
} | |||
@@ -7,12 +7,12 @@ namespace Discord | |||
public string Name { get; } | |||
public string Url { get; } | |||
public EmbedProvider(string name, string url) | |||
private EmbedProvider(string name, string url) | |||
{ | |||
Name = name; | |||
Url = url; | |||
} | |||
public static EmbedProvider Create(Model model) | |||
internal static EmbedProvider Create(Model model) | |||
{ | |||
return new EmbedProvider(model.Name, model.Url); | |||
} | |||
@@ -9,14 +9,14 @@ namespace Discord | |||
public int? Height { get; } | |||
public int? Width { get; } | |||
public EmbedThumbnail(string url, string proxyUrl, int? height, int? width) | |||
private EmbedThumbnail(string url, string proxyUrl, int? height, int? width) | |||
{ | |||
Url = url; | |||
ProxyUrl = proxyUrl; | |||
Height = height; | |||
Width = width; | |||
} | |||
public static EmbedThumbnail Create(Model model) | |||
internal static EmbedThumbnail Create(Model model) | |||
{ | |||
return new EmbedThumbnail(model.Url, model.ProxyUrl, | |||
model.Height.IsSpecified ? model.Height.Value : (int?)null, | |||
@@ -16,9 +16,9 @@ namespace Discord | |||
StreamUrl = streamUrl; | |||
StreamType = type; | |||
} | |||
public Game(string name) | |||
private Game(string name) | |||
: this(name, null, StreamType.NotStreaming) { } | |||
public static Game Create(Model model) | |||
internal static Game Create(Model model) | |||
{ | |||
return new Game(model.Name, | |||
model.StreamUrl.GetValueOrDefault(null), | |||
@@ -5,6 +5,8 @@ | |||
Unknown, | |||
Online, | |||
Idle, | |||
DoNotDisturb, | |||
Invisible, | |||
Offline | |||
} | |||
} |
@@ -27,9 +27,7 @@ | |||
public static string Sanitize(string text) | |||
{ | |||
foreach (string unsafeChar in SensitiveCharacters) | |||
{ | |||
text = text.Replace(unsafeChar, $"\\{unsafeChar}"); | |||
} | |||
return text; | |||
} | |||
} | |||
@@ -26,14 +26,12 @@ namespace Discord | |||
Task<IGuild> GetGuildAsync(ulong id); | |||
Task<IReadOnlyCollection<IGuild>> GetGuildsAsync(); | |||
Task<IReadOnlyCollection<IUserGuild>> GetGuildSummariesAsync(); | |||
Task<IGuild> CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null); | |||
Task<IInvite> GetInviteAsync(string inviteId); | |||
Task<IUser> GetUserAsync(ulong id); | |||
Task<IUser> GetUserAsync(string username, string discriminator); | |||
Task<IReadOnlyCollection<IUser>> QueryUsersAsync(string query, int limit); | |||
Task<IReadOnlyCollection<IVoiceRegion>> GetVoiceRegionsAsync(); | |||
Task<IVoiceRegion> GetVoiceRegionAsync(string id); | |||
@@ -1,4 +1,5 @@ | |||
using System; | |||
using System.Runtime.InteropServices; | |||
using System.Threading.Tasks; | |||
namespace Discord.Logging | |||
@@ -6,6 +7,7 @@ namespace Discord.Logging | |||
internal class LogManager | |||
{ | |||
public LogSeverity Level { get; } | |||
public Logger ClientLogger { get; } | |||
public event Func<LogMessage, Task> Message { add { _messageEvent.Add(value); } remove { _messageEvent.Remove(value); } } | |||
private readonly AsyncEvent<Func<LogMessage, Task>> _messageEvent = new AsyncEvent<Func<LogMessage, Task>>(); | |||
@@ -13,6 +15,7 @@ namespace Discord.Logging | |||
public LogManager(LogSeverity minSeverity) | |||
{ | |||
Level = minSeverity; | |||
ClientLogger = new Logger(this, "Discord"); | |||
} | |||
public async Task LogAsync(LogSeverity severity, string source, string message, Exception ex = null) | |||
@@ -67,5 +70,21 @@ namespace Discord.Logging | |||
=> LogAsync(LogSeverity.Debug, source, ex); | |||
public Logger CreateLogger(string name) => new Logger(this, name); | |||
public async Task WriteInitialLog() | |||
{ | |||
await ClientLogger.InfoAsync($"Discord.Net v{DiscordConfig.Version} (API v{DiscordConfig.APIVersion})").ConfigureAwait(false); | |||
await ClientLogger.VerboseAsync($"Runtime: {RuntimeInformation.FrameworkDescription.Trim()} ({ToArchString(RuntimeInformation.ProcessArchitecture)})").ConfigureAwait(false); | |||
await ClientLogger.VerboseAsync($"OS: {RuntimeInformation.OSDescription.Trim()} ({ToArchString(RuntimeInformation.OSArchitecture)})").ConfigureAwait(false); | |||
} | |||
private static string ToArchString(Architecture arch) | |||
{ | |||
switch (arch) | |||
{ | |||
case Architecture.X64: return "x64"; | |||
case Architecture.X86: return "x86"; | |||
default: return arch.ToString(); | |||
} | |||
} | |||
} | |||
} |
@@ -19,6 +19,10 @@ namespace Discord.Net.Converters | |||
return UserStatus.Online; | |||
case "idle": | |||
return UserStatus.Idle; | |||
case "dnd": | |||
return UserStatus.DoNotDisturb; | |||
case "invisible": | |||
return UserStatus.Invisible; //Should never happen | |||
case "offline": | |||
return UserStatus.Offline; | |||
default: | |||
@@ -36,6 +40,12 @@ namespace Discord.Net.Converters | |||
case UserStatus.Idle: | |||
writer.WriteValue("idle"); | |||
break; | |||
case UserStatus.DoNotDisturb: | |||
writer.WriteValue("dnd"); | |||
break; | |||
case UserStatus.Invisible: | |||
writer.WriteValue("invisible"); | |||
break; | |||
case UserStatus.Offline: | |||
writer.WriteValue("offline"); | |||
break; | |||
@@ -0,0 +1,6 @@ | |||
using System.Runtime.CompilerServices; | |||
[assembly: InternalsVisibleTo("Discord.Net.Rpc")] | |||
[assembly: InternalsVisibleTo("Discord.Net.WebSocket")] | |||
[assembly: InternalsVisibleTo("Discord.Net.Commands")] | |||
[assembly: InternalsVisibleTo("Discord.Net.Test")] |
@@ -0,0 +1,118 @@ | |||
using Discord.API.Rest; | |||
using System.Collections.Generic; | |||
using System.Collections.Immutable; | |||
using System.IO; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
namespace Discord.Rest | |||
{ | |||
internal static class ClientHelper | |||
{ | |||
//Applications | |||
public static async Task<RestApplication> GetApplicationInfoAsync(DiscordClient client) | |||
{ | |||
var model = await client.ApiClient.GetMyApplicationAsync().ConfigureAwait(false); | |||
return RestApplication.Create(client, model); | |||
} | |||
public static async Task<RestChannel> GetChannelAsync(DiscordClient client, | |||
ulong id) | |||
{ | |||
var model = await client.ApiClient.GetChannelAsync(id).ConfigureAwait(false); | |||
if (model != null) | |||
return RestChannel.Create(client, model); | |||
return null; | |||
} | |||
public static async Task<IReadOnlyCollection<IPrivateChannel>> GetPrivateChannelsAsync(DiscordClient client) | |||
{ | |||
var models = await client.ApiClient.GetMyPrivateChannelsAsync().ConfigureAwait(false); | |||
return models.Select(x => RestDMChannel.Create(client, x)).ToImmutableArray(); | |||
} | |||
public static async Task<IReadOnlyCollection<RestConnection>> GetConnectionsAsync(DiscordClient client) | |||
{ | |||
var models = await client.ApiClient.GetMyConnectionsAsync().ConfigureAwait(false); | |||
return models.Select(x => RestConnection.Create(x)).ToImmutableArray(); | |||
} | |||
public static async Task<RestInvite> GetInviteAsync(DiscordClient client, | |||
string inviteId) | |||
{ | |||
var model = await client.ApiClient.GetInviteAsync(inviteId).ConfigureAwait(false); | |||
if (model != null) | |||
return RestInvite.Create(client, model); | |||
return null; | |||
} | |||
public static async Task<RestGuild> GetGuildAsync(DiscordClient client, | |||
ulong id) | |||
{ | |||
var model = await client.ApiClient.GetGuildAsync(id).ConfigureAwait(false); | |||
if (model != null) | |||
return RestGuild.Create(client, model); | |||
return null; | |||
} | |||
public static async Task<RestGuildEmbed?> GetGuildEmbedAsync(DiscordClient client, | |||
ulong id) | |||
{ | |||
var model = await client.ApiClient.GetGuildEmbedAsync(id).ConfigureAwait(false); | |||
if (model != null) | |||
return RestGuildEmbed.Create(model); | |||
return null; | |||
} | |||
public static async Task<IReadOnlyCollection<RestUserGuild>> GetGuildSummariesAsync(DiscordClient client) | |||
{ | |||
var models = await client.ApiClient.GetMyGuildsAsync().ConfigureAwait(false); | |||
return models.Select(x => RestUserGuild.Create(client, x)).ToImmutableArray(); | |||
} | |||
public static async Task<IReadOnlyCollection<RestGuild>> GetGuildsAsync(DiscordClient client) | |||
{ | |||
var summaryModels = await client.ApiClient.GetMyGuildsAsync().ConfigureAwait(false); | |||
var guilds = ImmutableArray.CreateBuilder<RestGuild>(summaryModels.Count); | |||
foreach (var summaryModel in summaryModels) | |||
{ | |||
var guildModel = await client.ApiClient.GetGuildAsync(summaryModel.Id).ConfigureAwait(false); | |||
if (guildModel != null) | |||
guilds.Add(RestGuild.Create(client, guildModel)); | |||
} | |||
return guilds.ToImmutable(); | |||
} | |||
public static async Task<RestGuild> CreateGuildAsync(DiscordClient client, | |||
string name, IVoiceRegion region, Stream jpegIcon = null) | |||
{ | |||
var args = new CreateGuildParams(name, region.Id); | |||
var model = await client.ApiClient.CreateGuildAsync(args).ConfigureAwait(false); | |||
return RestGuild.Create(client, model); | |||
} | |||
public static async Task<RestUser> GetUserAsync(DiscordClient client, | |||
ulong id) | |||
{ | |||
var model = await client.ApiClient.GetUserAsync(id).ConfigureAwait(false); | |||
if (model != null) | |||
return RestUser.Create(client, model); | |||
return null; | |||
} | |||
public static async Task<RestUser> GetUserAsync(DiscordClient client, | |||
string username, string discriminator) | |||
{ | |||
var model = await client.ApiClient.GetUserAsync(username, discriminator).ConfigureAwait(false); | |||
if (model != null) | |||
return RestUser.Create(client, model); | |||
return null; | |||
} | |||
public static async Task<IReadOnlyCollection<RestVoiceRegion>> GetVoiceRegionsAsync(DiscordClient client) | |||
{ | |||
var models = await client.ApiClient.GetVoiceRegionsAsync().ConfigureAwait(false); | |||
return models.Select(x => RestVoiceRegion.Create(client, x)).ToImmutableArray(); | |||
} | |||
public static async Task<RestVoiceRegion> GetVoiceRegionAsync(DiscordClient client, | |||
string id) | |||
{ | |||
var models = await client.ApiClient.GetVoiceRegionsAsync().ConfigureAwait(false); | |||
return models.Select(x => RestVoiceRegion.Create(client, x)).Where(x => x.Id == id).FirstOrDefault(); | |||
} | |||
} | |||
} |
@@ -0,0 +1,162 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.IO; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
using Discord.Logging; | |||
using System.Collections.Immutable; | |||
namespace Discord.Rest | |||
{ | |||
public abstract class DiscordClient : IDiscordClient | |||
{ | |||
public event Func<LogMessage, Task> Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } | |||
private readonly AsyncEvent<Func<LogMessage, Task>> _logEvent = new AsyncEvent<Func<LogMessage, Task>>(); | |||
public event Func<Task> LoggedIn { add { _loggedInEvent.Add(value); } remove { _loggedInEvent.Remove(value); } } | |||
private readonly AsyncEvent<Func<Task>> _loggedInEvent = new AsyncEvent<Func<Task>>(); | |||
public event Func<Task> LoggedOut { add { _loggedOutEvent.Add(value); } remove { _loggedOutEvent.Remove(value); } } | |||
private readonly AsyncEvent<Func<Task>> _loggedOutEvent = new AsyncEvent<Func<Task>>(); | |||
internal readonly Logger _restLogger, _queueLogger; | |||
internal readonly SemaphoreSlim _connectionLock; | |||
private bool _isFirstLogin; | |||
private bool _isDisposed; | |||
public API.DiscordRestApiClient ApiClient { get; } | |||
internal LogManager LogManager { get; } | |||
public LoginState LoginState { get; private set; } | |||
public ISelfUser CurrentUser { get; protected set; } | |||
/// <summary> Creates a new REST-only discord client. </summary> | |||
internal DiscordClient(DiscordRestConfig config, API.DiscordRestApiClient client) | |||
{ | |||
ApiClient = client; | |||
LogManager = new LogManager(config.LogLevel); | |||
LogManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); | |||
_connectionLock = new SemaphoreSlim(1, 1); | |||
_restLogger = LogManager.CreateLogger("Rest"); | |||
_queueLogger = LogManager.CreateLogger("Queue"); | |||
_isFirstLogin = true; | |||
ApiClient.RequestQueue.RateLimitTriggered += async (id, bucket, millis) => | |||
{ | |||
await _queueLogger.WarningAsync($"Rate limit triggered (id = \"{id ?? "null"}\")").ConfigureAwait(false); | |||
if (bucket == null && id != null) | |||
await _queueLogger.WarningAsync($"Unknown rate limit bucket \"{id ?? "null"}\"").ConfigureAwait(false); | |||
}; | |||
ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false); | |||
} | |||
/// <inheritdoc /> | |||
public async Task LoginAsync(TokenType tokenType, string token, bool validateToken = true) | |||
{ | |||
await _connectionLock.WaitAsync().ConfigureAwait(false); | |||
try | |||
{ | |||
await LoginInternalAsync(tokenType, token).ConfigureAwait(false); | |||
} | |||
finally { _connectionLock.Release(); } | |||
} | |||
private async Task LoginInternalAsync(TokenType tokenType, string token) | |||
{ | |||
if (_isFirstLogin) | |||
{ | |||
_isFirstLogin = false; | |||
await LogManager.WriteInitialLog().ConfigureAwait(false); | |||
} | |||
if (LoginState != LoginState.LoggedOut) | |||
await LogoutInternalAsync().ConfigureAwait(false); | |||
LoginState = LoginState.LoggingIn; | |||
try | |||
{ | |||
await OnLoginAsync(tokenType, token).ConfigureAwait(false); | |||
LoginState = LoginState.LoggedIn; | |||
} | |||
catch (Exception) | |||
{ | |||
await LogoutInternalAsync().ConfigureAwait(false); | |||
throw; | |||
} | |||
await _loggedInEvent.InvokeAsync().ConfigureAwait(false); | |||
} | |||
protected virtual Task OnLoginAsync(TokenType tokenType, string token) { return Task.CompletedTask; } | |||
/// <inheritdoc /> | |||
public async Task LogoutAsync() | |||
{ | |||
await _connectionLock.WaitAsync().ConfigureAwait(false); | |||
try | |||
{ | |||
await LogoutInternalAsync().ConfigureAwait(false); | |||
} | |||
finally { _connectionLock.Release(); } | |||
} | |||
private async Task LogoutInternalAsync() | |||
{ | |||
if (LoginState == LoginState.LoggedOut) return; | |||
LoginState = LoginState.LoggingOut; | |||
await ApiClient.LogoutAsync().ConfigureAwait(false); | |||
await OnLogoutAsync().ConfigureAwait(false); | |||
CurrentUser = null; | |||
LoginState = LoginState.LoggedOut; | |||
await _loggedOutEvent.InvokeAsync().ConfigureAwait(false); | |||
} | |||
protected virtual Task OnLogoutAsync() { return Task.CompletedTask; } | |||
internal virtual void Dispose(bool disposing) | |||
{ | |||
if (!_isDisposed) | |||
{ | |||
ApiClient.Dispose(); | |||
_isDisposed = true; | |||
} | |||
} | |||
/// <inheritdoc /> | |||
public void Dispose() => Dispose(true); | |||
//IDiscordClient | |||
ConnectionState IDiscordClient.ConnectionState => ConnectionState.Disconnected; | |||
ISelfUser IDiscordClient.CurrentUser => CurrentUser; | |||
Task<IApplication> IDiscordClient.GetApplicationInfoAsync() { throw new NotSupportedException(); } | |||
Task<IChannel> IDiscordClient.GetChannelAsync(ulong id) | |||
=> Task.FromResult<IChannel>(null); | |||
Task<IReadOnlyCollection<IPrivateChannel>> IDiscordClient.GetPrivateChannelsAsync() | |||
=> Task.FromResult<IReadOnlyCollection<IPrivateChannel>>(ImmutableArray.Create<IPrivateChannel>()); | |||
Task<IReadOnlyCollection<IConnection>> IDiscordClient.GetConnectionsAsync() | |||
=> Task.FromResult<IReadOnlyCollection<IConnection>>(ImmutableArray.Create<IConnection>()); | |||
Task<IInvite> IDiscordClient.GetInviteAsync(string inviteId) | |||
=> Task.FromResult<IInvite>(null); | |||
Task<IGuild> IDiscordClient.GetGuildAsync(ulong id) | |||
=> Task.FromResult<IGuild>(null); | |||
Task<IReadOnlyCollection<IGuild>> IDiscordClient.GetGuildsAsync() | |||
=> Task.FromResult<IReadOnlyCollection<IGuild>>(ImmutableArray.Create<IGuild>()); | |||
Task<IGuild> IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon) { throw new NotSupportedException(); } | |||
Task<IUser> IDiscordClient.GetUserAsync(ulong id) | |||
=> Task.FromResult<IUser>(null); | |||
Task<IUser> IDiscordClient.GetUserAsync(string username, string discriminator) | |||
=> Task.FromResult<IUser>(null); | |||
Task<IReadOnlyCollection<IVoiceRegion>> IDiscordClient.GetVoiceRegionsAsync() | |||
=> Task.FromResult<IReadOnlyCollection<IVoiceRegion>>(ImmutableArray.Create<IVoiceRegion>()); | |||
Task<IVoiceRegion> IDiscordClient.GetVoiceRegionAsync(string id) | |||
=> Task.FromResult<IVoiceRegion>(null); | |||
Task IDiscordClient.ConnectAsync() { throw new NotSupportedException(); } | |||
Task IDiscordClient.DisconnectAsync() { throw new NotSupportedException(); } | |||
} | |||
} |
@@ -1,324 +1,105 @@ | |||
using Discord.API.Rest; | |||
using Discord.Net; | |||
using Discord.Net.Queue; | |||
using System; | |||
using Discord.Net.Queue; | |||
using System.Collections.Generic; | |||
using System.Collections.Immutable; | |||
using System.IO; | |||
using System.Linq; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
using System.Runtime.InteropServices; | |||
using Discord.Logging; | |||
namespace Discord.Rest | |||
{ | |||
public class DiscordRestClient : IDiscordClient | |||
public class DiscordRestClient : DiscordClient, IDiscordClient | |||
{ | |||
private readonly object _eventLock = new object(); | |||
public new RestSelfUser CurrentUser => base.CurrentUser as RestSelfUser; | |||
public event Func<LogMessage, Task> Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } | |||
private readonly AsyncEvent<Func<LogMessage, Task>> _logEvent = new AsyncEvent<Func<LogMessage, Task>>(); | |||
public event Func<Task> LoggedIn { add { _loggedInEvent.Add(value); } remove { _loggedInEvent.Remove(value); } } | |||
private readonly AsyncEvent<Func<Task>> _loggedInEvent = new AsyncEvent<Func<Task>>(); | |||
public event Func<Task> LoggedOut { add { _loggedOutEvent.Add(value); } remove { _loggedOutEvent.Remove(value); } } | |||
private readonly AsyncEvent<Func<Task>> _loggedOutEvent = new AsyncEvent<Func<Task>>(); | |||
internal readonly Logger _clientLogger, _restLogger, _queueLogger; | |||
internal readonly SemaphoreSlim _connectionLock; | |||
private bool _isFirstLogSub; | |||
internal bool _isDisposed; | |||
public API.DiscordRestApiClient ApiClient { get; } | |||
internal LogManager LogManager { get; } | |||
public LoginState LoginState { get; private set; } | |||
public RestSelfUser CurrentUser { get; private set; } | |||
/// <summary> Creates a new REST-only discord client. </summary> | |||
public DiscordRestClient() : this(new DiscordRestConfig()) { } | |||
public DiscordRestClient(DiscordRestConfig config) : this(config, CreateApiClient(config)) { } | |||
/// <summary> Creates a new REST-only discord client. </summary> | |||
internal DiscordRestClient(DiscordRestConfig config, API.DiscordRestApiClient client) | |||
{ | |||
ApiClient = client; | |||
LogManager = new LogManager(config.LogLevel); | |||
LogManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); | |||
_clientLogger = LogManager.CreateLogger("Client"); | |||
_restLogger = LogManager.CreateLogger("Rest"); | |||
_queueLogger = LogManager.CreateLogger("Queue"); | |||
_isFirstLogSub = true; | |||
public DiscordRestClient(DiscordRestConfig config) : base(config, CreateApiClient(config)) { } | |||
_connectionLock = new SemaphoreSlim(1, 1); | |||
ApiClient.RequestQueue.RateLimitTriggered += async (id, bucket, millis) => | |||
{ | |||
await _queueLogger.WarningAsync($"Rate limit triggered (id = \"{id ?? "null"}\")").ConfigureAwait(false); | |||
if (bucket == null && id != null) | |||
await _queueLogger.WarningAsync($"Unknown rate limit bucket \"{id ?? "null"}\"").ConfigureAwait(false); | |||
}; | |||
ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false); | |||
} | |||
private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config) | |||
=> new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent, requestQueue: new RequestQueue()); | |||
/// <inheritdoc /> | |||
public async Task LoginAsync(TokenType tokenType, string token, bool validateToken = true) | |||
{ | |||
await _connectionLock.WaitAsync().ConfigureAwait(false); | |||
try | |||
{ | |||
await LoginInternalAsync(tokenType, token).ConfigureAwait(false); | |||
} | |||
finally { _connectionLock.Release(); } | |||
} | |||
private async Task LoginInternalAsync(TokenType tokenType, string token) | |||
{ | |||
if (_isFirstLogSub) | |||
{ | |||
_isFirstLogSub = false; | |||
await WriteInitialLog().ConfigureAwait(false); | |||
} | |||
if (LoginState != LoginState.LoggedOut) | |||
await LogoutInternalAsync().ConfigureAwait(false); | |||
LoginState = LoginState.LoggingIn; | |||
try | |||
{ | |||
await ApiClient.LoginAsync(tokenType, token).ConfigureAwait(false); | |||
CurrentUser = RestSelfUser.Create(this, ApiClient.CurrentUser); | |||
await OnLoginAsync(tokenType, token).ConfigureAwait(false); | |||
LoginState = LoginState.LoggedIn; | |||
} | |||
catch (Exception) | |||
{ | |||
await LogoutInternalAsync().ConfigureAwait(false); | |||
throw; | |||
} | |||
await _loggedInEvent.InvokeAsync().ConfigureAwait(false); | |||
} | |||
protected virtual Task OnLoginAsync(TokenType tokenType, string token) => Task.CompletedTask; | |||
/// <inheritdoc /> | |||
public async Task LogoutAsync() | |||
protected override async Task OnLoginAsync(TokenType tokenType, string token) | |||
{ | |||
await _connectionLock.WaitAsync().ConfigureAwait(false); | |||
try | |||
{ | |||
await LogoutInternalAsync().ConfigureAwait(false); | |||
} | |||
finally { _connectionLock.Release(); } | |||
await ApiClient.LoginAsync(tokenType, token).ConfigureAwait(false); | |||
base.CurrentUser = RestSelfUser.Create(this, ApiClient.CurrentUser); | |||
} | |||
private async Task LogoutInternalAsync() | |||
{ | |||
if (LoginState == LoginState.LoggedOut) return; | |||
LoginState = LoginState.LoggingOut; | |||
await ApiClient.LogoutAsync().ConfigureAwait(false); | |||
await OnLogoutAsync().ConfigureAwait(false); | |||
CurrentUser = null; | |||
LoginState = LoginState.LoggedOut; | |||
await _loggedOutEvent.InvokeAsync().ConfigureAwait(false); | |||
} | |||
protected virtual Task OnLogoutAsync() => Task.CompletedTask; | |||
/// <inheritdoc /> | |||
public async Task<IApplication> GetApplicationInfoAsync() | |||
{ | |||
var model = await ApiClient.GetMyApplicationAsync().ConfigureAwait(false); | |||
return RestApplication.Create(this, model); | |||
} | |||
public Task<RestApplication> GetApplicationInfoAsync() | |||
=> ClientHelper.GetApplicationInfoAsync(this); | |||
/// <inheritdoc /> | |||
public virtual async Task<IChannel> GetChannelAsync(ulong id) | |||
{ | |||
var model = await ApiClient.GetChannelAsync(id).ConfigureAwait(false); | |||
if (model != null) | |||
{ | |||
switch (model.Type) | |||
{ | |||
case ChannelType.Text: | |||
return RestTextChannel.Create(this, model); | |||
case ChannelType.Voice: | |||
return RestVoiceChannel.Create(this, model); | |||
case ChannelType.DM: | |||
return RestDMChannel.Create(this, model); | |||
case ChannelType.Group: | |||
return RestGroupChannel.Create(this, model); | |||
default: | |||
throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); | |||
} | |||
} | |||
return null; | |||
} | |||
public Task<RestChannel> GetChannelAsync(ulong id) | |||
=> ClientHelper.GetChannelAsync(this, id); | |||
/// <inheritdoc /> | |||
public virtual async Task<IReadOnlyCollection<IPrivateChannel>> GetPrivateChannelsAsync() | |||
{ | |||
var models = await ApiClient.GetMyPrivateChannelsAsync().ConfigureAwait(false); | |||
return models.Select(x => RestDMChannel.Create(this, x)).ToImmutableArray(); | |||
} | |||
public Task<IReadOnlyCollection<IPrivateChannel>> GetPrivateChannelsAsync() | |||
=> ClientHelper.GetPrivateChannelsAsync(this); | |||
/// <inheritdoc /> | |||
public async Task<IReadOnlyCollection<RestConnection>> GetConnectionsAsync() | |||
{ | |||
var models = await ApiClient.GetMyConnectionsAsync().ConfigureAwait(false); | |||
return models.Select(x => RestConnection.Create(x)).ToImmutableArray(); | |||
} | |||
public Task<IReadOnlyCollection<RestConnection>> GetConnectionsAsync() | |||
=> ClientHelper.GetConnectionsAsync(this); | |||
/// <inheritdoc /> | |||
public virtual async Task<RestInvite> GetInviteAsync(string inviteId) | |||
{ | |||
var model = await ApiClient.GetInviteAsync(inviteId).ConfigureAwait(false); | |||
if (model != null) | |||
return RestInvite.Create(this, model); | |||
return null; | |||
} | |||
public Task<RestInvite> GetInviteAsync(string inviteId) | |||
=> ClientHelper.GetInviteAsync(this, inviteId); | |||
/// <inheritdoc /> | |||
public virtual async Task<RestGuild> GetGuildAsync(ulong id) | |||
{ | |||
var model = await ApiClient.GetGuildAsync(id).ConfigureAwait(false); | |||
if (model != null) | |||
return RestGuild.Create(this, model); | |||
return null; | |||
} | |||
public Task<RestGuild> GetGuildAsync(ulong id) | |||
=> ClientHelper.GetGuildAsync(this, id); | |||
/// <inheritdoc /> | |||
public virtual async Task<RestGuildEmbed?> GetGuildEmbedAsync(ulong id) | |||
{ | |||
var model = await ApiClient.GetGuildEmbedAsync(id).ConfigureAwait(false); | |||
if (model != null) | |||
return RestGuildEmbed.Create(model); | |||
return null; | |||
} | |||
public Task<RestGuildEmbed?> GetGuildEmbedAsync(ulong id) | |||
=> ClientHelper.GetGuildEmbedAsync(this, id); | |||
/// <inheritdoc /> | |||
public virtual async Task<IReadOnlyCollection<RestUserGuild>> GetGuildSummariesAsync() | |||
{ | |||
var models = await ApiClient.GetMyGuildsAsync().ConfigureAwait(false); | |||
return models.Select(x => RestUserGuild.Create(this, x)).ToImmutableArray(); | |||
} | |||
public Task<IReadOnlyCollection<RestUserGuild>> GetGuildSummariesAsync() | |||
=> ClientHelper.GetGuildSummariesAsync(this); | |||
/// <inheritdoc /> | |||
public virtual async Task<IReadOnlyCollection<RestGuild>> GetGuildsAsync() | |||
{ | |||
var summaryModels = await ApiClient.GetMyGuildsAsync().ConfigureAwait(false); | |||
var guilds = ImmutableArray.CreateBuilder<RestGuild>(summaryModels.Count); | |||
foreach (var summaryModel in summaryModels) | |||
{ | |||
var guildModel = await ApiClient.GetGuildAsync(summaryModel.Id).ConfigureAwait(false); | |||
if (guildModel != null) | |||
guilds.Add(RestGuild.Create(this, guildModel)); | |||
} | |||
return guilds.ToImmutable(); | |||
} | |||
public Task<IReadOnlyCollection<RestGuild>> GetGuildsAsync() | |||
=> ClientHelper.GetGuildsAsync(this); | |||
/// <inheritdoc /> | |||
public virtual async Task<RestGuild> CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null) | |||
{ | |||
var args = new CreateGuildParams(name, region.Id); | |||
var model = await ApiClient.CreateGuildAsync(args).ConfigureAwait(false); | |||
return RestGuild.Create(this, model); | |||
} | |||
public Task<RestGuild> CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null) | |||
=> ClientHelper.CreateGuildAsync(this, name, region, jpegIcon); | |||
/// <inheritdoc /> | |||
public virtual async Task<RestUser> GetUserAsync(ulong id) | |||
{ | |||
var model = await ApiClient.GetUserAsync(id).ConfigureAwait(false); | |||
if (model != null) | |||
return RestUser.Create(this, model); | |||
return null; | |||
} | |||
public Task<RestUser> GetUserAsync(ulong id) | |||
=> ClientHelper.GetUserAsync(this, id); | |||
/// <inheritdoc /> | |||
public virtual async Task<RestUser> GetUserAsync(string username, string discriminator) | |||
{ | |||
var model = await ApiClient.GetUserAsync(username, discriminator).ConfigureAwait(false); | |||
if (model != null) | |||
return RestUser.Create(this, model); | |||
return null; | |||
} | |||
public Task<RestUser> GetUserAsync(string username, string discriminator) | |||
=> ClientHelper.GetUserAsync(this, username, discriminator); | |||
/// <inheritdoc /> | |||
public virtual async Task<IReadOnlyCollection<RestUser>> QueryUsersAsync(string query, int limit) | |||
{ | |||
var models = await ApiClient.QueryUsersAsync(query, limit).ConfigureAwait(false); | |||
return models.Select(x => RestUser.Create(this, x)).ToImmutableArray(); | |||
} | |||
/// <inheritdoc /> | |||
public virtual async Task<IReadOnlyCollection<RestVoiceRegion>> GetVoiceRegionsAsync() | |||
{ | |||
var models = await ApiClient.GetVoiceRegionsAsync().ConfigureAwait(false); | |||
return models.Select(x => RestVoiceRegion.Create(this, x)).ToImmutableArray(); | |||
} | |||
/// <inheritdoc /> | |||
public virtual async Task<RestVoiceRegion> GetVoiceRegionAsync(string id) | |||
{ | |||
var models = await ApiClient.GetVoiceRegionsAsync().ConfigureAwait(false); | |||
return models.Select(x => RestVoiceRegion.Create(this, x)).Where(x => x.Id == id).FirstOrDefault(); | |||
} | |||
internal virtual void Dispose(bool disposing) | |||
{ | |||
if (!_isDisposed) | |||
{ | |||
ApiClient.Dispose(); | |||
_isDisposed = true; | |||
} | |||
} | |||
public Task<IReadOnlyCollection<RestVoiceRegion>> GetVoiceRegionsAsync() | |||
=> ClientHelper.GetVoiceRegionsAsync(this); | |||
/// <inheritdoc /> | |||
public void Dispose() => Dispose(true); | |||
private async Task WriteInitialLog() | |||
{ | |||
/*if (this is DiscordSocketClient) | |||
await _clientLogger.InfoAsync($"DiscordSocketClient v{DiscordConfig.Version} (API v{DiscordConfig.APIVersion}, {DiscordSocketConfig.GatewayEncoding})").ConfigureAwait(false); | |||
else if (this is DiscordRpcClient) | |||
await _clientLogger.InfoAsync($"DiscordRpcClient v{DiscordConfig.Version} (API v{DiscordConfig.APIVersion}, RPC API v{DiscordRpcConfig.RpcAPIVersion})").ConfigureAwait(false);*/ | |||
await _clientLogger.InfoAsync($"Discord.Net v{DiscordConfig.Version} (API v{DiscordConfig.APIVersion})").ConfigureAwait(false); | |||
await _clientLogger.VerboseAsync($"Runtime: {RuntimeInformation.FrameworkDescription.Trim()} ({ToArchString(RuntimeInformation.ProcessArchitecture)})").ConfigureAwait(false); | |||
await _clientLogger.VerboseAsync($"OS: {RuntimeInformation.OSDescription.Trim()} ({ToArchString(RuntimeInformation.OSArchitecture)})").ConfigureAwait(false); | |||
await _clientLogger.VerboseAsync($"Processors: {Environment.ProcessorCount}").ConfigureAwait(false); | |||
} | |||
private static string ToArchString(Architecture arch) | |||
{ | |||
switch (arch) | |||
{ | |||
case Architecture.X64: return "x64"; | |||
case Architecture.X86: return "x86"; | |||
default: return arch.ToString(); | |||
} | |||
} | |||
public Task<RestVoiceRegion> GetVoiceRegionAsync(string id) | |||
=> ClientHelper.GetVoiceRegionAsync(this, id); | |||
//IDiscordClient | |||
ConnectionState IDiscordClient.ConnectionState => ConnectionState.Disconnected; | |||
ISelfUser IDiscordClient.CurrentUser => CurrentUser; | |||
async Task<IApplication> IDiscordClient.GetApplicationInfoAsync() | |||
=> await GetApplicationInfoAsync().ConfigureAwait(false); | |||
async Task<IChannel> IDiscordClient.GetChannelAsync(ulong id) | |||
=> await GetChannelAsync(id); | |||
async Task<IReadOnlyCollection<IPrivateChannel>> IDiscordClient.GetPrivateChannelsAsync() | |||
=> await GetPrivateChannelsAsync(); | |||
async Task<IReadOnlyCollection<IConnection>> IDiscordClient.GetConnectionsAsync() | |||
=> await GetConnectionsAsync().ConfigureAwait(false); | |||
async Task<IInvite> IDiscordClient.GetInviteAsync(string inviteId) | |||
=> await GetInviteAsync(inviteId).ConfigureAwait(false); | |||
async Task<IGuild> IDiscordClient.GetGuildAsync(ulong id) | |||
=> await GetGuildAsync(id).ConfigureAwait(false); | |||
async Task<IReadOnlyCollection<IUserGuild>> IDiscordClient.GetGuildSummariesAsync() | |||
=> await GetGuildSummariesAsync().ConfigureAwait(false); | |||
async Task<IReadOnlyCollection<IGuild>> IDiscordClient.GetGuildsAsync() | |||
=> await GetGuildsAsync().ConfigureAwait(false); | |||
async Task<IGuild> IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon) | |||
=> await CreateGuildAsync(name, region, jpegIcon).ConfigureAwait(false); | |||
async Task<IUser> IDiscordClient.GetUserAsync(ulong id) | |||
=> await GetUserAsync(id).ConfigureAwait(false); | |||
async Task<IUser> IDiscordClient.GetUserAsync(string username, string discriminator) | |||
=> await GetUserAsync(username, discriminator).ConfigureAwait(false); | |||
async Task<IReadOnlyCollection<IUser>> IDiscordClient.QueryUsersAsync(string query, int limit) | |||
=> await QueryUsersAsync(query, limit).ConfigureAwait(false); | |||
async Task<IReadOnlyCollection<IVoiceRegion>> IDiscordClient.GetVoiceRegionsAsync() | |||
=> await GetVoiceRegionsAsync().ConfigureAwait(false); | |||
async Task<IVoiceRegion> IDiscordClient.GetVoiceRegionAsync(string id) | |||
=> await GetVoiceRegionAsync(id).ConfigureAwait(false); | |||
Task IDiscordClient.ConnectAsync() { throw new NotSupportedException(); } | |||
Task IDiscordClient.DisconnectAsync() { throw new NotSupportedException(); } | |||
} | |||
} |
@@ -12,33 +12,33 @@ namespace Discord.Rest | |||
internal static class ChannelHelper | |||
{ | |||
//General | |||
public static async Task<Model> GetAsync(IGuildChannel channel, DiscordRestClient client) | |||
public static async Task<Model> GetAsync(IGuildChannel channel, DiscordClient client) | |||
{ | |||
return await client.ApiClient.GetChannelAsync(channel.GuildId, channel.Id).ConfigureAwait(false); | |||
} | |||
public static async Task<Model> GetAsync(IPrivateChannel channel, DiscordRestClient client) | |||
public static async Task<Model> GetAsync(IPrivateChannel channel, DiscordClient client) | |||
{ | |||
return await client.ApiClient.GetChannelAsync(channel.Id).ConfigureAwait(false); | |||
} | |||
public static async Task DeleteAsync(IChannel channel, DiscordRestClient client) | |||
public static async Task DeleteAsync(IChannel channel, DiscordClient client) | |||
{ | |||
await client.ApiClient.DeleteChannelAsync(channel.Id).ConfigureAwait(false); | |||
} | |||
public static async Task ModifyAsync(IGuildChannel channel, DiscordRestClient client, | |||
public static async Task ModifyAsync(IGuildChannel channel, DiscordClient client, | |||
Action<ModifyGuildChannelParams> func) | |||
{ | |||
var args = new ModifyGuildChannelParams(); | |||
func(args); | |||
await client.ApiClient.ModifyGuildChannelAsync(channel.Id, args); | |||
} | |||
public static async Task ModifyAsync(ITextChannel channel, DiscordRestClient client, | |||
public static async Task ModifyAsync(ITextChannel channel, DiscordClient client, | |||
Action<ModifyTextChannelParams> func) | |||
{ | |||
var args = new ModifyTextChannelParams(); | |||
func(args); | |||
await client.ApiClient.ModifyGuildChannelAsync(channel.Id, args); | |||
} | |||
public static async Task ModifyAsync(IVoiceChannel channel, DiscordRestClient client, | |||
public static async Task ModifyAsync(IVoiceChannel channel, DiscordClient client, | |||
Action<ModifyVoiceChannelParams> func) | |||
{ | |||
var args = new ModifyVoiceChannelParams(); | |||
@@ -47,12 +47,12 @@ namespace Discord.Rest | |||
} | |||
//Invites | |||
public static async Task<IReadOnlyCollection<RestInviteMetadata>> GetInvitesAsync(IChannel channel, DiscordRestClient client) | |||
public static async Task<IReadOnlyCollection<RestInviteMetadata>> GetInvitesAsync(IChannel channel, DiscordClient client) | |||
{ | |||
var models = await client.ApiClient.GetChannelInvitesAsync(channel.Id); | |||
return models.Select(x => RestInviteMetadata.Create(client, x)).ToImmutableArray(); | |||
} | |||
public static async Task<RestInviteMetadata> CreateInviteAsync(IChannel channel, DiscordRestClient client, | |||
public static async Task<RestInviteMetadata> CreateInviteAsync(IChannel channel, DiscordClient client, | |||
int? maxAge, int? maxUses, bool isTemporary) | |||
{ | |||
var args = new CreateChannelInviteParams { IsTemporary = isTemporary }; | |||
@@ -65,13 +65,13 @@ namespace Discord.Rest | |||
} | |||
//Messages | |||
public static async Task<RestMessage> GetMessageAsync(IChannel channel, DiscordRestClient client, | |||
public static async Task<RestMessage> GetMessageAsync(IChannel channel, DiscordClient client, | |||
ulong id) | |||
{ | |||
var model = await client.ApiClient.GetChannelMessageAsync(channel.Id, id).ConfigureAwait(false); | |||
return RestMessage.Create(client, model); | |||
} | |||
public static PagedAsyncEnumerable<RestMessage> GetMessagesAsync(IChannel channel, DiscordRestClient client, | |||
public static IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(IChannel channel, DiscordClient client, | |||
ulong? fromMessageId = null, Direction dir = Direction.Before, int limit = DiscordConfig.MaxMessagesPerBatch) | |||
{ | |||
//TODO: Test this with Around direction | |||
@@ -102,13 +102,13 @@ namespace Discord.Rest | |||
count: (uint)limit | |||
); | |||
} | |||
public static async Task<IReadOnlyCollection<RestMessage>> GetPinnedMessagesAsync(IChannel channel, DiscordRestClient client) | |||
public static async Task<IReadOnlyCollection<RestMessage>> GetPinnedMessagesAsync(IChannel channel, DiscordClient client) | |||
{ | |||
var models = await client.ApiClient.GetPinsAsync(channel.Id).ConfigureAwait(false); | |||
return models.Select(x => RestMessage.Create(client, x)).ToImmutableArray(); | |||
} | |||
public static async Task<RestUserMessage> SendMessageAsync(IChannel channel, DiscordRestClient client, | |||
public static async Task<RestUserMessage> SendMessageAsync(IChannel channel, DiscordClient client, | |||
string text, bool isTTS) | |||
{ | |||
var args = new CreateMessageParams(text) { IsTTS = isTTS }; | |||
@@ -116,14 +116,14 @@ namespace Discord.Rest | |||
return RestUserMessage.Create(client, model); | |||
} | |||
public static Task<RestUserMessage> SendFileAsync(IChannel channel, DiscordRestClient client, | |||
public static Task<RestUserMessage> SendFileAsync(IChannel channel, DiscordClient client, | |||
string filePath, string text, bool isTTS) | |||
{ | |||
string filename = Path.GetFileName(filePath); | |||
using (var file = File.OpenRead(filePath)) | |||
return SendFileAsync(channel, client, file, filename, text, isTTS); | |||
} | |||
public static async Task<RestUserMessage> SendFileAsync(IChannel channel, DiscordRestClient client, | |||
public static async Task<RestUserMessage> SendFileAsync(IChannel channel, DiscordClient client, | |||
Stream stream, string filename, string text, bool isTTS) | |||
{ | |||
var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS }; | |||
@@ -131,7 +131,7 @@ namespace Discord.Rest | |||
return RestUserMessage.Create(client, model); | |||
} | |||
public static async Task DeleteMessagesAsync(IChannel channel, DiscordRestClient client, | |||
public static async Task DeleteMessagesAsync(IChannel channel, DiscordClient client, | |||
IEnumerable<IMessage> messages) | |||
{ | |||
var args = new DeleteMessagesParams(messages.Select(x => x.Id).ToArray()); | |||
@@ -139,31 +139,31 @@ namespace Discord.Rest | |||
} | |||
//Permission Overwrites | |||
public static async Task AddPermissionOverwriteAsync(IGuildChannel channel, DiscordRestClient client, | |||
public static async Task AddPermissionOverwriteAsync(IGuildChannel channel, DiscordClient client, | |||
IUser user, OverwritePermissions perms) | |||
{ | |||
var args = new ModifyChannelPermissionsParams("member", perms.AllowValue, perms.DenyValue); | |||
await client.ApiClient.ModifyChannelPermissionsAsync(channel.Id, user.Id, args).ConfigureAwait(false); | |||
} | |||
public static async Task AddPermissionOverwriteAsync(IGuildChannel channel, DiscordRestClient client, | |||
public static async Task AddPermissionOverwriteAsync(IGuildChannel channel, DiscordClient client, | |||
IRole role, OverwritePermissions perms) | |||
{ | |||
var args = new ModifyChannelPermissionsParams("role", perms.AllowValue, perms.DenyValue); | |||
await client.ApiClient.ModifyChannelPermissionsAsync(channel.Id, role.Id, args).ConfigureAwait(false); | |||
} | |||
public static async Task RemovePermissionOverwriteAsync(IGuildChannel channel, DiscordRestClient client, | |||
public static async Task RemovePermissionOverwriteAsync(IGuildChannel channel, DiscordClient client, | |||
IUser user) | |||
{ | |||
await client.ApiClient.DeleteChannelPermissionAsync(channel.Id, user.Id).ConfigureAwait(false); | |||
} | |||
public static async Task RemovePermissionOverwriteAsync(IGuildChannel channel, DiscordRestClient client, | |||
public static async Task RemovePermissionOverwriteAsync(IGuildChannel channel, DiscordClient client, | |||
IRole role) | |||
{ | |||
await client.ApiClient.DeleteChannelPermissionAsync(channel.Id, role.Id).ConfigureAwait(false); | |||
} | |||
//Users | |||
public static async Task<RestGuildUser> GetUserAsync(IGuildChannel channel, DiscordRestClient client, | |||
public static async Task<RestGuildUser> GetUserAsync(IGuildChannel channel, DiscordClient client, | |||
ulong id) | |||
{ | |||
var model = await client.ApiClient.GetGuildMemberAsync(channel.GuildId, id); | |||
@@ -175,7 +175,7 @@ namespace Discord.Rest | |||
return user; | |||
} | |||
public static IAsyncEnumerable<IReadOnlyCollection<RestGuildUser>> GetUsersAsync(IGuildChannel channel, DiscordRestClient client, | |||
public static IAsyncEnumerable<IReadOnlyCollection<RestGuildUser>> GetUsersAsync(IGuildChannel channel, DiscordClient client, | |||
ulong? froUserId = null, uint? limit = DiscordConfig.MaxUsersPerBatch) | |||
{ | |||
return new PagedAsyncEnumerable<RestGuildUser>( | |||
@@ -203,7 +203,7 @@ namespace Discord.Rest | |||
} | |||
//Typing | |||
public static IDisposable EnterTypingState(IChannel channel, DiscordRestClient client) | |||
public static IDisposable EnterTypingState(IChannel channel, DiscordClient client) | |||
{ | |||
throw new NotImplementedException(); //TODO: Impl | |||
} | |||
@@ -0,0 +1,48 @@ | |||
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}")] | |||
public abstract class RestChannel : RestEntity<ulong>, IChannel, IUpdateable | |||
{ | |||
internal RestChannel(DiscordClient discord, ulong id) | |||
: base(discord, id) | |||
{ | |||
} | |||
internal static RestChannel Create(DiscordClient discord, Model model) | |||
{ | |||
switch (model.Type) | |||
{ | |||
case ChannelType.Text: | |||
return RestTextChannel.Create(discord, model); | |||
case ChannelType.Voice: | |||
return RestVoiceChannel.Create(discord, model); | |||
case ChannelType.DM: | |||
return RestDMChannel.Create(discord, model); | |||
case ChannelType.Group: | |||
return RestGroupChannel.Create(discord, model); | |||
default: | |||
throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); | |||
} | |||
} | |||
internal abstract void Update(Model model); | |||
public abstract Task UpdateAsync(); | |||
//IChannel | |||
IReadOnlyCollection<IUser> IChannel.CachedUsers => ImmutableArray.Create<IUser>(); | |||
IUser IChannel.GetCachedUser(ulong id) | |||
=> null; | |||
Task<IUser> IChannel.GetUserAsync(ulong id) | |||
=> Task.FromResult<IUser>(null); | |||
IAsyncEnumerable<IReadOnlyCollection<IUser>> IChannel.GetUsersAsync() | |||
=> ImmutableArray.Create<IReadOnlyCollection<IUser>>().ToAsyncEnumerable(); | |||
} | |||
} |
@@ -10,28 +10,31 @@ using Model = Discord.API.Channel; | |||
namespace Discord.Rest | |||
{ | |||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
public class RestDMChannel : RestEntity<ulong>, IDMChannel, IUpdateable | |||
public class RestDMChannel : RestChannel, IDMChannel, IUpdateable | |||
{ | |||
public RestUser Recipient { get; } | |||
public RestUser CurrentUser { get; private set; } | |||
public RestUser Recipient { get; private set; } | |||
public IReadOnlyCollection<RestUser> Users => ImmutableArray.Create(Discord.CurrentUser, Recipient); | |||
public IReadOnlyCollection<RestUser> Users => ImmutableArray.Create(CurrentUser, Recipient); | |||
internal RestDMChannel(DiscordRestClient discord, ulong id) | |||
internal RestDMChannel(DiscordClient discord, ulong id, ulong recipientId) | |||
: base(discord, id) | |||
{ | |||
Recipient = new RestUser(Discord, recipientId); | |||
CurrentUser = new RestUser(Discord, discord.CurrentUser.Id); | |||
} | |||
internal static RestDMChannel Create(DiscordRestClient discord, Model model) | |||
internal new static RestDMChannel Create(DiscordClient discord, Model model) | |||
{ | |||
var entity = new RestDMChannel(discord, model.Id); | |||
var entity = new RestDMChannel(discord, model.Id, model.Recipients.Value[0].Id); | |||
entity.Update(model); | |||
return entity; | |||
} | |||
internal void Update(Model model) | |||
internal override void Update(Model model) | |||
{ | |||
Recipient.Update(model.Recipients.Value[0]); | |||
} | |||
public async Task UpdateAsync() | |||
public override async Task UpdateAsync() | |||
=> Update(await ChannelHelper.GetAsync(this, Discord)); | |||
public Task CloseAsync() | |||
=> ChannelHelper.DeleteAsync(this, Discord); | |||
@@ -41,7 +44,7 @@ namespace Discord.Rest | |||
if (id == Recipient.Id) | |||
return Recipient; | |||
else if (id == Discord.CurrentUser.Id) | |||
return Discord.CurrentUser; | |||
return CurrentUser; | |||
else | |||
return null; | |||
} | |||
@@ -10,7 +10,7 @@ using Model = Discord.API.Channel; | |||
namespace Discord.Rest | |||
{ | |||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
public class RestGroupChannel : RestEntity<ulong>, IGroupChannel, IUpdateable | |||
public class RestGroupChannel : RestChannel, IGroupChannel, IUpdateable | |||
{ | |||
private string _iconId; | |||
private ImmutableDictionary<ulong, RestGroupUser> _users; | |||
@@ -21,17 +21,17 @@ namespace Discord.Rest | |||
public IReadOnlyCollection<RestGroupUser> Recipients | |||
=> _users.Select(x => x.Value).Where(x => x.Id != Discord.CurrentUser.Id).ToReadOnlyCollection(() => _users.Count - 1); | |||
internal RestGroupChannel(DiscordRestClient discord, ulong id) | |||
internal RestGroupChannel(DiscordClient discord, ulong id) | |||
: base(discord, id) | |||
{ | |||
} | |||
internal static RestGroupChannel Create(DiscordRestClient discord, Model model) | |||
internal new static RestGroupChannel Create(DiscordClient discord, Model model) | |||
{ | |||
var entity = new RestGroupChannel(discord, model.Id); | |||
entity.Update(model); | |||
return entity; | |||
} | |||
internal void Update(Model model) | |||
internal override void Update(Model model) | |||
{ | |||
if (model.Name.IsSpecified) | |||
Name = model.Name.Value; | |||
@@ -49,7 +49,7 @@ namespace Discord.Rest | |||
_users = users.ToImmutable(); | |||
} | |||
public async Task UpdateAsync() | |||
public override async Task UpdateAsync() | |||
=> Update(await ChannelHelper.GetAsync(this, Discord)); | |||
public Task LeaveAsync() | |||
=> ChannelHelper.DeleteAsync(this, Discord); | |||
@@ -10,7 +10,7 @@ using Model = Discord.API.Channel; | |||
namespace Discord.Rest | |||
{ | |||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
public abstract class RestGuildChannel : RestEntity<ulong>, IGuildChannel, IUpdateable | |||
public abstract class RestGuildChannel : RestChannel, IGuildChannel, IUpdateable | |||
{ | |||
private ImmutableArray<Overwrite> _overwrites; | |||
@@ -21,12 +21,12 @@ namespace Discord.Rest | |||
public string Name { get; private set; } | |||
public int Position { get; private set; } | |||
internal RestGuildChannel(DiscordRestClient discord, ulong id, ulong guildId) | |||
internal RestGuildChannel(DiscordClient discord, ulong id, ulong guildId) | |||
: base(discord, id) | |||
{ | |||
GuildId = guildId; | |||
} | |||
internal static RestGuildChannel Create(DiscordRestClient discord, Model model) | |||
internal new static RestGuildChannel Create(DiscordClient discord, Model model) | |||
{ | |||
switch (model.Type) | |||
{ | |||
@@ -38,7 +38,7 @@ namespace Discord.Rest | |||
throw new InvalidOperationException("Unknown guild channel type"); | |||
} | |||
} | |||
internal virtual void Update(Model model) | |||
internal override void Update(Model model) | |||
{ | |||
Name = model.Name.Value; | |||
Position = model.Position.Value; | |||
@@ -50,7 +50,7 @@ namespace Discord.Rest | |||
_overwrites = newOverwrites.ToImmutable(); | |||
} | |||
public async Task UpdateAsync() | |||
public override async Task UpdateAsync() | |||
=> Update(await ChannelHelper.GetAsync(this, Discord)); | |||
public Task ModifyAsync(Action<ModifyGuildChannelParams> func) | |||
=> ChannelHelper.ModifyAsync(this, Discord, func); | |||
@@ -16,11 +16,11 @@ namespace Discord.Rest | |||
public string Mention => MentionUtils.MentionChannel(Id); | |||
internal RestTextChannel(DiscordRestClient discord, ulong id, ulong guildId) | |||
internal RestTextChannel(DiscordClient discord, ulong id, ulong guildId) | |||
: base(discord, id, guildId) | |||
{ | |||
} | |||
internal new static RestTextChannel Create(DiscordRestClient discord, Model model) | |||
internal new static RestTextChannel Create(DiscordClient discord, Model model) | |||
{ | |||
var entity = new RestTextChannel(discord, model.Id, model.GuildId.Value); | |||
entity.Update(model); | |||
@@ -16,11 +16,11 @@ namespace Discord.Rest | |||
public int Bitrate { get; private set; } | |||
public int UserLimit { get; private set; } | |||
internal RestVoiceChannel(DiscordRestClient discord, ulong id, ulong guildId) | |||
internal RestVoiceChannel(DiscordClient discord, ulong id, ulong guildId) | |||
: base(discord, id, guildId) | |||
{ | |||
} | |||
internal new static RestVoiceChannel Create(DiscordRestClient discord, Model model) | |||
internal new static RestVoiceChannel Create(DiscordClient discord, Model model) | |||
{ | |||
var entity = new RestVoiceChannel(discord, model.Id, model.GuildId.Value); | |||
entity.Update(model); | |||
@@ -13,7 +13,7 @@ namespace Discord.Rest | |||
internal static class GuildHelper | |||
{ | |||
//General | |||
public static async Task<Model> ModifyAsync(IGuild guild, DiscordRestClient client, | |||
public static async Task<Model> ModifyAsync(IGuild guild, DiscordClient client, | |||
Action<ModifyGuildParams> func) | |||
{ | |||
if (func == null) throw new NullReferenceException(nameof(func)); | |||
@@ -28,7 +28,7 @@ namespace Discord.Rest | |||
return await client.ApiClient.ModifyGuildAsync(guild.Id, args).ConfigureAwait(false); | |||
} | |||
public static async Task<EmbedModel> ModifyEmbedAsync(IGuild guild, DiscordRestClient client, | |||
public static async Task<EmbedModel> ModifyEmbedAsync(IGuild guild, DiscordClient client, | |||
Action<ModifyGuildEmbedParams> func) | |||
{ | |||
if (func == null) throw new NullReferenceException(nameof(func)); | |||
@@ -37,46 +37,46 @@ namespace Discord.Rest | |||
func(args); | |||
return await client.ApiClient.ModifyGuildEmbedAsync(guild.Id, args).ConfigureAwait(false); | |||
} | |||
public static async Task ModifyChannelsAsync(IGuild guild, DiscordRestClient client, | |||
public static async Task ModifyChannelsAsync(IGuild guild, DiscordClient client, | |||
IEnumerable<ModifyGuildChannelsParams> args) | |||
{ | |||
await client.ApiClient.ModifyGuildChannelsAsync(guild.Id, args).ConfigureAwait(false); | |||
} | |||
public static async Task<IReadOnlyCollection<RoleModel>> ModifyRolesAsync(IGuild guild, DiscordRestClient client, | |||
public static async Task<IReadOnlyCollection<RoleModel>> ModifyRolesAsync(IGuild guild, DiscordClient client, | |||
IEnumerable<ModifyGuildRolesParams> args) | |||
{ | |||
return await client.ApiClient.ModifyGuildRolesAsync(guild.Id, args).ConfigureAwait(false); | |||
} | |||
public static async Task LeaveAsync(IGuild guild, DiscordRestClient client) | |||
public static async Task LeaveAsync(IGuild guild, DiscordClient client) | |||
{ | |||
await client.ApiClient.LeaveGuildAsync(guild.Id).ConfigureAwait(false); | |||
} | |||
public static async Task DeleteAsync(IGuild guild, DiscordRestClient client) | |||
public static async Task DeleteAsync(IGuild guild, DiscordClient client) | |||
{ | |||
await client.ApiClient.DeleteGuildAsync(guild.Id).ConfigureAwait(false); | |||
} | |||
//Bans | |||
public static async Task<IReadOnlyCollection<RestBan>> GetBansAsync(IGuild guild, DiscordRestClient client) | |||
public static async Task<IReadOnlyCollection<RestBan>> GetBansAsync(IGuild guild, DiscordClient client) | |||
{ | |||
var models = await client.ApiClient.GetGuildBansAsync(guild.Id); | |||
return models.Select(x => RestBan.Create(client, x)).ToImmutableArray(); | |||
} | |||
public static async Task AddBanAsync(IGuild guild, DiscordRestClient client, | |||
public static async Task AddBanAsync(IGuild guild, DiscordClient client, | |||
ulong userId, int pruneDays) | |||
{ | |||
var args = new CreateGuildBanParams { DeleteMessageDays = pruneDays }; | |||
await client.ApiClient.CreateGuildBanAsync(guild.Id, userId, args); | |||
} | |||
public static async Task RemoveBanAsync(IGuild guild, DiscordRestClient client, | |||
public static async Task RemoveBanAsync(IGuild guild, DiscordClient client, | |||
ulong userId) | |||
{ | |||
await client.ApiClient.RemoveGuildBanAsync(guild.Id, userId); | |||
} | |||
//Channels | |||
public static async Task<RestGuildChannel> GetChannelAsync(IGuild guild, DiscordRestClient client, | |||
public static async Task<RestGuildChannel> GetChannelAsync(IGuild guild, DiscordClient client, | |||
ulong id) | |||
{ | |||
var model = await client.ApiClient.GetChannelAsync(guild.Id, id).ConfigureAwait(false); | |||
@@ -84,12 +84,12 @@ namespace Discord.Rest | |||
return RestGuildChannel.Create(client, model); | |||
return null; | |||
} | |||
public static async Task<IReadOnlyCollection<RestGuildChannel>> GetChannelsAsync(IGuild guild, DiscordRestClient client) | |||
public static async Task<IReadOnlyCollection<RestGuildChannel>> GetChannelsAsync(IGuild guild, DiscordClient client) | |||
{ | |||
var models = await client.ApiClient.GetGuildChannelsAsync(guild.Id).ConfigureAwait(false); | |||
return models.Select(x => RestGuildChannel.Create(client, x)).ToImmutableArray(); | |||
} | |||
public static async Task<RestTextChannel> CreateTextChannelAsync(IGuild guild, DiscordRestClient client, | |||
public static async Task<RestTextChannel> CreateTextChannelAsync(IGuild guild, DiscordClient client, | |||
string name) | |||
{ | |||
if (name == null) throw new ArgumentNullException(nameof(name)); | |||
@@ -98,7 +98,7 @@ namespace Discord.Rest | |||
var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args).ConfigureAwait(false); | |||
return RestTextChannel.Create(client, model); | |||
} | |||
public static async Task<RestVoiceChannel> CreateVoiceChannelAsync(IGuild guild, DiscordRestClient client, | |||
public static async Task<RestVoiceChannel> CreateVoiceChannelAsync(IGuild guild, DiscordClient client, | |||
string name) | |||
{ | |||
if (name == null) throw new ArgumentNullException(nameof(name)); | |||
@@ -109,12 +109,12 @@ namespace Discord.Rest | |||
} | |||
//Integrations | |||
public static async Task<IReadOnlyCollection<RestGuildIntegration>> GetIntegrationsAsync(IGuild guild, DiscordRestClient client) | |||
public static async Task<IReadOnlyCollection<RestGuildIntegration>> GetIntegrationsAsync(IGuild guild, DiscordClient client) | |||
{ | |||
var models = await client.ApiClient.GetGuildIntegrationsAsync(guild.Id).ConfigureAwait(false); | |||
return models.Select(x => RestGuildIntegration.Create(client, x)).ToImmutableArray(); | |||
} | |||
public static async Task<RestGuildIntegration> CreateIntegrationAsync(IGuild guild, DiscordRestClient client, | |||
public static async Task<RestGuildIntegration> CreateIntegrationAsync(IGuild guild, DiscordClient client, | |||
ulong id, string type) | |||
{ | |||
var args = new CreateGuildIntegrationParams(id, type); | |||
@@ -123,14 +123,14 @@ namespace Discord.Rest | |||
} | |||
//Invites | |||
public static async Task<IReadOnlyCollection<RestInviteMetadata>> GetInvitesAsync(IGuild guild, DiscordRestClient client) | |||
public static async Task<IReadOnlyCollection<RestInviteMetadata>> GetInvitesAsync(IGuild guild, DiscordClient client) | |||
{ | |||
var models = await client.ApiClient.GetGuildInvitesAsync(guild.Id).ConfigureAwait(false); | |||
return models.Select(x => RestInviteMetadata.Create(client, x)).ToImmutableArray(); | |||
} | |||
//Roles | |||
public static async Task<RestRole> CreateRoleAsync(IGuild guild, DiscordRestClient client, | |||
public static async Task<RestRole> CreateRoleAsync(IGuild guild, DiscordClient client, | |||
string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false) | |||
{ | |||
if (name == null) throw new ArgumentNullException(nameof(name)); | |||
@@ -150,7 +150,7 @@ namespace Discord.Rest | |||
} | |||
//Users | |||
public static async Task<RestGuildUser> GetUserAsync(IGuild guild, DiscordRestClient client, | |||
public static async Task<RestGuildUser> GetUserAsync(IGuild guild, DiscordClient client, | |||
ulong id) | |||
{ | |||
var model = await client.ApiClient.GetGuildMemberAsync(guild.Id, id).ConfigureAwait(false); | |||
@@ -158,17 +158,17 @@ namespace Discord.Rest | |||
return RestGuildUser.Create(client, model); | |||
return null; | |||
} | |||
public static async Task<RestGuildUser> GetCurrentUserAsync(IGuild guild, DiscordRestClient client) | |||
public static async Task<RestGuildUser> GetCurrentUserAsync(IGuild guild, DiscordClient client) | |||
{ | |||
return await GetUserAsync(guild, client, client.CurrentUser.Id).ConfigureAwait(false); | |||
} | |||
public static async Task<IReadOnlyCollection<RestGuildUser>> GetUsersAsync(IGuild guild, DiscordRestClient client) | |||
public static async Task<IReadOnlyCollection<RestGuildUser>> GetUsersAsync(IGuild guild, DiscordClient client) | |||
{ | |||
var args = new GetGuildMembersParams(); | |||
var models = await client.ApiClient.GetGuildMembersAsync(guild.Id, args).ConfigureAwait(false); | |||
return models.Select(x => RestGuildUser.Create(client, x)).ToImmutableArray(); | |||
} | |||
public static async Task<int> PruneUsersAsync(IGuild guild, DiscordRestClient client, | |||
public static async Task<int> PruneUsersAsync(IGuild guild, DiscordClient client, | |||
int days = 30, bool simulate = false) | |||
{ | |||
var args = new GuildPruneParams(days); | |||
@@ -14,7 +14,7 @@ namespace Discord.Rest | |||
User = user; | |||
Reason = reason; | |||
} | |||
internal static RestBan Create(DiscordRestClient client, Model model) | |||
internal static RestBan Create(DiscordClient client, Model model) | |||
{ | |||
return new RestBan(RestUser.Create(client, model.User), model.Reason); | |||
} | |||
@@ -39,11 +39,11 @@ namespace Discord.Rest | |||
public IReadOnlyCollection<Emoji> Emojis => _emojis; | |||
public IReadOnlyCollection<string> Features => _features; | |||
internal RestGuild(DiscordRestClient client, ulong id) | |||
internal RestGuild(DiscordClient client, ulong id) | |||
: base(client, id) | |||
{ | |||
} | |||
internal static RestGuild Create(DiscordRestClient discord, Model model) | |||
internal static RestGuild Create(DiscordClient discord, Model model) | |||
{ | |||
var entity = new RestGuild(discord, model.Id); | |||
entity.Update(model); | |||
@@ -25,11 +25,11 @@ namespace Discord.Rest | |||
public DateTimeOffset SyncedAt => DateTimeUtils.FromTicks(_syncedAtTicks); | |||
internal RestGuildIntegration(DiscordRestClient discord, ulong id) | |||
internal RestGuildIntegration(DiscordClient discord, ulong id) | |||
: base(discord, id) | |||
{ | |||
} | |||
internal static RestGuildIntegration Create(DiscordRestClient discord, Model model) | |||
internal static RestGuildIntegration Create(DiscordClient discord, Model model) | |||
{ | |||
var entity = new RestGuildIntegration(discord, model.Id); | |||
entity.Update(model); | |||
@@ -15,11 +15,11 @@ namespace Discord.Rest | |||
public string IconUrl => API.CDN.GetGuildIconUrl(Id, _iconId); | |||
internal RestUserGuild(DiscordRestClient discord, ulong id) | |||
internal RestUserGuild(DiscordClient discord, ulong id) | |||
: base(discord, id) | |||
{ | |||
} | |||
internal static RestUserGuild Create(DiscordRestClient discord, Model model) | |||
internal static RestUserGuild Create(DiscordClient discord, Model model) | |||
{ | |||
var entity = new RestUserGuild(discord, model.Id); | |||
entity.Update(model); | |||
@@ -13,11 +13,11 @@ namespace Discord | |||
public string SampleHostname { get; private set; } | |||
public int SamplePort { get; private set; } | |||
internal RestVoiceRegion(DiscordRestClient client, string id) | |||
internal RestVoiceRegion(DiscordClient client, string id) | |||
: base(client, id) | |||
{ | |||
} | |||
internal static RestVoiceRegion Create(DiscordRestClient client, Model model) | |||
internal static RestVoiceRegion Create(DiscordClient client, Model model) | |||
{ | |||
var entity = new RestVoiceRegion(client, model.Id); | |||
entity.Update(model); | |||
@@ -5,15 +5,15 @@ namespace Discord.Rest | |||
{ | |||
internal static class InviteHelper | |||
{ | |||
public static async Task<Model> GetAsync(IInvite invite, DiscordRestClient client) | |||
public static async Task<Model> GetAsync(IInvite invite, DiscordClient client) | |||
{ | |||
return await client.ApiClient.GetInviteAsync(invite.Code).ConfigureAwait(false); | |||
} | |||
public static async Task AcceptAsync(IInvite invite, DiscordRestClient client) | |||
public static async Task AcceptAsync(IInvite invite, DiscordClient client) | |||
{ | |||
await client.ApiClient.AcceptInviteAsync(invite.Code).ConfigureAwait(false); | |||
} | |||
public static async Task DeleteAsync(IInvite invite, DiscordRestClient client) | |||
public static async Task DeleteAsync(IInvite invite, DiscordClient client) | |||
{ | |||
await client.ApiClient.DeleteInviteAsync(invite.Code).ConfigureAwait(false); | |||
} | |||
@@ -16,11 +16,11 @@ namespace Discord.Rest | |||
public string Code => Id; | |||
public string Url => $"{DiscordConfig.InviteUrl}/{Code}"; | |||
internal RestInvite(DiscordRestClient discord, string id) | |||
internal RestInvite(DiscordClient discord, string id) | |||
: base(discord, id) | |||
{ | |||
} | |||
internal static RestInvite Create(DiscordRestClient discord, Model model) | |||
internal static RestInvite Create(DiscordClient discord, Model model) | |||
{ | |||
var entity = new RestInvite(discord, model.Code); | |||
entity.Update(model); | |||
@@ -18,11 +18,11 @@ namespace Discord.Rest | |||
public DateTimeOffset CreatedAt => DateTimeUtils.FromTicks(_createdAtTicks); | |||
internal RestInviteMetadata(DiscordRestClient discord, string id) | |||
internal RestInviteMetadata(DiscordClient discord, string id) | |||
: base(discord, id) | |||
{ | |||
} | |||
internal static RestInviteMetadata Create(DiscordRestClient discord, Model model) | |||
internal static RestInviteMetadata Create(DiscordClient discord, Model model) | |||
{ | |||
var entity = new RestInviteMetadata(discord, model.Code); | |||
entity.Update(model); | |||
@@ -6,26 +6,26 @@ namespace Discord.Rest | |||
{ | |||
internal static class MessageHelper | |||
{ | |||
public static async Task GetAsync(IMessage msg, DiscordRestClient client) | |||
public static async Task GetAsync(IMessage msg, DiscordClient client) | |||
{ | |||
await client.ApiClient.GetChannelMessageAsync(msg.ChannelId, msg.Id); | |||
} | |||
public static async Task ModifyAsync(IMessage msg, DiscordRestClient client, Action<ModifyMessageParams> func) | |||
public static async Task ModifyAsync(IMessage msg, DiscordClient client, Action<ModifyMessageParams> func) | |||
{ | |||
var args = new ModifyMessageParams(); | |||
func(args); | |||
await client.ApiClient.ModifyMessageAsync(msg.ChannelId, msg.Id, args); | |||
} | |||
public static async Task DeleteAsync(IMessage msg, DiscordRestClient client) | |||
public static async Task DeleteAsync(IMessage msg, DiscordClient client) | |||
{ | |||
await client.ApiClient.DeleteMessageAsync(msg.ChannelId, msg.Id); | |||
} | |||
public static async Task PinAsync(IMessage msg, DiscordRestClient client) | |||
public static async Task PinAsync(IMessage msg, DiscordClient client) | |||
{ | |||
await client.ApiClient.AddPinAsync(msg.ChannelId, msg.Id); | |||
} | |||
public static async Task UnpinAsync(IMessage msg, DiscordRestClient client) | |||
public static async Task UnpinAsync(IMessage msg, DiscordClient client) | |||
{ | |||
await client.ApiClient.RemovePinAsync(msg.ChannelId, msg.Id); | |||
} | |||
@@ -2,6 +2,7 @@ | |||
namespace Discord | |||
{ | |||
//TODO: Rename to Attachment? | |||
public class RestAttachment : IAttachment | |||
{ | |||
public ulong Id { get; } | |||
@@ -29,12 +29,12 @@ namespace Discord.Rest | |||
public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); | |||
internal RestMessage(DiscordRestClient discord, ulong id, ulong channelId) | |||
internal RestMessage(DiscordClient discord, ulong id, ulong channelId) | |||
: base(discord, id) | |||
{ | |||
ChannelId = channelId; | |||
} | |||
internal static RestMessage Create(DiscordRestClient discord, Model model) | |||
internal static RestMessage Create(DiscordClient discord, Model model) | |||
{ | |||
if (model.Type == MessageType.Default) | |||
return RestUserMessage.Create(discord, model); | |||
@@ -8,11 +8,11 @@ namespace Discord.Rest | |||
{ | |||
public MessageType Type { get; private set; } | |||
internal RestSystemMessage(DiscordRestClient discord, ulong id, ulong channelId) | |||
internal RestSystemMessage(DiscordClient discord, ulong id, ulong channelId) | |||
: base(discord, id, channelId) | |||
{ | |||
} | |||
internal new static RestSystemMessage Create(DiscordRestClient discord, Model model) | |||
internal new static RestSystemMessage Create(DiscordClient discord, Model model) | |||
{ | |||
var entity = new RestSystemMessage(discord, model.Id, model.ChannelId); | |||
entity.Update(model); | |||
@@ -30,11 +30,11 @@ namespace Discord.Rest | |||
public override IReadOnlyCollection<IRole> MentionedRoles => _mentionedRoles; | |||
public override IReadOnlyCollection<IUser> MentionedUsers => _mentionedUsers; | |||
internal RestUserMessage(DiscordRestClient discord, ulong id, ulong channelId) | |||
internal RestUserMessage(DiscordClient discord, ulong id, ulong channelId) | |||
: base(discord, id, channelId) | |||
{ | |||
} | |||
internal new static RestUserMessage Create(DiscordRestClient discord, Model model) | |||
internal new static RestUserMessage Create(DiscordClient discord, Model model) | |||
{ | |||
var entity = new RestUserMessage(discord, model.Id, model.ChannelId); | |||
entity.Update(model); | |||
@@ -17,11 +17,11 @@ namespace Discord.Rest | |||
public string IconUrl => API.CDN.GetApplicationIconUrl(Id, _iconId); | |||
internal RestApplication(DiscordRestClient discord, ulong id) | |||
internal RestApplication(DiscordClient discord, ulong id) | |||
: base(discord, id) | |||
{ | |||
} | |||
internal static RestApplication Create(DiscordRestClient discord, Model model) | |||
internal static RestApplication Create(DiscordClient discord, Model model) | |||
{ | |||
var entity = new RestApplication(discord, model.Id); | |||
entity.Update(model); | |||
@@ -5,10 +5,10 @@ namespace Discord.Rest | |||
public abstract class RestEntity<T> : IEntity<T> | |||
where T : IEquatable<T> | |||
{ | |||
public DiscordClient Discord { get; } | |||
public T Id { get; } | |||
public DiscordRestClient Discord { get; } | |||
public RestEntity(DiscordRestClient discord, T id) | |||
internal RestEntity(DiscordClient discord, T id) | |||
{ | |||
Discord = discord; | |||
Id = id; | |||
@@ -21,11 +21,11 @@ namespace Discord.Rest | |||
public bool IsEveryone => Id == Guild.Id; | |||
public string Mention => MentionUtils.MentionRole(Id); | |||
internal RestRole(DiscordRestClient discord, ulong id) | |||
internal RestRole(DiscordClient discord, ulong id) | |||
: base(discord, id) | |||
{ | |||
} | |||
internal static RestRole Create(DiscordRestClient discord, Model model) | |||
internal static RestRole Create(DiscordClient discord, Model model) | |||
{ | |||
var entity = new RestRole(discord, model.Id); | |||
entity.Update(model); | |||
@@ -7,11 +7,11 @@ namespace Discord.Rest | |||
internal static class RoleHelper | |||
{ | |||
//General | |||
public static async Task DeleteAsync(IRole role, DiscordRestClient client) | |||
public static async Task DeleteAsync(IRole role, DiscordClient client) | |||
{ | |||
await client.ApiClient.DeleteGuildRoleAsync(role.Guild.Id, role.Id).ConfigureAwait(false); | |||
} | |||
public static async Task ModifyAsync(IRole role, DiscordRestClient client, | |||
public static async Task ModifyAsync(IRole role, DiscordClient client, | |||
Action<ModifyGuildRoleParams> func) | |||
{ | |||
var args = new ModifyGuildRoleParams(); | |||
@@ -6,11 +6,11 @@ namespace Discord.Rest | |||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
public class RestGroupUser : RestUser, IGroupUser | |||
{ | |||
internal RestGroupUser(DiscordRestClient discord, ulong id) | |||
internal RestGroupUser(DiscordClient discord, ulong id) | |||
: base(discord, id) | |||
{ | |||
} | |||
internal new static RestGroupUser Create(DiscordRestClient discord, Model model) | |||
internal new static RestGroupUser Create(DiscordClient discord, Model model) | |||
{ | |||
var entity = new RestGroupUser(discord, model.Id); | |||
entity.Update(model); | |||
@@ -21,11 +21,11 @@ namespace Discord.Rest | |||
public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); | |||
internal RestGuildUser(DiscordRestClient discord, ulong id) | |||
internal RestGuildUser(DiscordClient discord, ulong id) | |||
: base(discord, id) | |||
{ | |||
} | |||
internal static RestGuildUser Create(DiscordRestClient discord, Model model) | |||
internal static RestGuildUser Create(DiscordClient discord, Model model) | |||
{ | |||
var entity = new RestGuildUser(discord, model.User.Id); | |||
entity.Update(model); | |||
@@ -13,11 +13,11 @@ namespace Discord.Rest | |||
public bool IsVerified { get; private set; } | |||
public bool IsMfaEnabled { get; private set; } | |||
internal RestSelfUser(DiscordRestClient discord, ulong id) | |||
internal RestSelfUser(DiscordClient discord, ulong id) | |||
: base(discord, id) | |||
{ | |||
} | |||
internal new static RestSelfUser Create(DiscordRestClient discord, Model model) | |||
internal new static RestSelfUser Create(DiscordClient discord, Model model) | |||
{ | |||
var entity = new RestSelfUser(discord, model.Id); | |||
entity.Update(model); | |||
@@ -18,11 +18,11 @@ namespace Discord.Rest | |||
public virtual Game? Game => null; | |||
public virtual UserStatus Status => UserStatus.Unknown; | |||
internal RestUser(DiscordRestClient discord, ulong id) | |||
internal RestUser(DiscordClient discord, ulong id) | |||
: base(discord, id) | |||
{ | |||
} | |||
internal static RestUser Create(DiscordRestClient discord, Model model) | |||
internal static RestUser Create(DiscordClient discord, Model model) | |||
{ | |||
var entity = new RestUser(discord, model.Id); | |||
entity.Update(model); | |||
@@ -8,22 +8,22 @@ namespace Discord.Rest | |||
{ | |||
internal static class UserHelper | |||
{ | |||
public static async Task<Model> GetAsync(IUser user, DiscordRestClient client) | |||
public static async Task<Model> GetAsync(IUser user, DiscordClient client) | |||
{ | |||
return await client.ApiClient.GetUserAsync(user.Id); | |||
} | |||
public static async Task<Model> GetAsync(ISelfUser user, DiscordRestClient client) | |||
public static async Task<Model> GetAsync(ISelfUser user, DiscordClient client) | |||
{ | |||
var model = await client.ApiClient.GetMyUserAsync(); | |||
if (model.Id != user.Id) | |||
throw new InvalidOperationException("Unable to update this object using a different token."); | |||
return model; | |||
} | |||
public static async Task<MemberModel> GetAsync(IGuildUser user, DiscordRestClient client) | |||
public static async Task<MemberModel> GetAsync(IGuildUser user, DiscordClient client) | |||
{ | |||
return await client.ApiClient.GetGuildMemberAsync(user.GuildId, user.Id); | |||
} | |||
public static async Task ModifyAsync(ISelfUser user, DiscordRestClient client, Action<ModifyCurrentUserParams> func) | |||
public static async Task ModifyAsync(ISelfUser user, DiscordClient client, Action<ModifyCurrentUserParams> func) | |||
{ | |||
if (user.Id != client.CurrentUser.Id) | |||
throw new InvalidOperationException("Unable to modify this object using a different token."); | |||
@@ -32,19 +32,19 @@ namespace Discord.Rest | |||
func(args); | |||
await client.ApiClient.ModifySelfAsync(args); | |||
} | |||
public static async Task ModifyAsync(IGuildUser user, DiscordRestClient client, Action<ModifyGuildMemberParams> func) | |||
public static async Task ModifyAsync(IGuildUser user, DiscordClient client, Action<ModifyGuildMemberParams> func) | |||
{ | |||
var args = new ModifyGuildMemberParams(); | |||
func(args); | |||
await client.ApiClient.ModifyGuildMemberAsync(user.GuildId, user.Id, args); | |||
} | |||
public static async Task KickAsync(IGuildUser user, DiscordRestClient client) | |||
public static async Task KickAsync(IGuildUser user, DiscordClient client) | |||
{ | |||
await client.ApiClient.RemoveGuildMemberAsync(user.GuildId, user.Id); | |||
} | |||
public static async Task<IDMChannel> CreateDMChannelAsync(IUser user, DiscordRestClient client) | |||
public static async Task<IDMChannel> CreateDMChannelAsync(IUser user, DiscordClient client) | |||
{ | |||
var args = new CreateDMChannelParams(user.Id); | |||
return RestDMChannel.Create(client, await client.ApiClient.CreateDMChannelAsync(args)); | |||
@@ -1,12 +1,6 @@ | |||
{ | |||
"version": "1.0.0-beta2-*", | |||
"buildOptions": { | |||
"compile": { | |||
"include": [ "../Discord.Net.Utils/**.cs" ] | |||
} | |||
}, | |||
"configurations": { | |||
"Release": { | |||
"buildOptions": { | |||
@@ -3,6 +3,7 @@ using Discord.API.Rpc; | |||
using Discord.Net.Queue; | |||
using Discord.Net.Rest; | |||
using Discord.Net.WebSockets; | |||
using Discord.Rpc; | |||
using Newtonsoft.Json; | |||
using Newtonsoft.Json.Linq; | |||
using System; | |||
@@ -66,8 +67,8 @@ namespace Discord.API | |||
public ConnectionState ConnectionState { get; private set; } | |||
public DiscordRpcApiClient(string clientId, string origin, RestClientProvider restClientProvider, WebSocketProvider webSocketProvider, JsonSerializer serializer = null, RequestQueue requestQueue = null) | |||
: base(restClientProvider, serializer, requestQueue) | |||
public DiscordRpcApiClient(string clientId, string userAgent, string origin, RestClientProvider restClientProvider, WebSocketProvider webSocketProvider, JsonSerializer serializer = null, RequestQueue requestQueue = null) | |||
: base(restClientProvider, userAgent, serializer, requestQueue) | |||
{ | |||
_connectionLock = new SemaphoreSlim(1, 1); | |||
_clientId = clientId; | |||
@@ -0,0 +1,3 @@ | |||
using System.Runtime.CompilerServices; | |||
[assembly: InternalsVisibleTo("Discord.Net.Test")] |
@@ -11,9 +11,9 @@ using System.Threading.Tasks; | |||
namespace Discord.Rpc | |||
{ | |||
public partial class DiscordRpcClient : DiscordRestClient | |||
public partial class DiscordRpcClient : DiscordClient | |||
{ | |||
private readonly ILogger _rpcLogger; | |||
private readonly Logger _rpcLogger; | |||
private readonly JsonSerializer _serializer; | |||
private TaskCompletionSource<bool> _connectTask; | |||
@@ -58,18 +58,7 @@ namespace Discord.Rpc | |||
}; | |||
} | |||
private static API.DiscordRpcApiClient CreateApiClient(DiscordRpcConfig config) | |||
=> new API.DiscordRpcApiClient(config.ClientId, config.Origin, config.RestClientProvider, config.WebSocketProvider, requestQueue: new RequestQueue()); | |||
internal override void Dispose(bool disposing) | |||
{ | |||
if (!_isDisposed) | |||
ApiClient.Dispose(); | |||
} | |||
protected override Task ValidateTokenAsync(TokenType tokenType, string token) | |||
{ | |||
return Task.CompletedTask; //Validation is done in DiscordRpcAPIClient | |||
} | |||
=> new API.DiscordRpcApiClient(config.ClientId, DiscordRestConfig.UserAgent, config.Origin, config.RestClientProvider, config.WebSocketProvider, requestQueue: new RequestQueue()); | |||
/// <inheritdoc /> | |||
public Task ConnectAsync() => ConnectAsync(false); | |||
@@ -371,20 +360,20 @@ namespace Discord.Rpc | |||
//Messages | |||
case "MESSAGE_CREATE": | |||
{ | |||
await _rpcLogger.DebugAsync("Received Dispatch (MESSAGE_CREATE)").ConfigureAwait(false); | |||
/*await _rpcLogger.DebugAsync("Received Dispatch (MESSAGE_CREATE)").ConfigureAwait(false); | |||
var data = (payload.Value as JToken).ToObject<MessageEvent>(_serializer); | |||
var msg = new RpcMessage(this, data.Message); | |||
await _messageReceivedEvent.InvokeAsync(data.ChannelId, msg).ConfigureAwait(false); | |||
await _messageReceivedEvent.InvokeAsync(data.ChannelId, msg).ConfigureAwait(false);*/ | |||
} | |||
break; | |||
case "MESSAGE_UPDATE": | |||
{ | |||
await _rpcLogger.DebugAsync("Received Dispatch (MESSAGE_UPDATE)").ConfigureAwait(false); | |||
/*await _rpcLogger.DebugAsync("Received Dispatch (MESSAGE_UPDATE)").ConfigureAwait(false); | |||
var data = (payload.Value as JToken).ToObject<MessageEvent>(_serializer); | |||
var msg = new RpcMessage(this, data.Message); | |||
await _messageUpdatedEvent.InvokeAsync(data.ChannelId, msg).ConfigureAwait(false); | |||
await _messageUpdatedEvent.InvokeAsync(data.ChannelId, msg).ConfigureAwait(false);*/ | |||
} | |||
break; | |||
case "MESSAGE_DELETE": | |||
@@ -1,8 +0,0 @@ | |||
namespace Discord.Rpc | |||
{ | |||
/*public interface IRemoteUserGuild : ISnowflakeEntity | |||
{ | |||
/// <summary> Gets the name of this guild. </summary> | |||
string Name { get; } | |||
}*/ | |||
} |
@@ -0,0 +1,10 @@ | |||
namespace Discord.Rpc | |||
{ | |||
/*internal class RpcMessage : RpcEntity<ulong>, IMessage | |||
{ | |||
internal RpcMessage(DiscordRpcClient discord, API.Message model) | |||
: base(dicsord, model.Id) | |||
{ | |||
} | |||
}*/ | |||
} |
@@ -0,0 +1,19 @@ | |||
using System; | |||
namespace Discord.Rpc | |||
{ | |||
public abstract class RpcEntity<T> : IEntity<T> | |||
where T : IEquatable<T> | |||
{ | |||
public DiscordRpcClient Discord { get; } | |||
public T Id { get; } | |||
internal RpcEntity(DiscordRpcClient discord, T id) | |||
{ | |||
Discord = discord; | |||
Id = id; | |||
} | |||
IDiscordClient IEntity<T>.Discord => Discord; | |||
} | |||
} |
@@ -4,7 +4,7 @@ using Model = Discord.API.Rpc.RpcUserGuild; | |||
namespace Discord.Rpc | |||
{ | |||
/*internal class RemoteUserGuild : IRemoteUserGuild, ISnowflakeEntity | |||
/*internal class RemoteUserGuild : RpcEntity, IRemoteUserGuild, ISnowflakeEntity | |||
{ | |||
public ulong Id { get; } | |||
public DiscordRestClient Discord { get; } | |||
@@ -12,7 +12,7 @@ namespace Discord.Rpc | |||
public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); | |||
public RemoteUserGuild(DiscordRestClient discord, Model model) | |||
internal RemoteUserGuild(DiscordRestClient discord, Model model) | |||
{ | |||
Id = model.Id; | |||
Discord = discord; |
@@ -1,15 +0,0 @@ | |||
using Discord.Rest; | |||
namespace Discord.Rpc | |||
{ | |||
internal class RpcMessage : Message | |||
{ | |||
public override DiscordRestClient Discord { get; } | |||
public RpcMessage(DiscordRpcClient discord, API.Message model) | |||
: base(null, model.Author.IsSpecified ? new User(model.Author.Value) : null, model) | |||
{ | |||
Discord = discord; | |||
} | |||
} | |||
} |
@@ -1,19 +1,35 @@ | |||
{ | |||
"version": "1.0.0-beta2-*", | |||
"buildOptions": { | |||
"compile": { | |||
"include": [ "../Discord.Net.Utils/**.cs" ] | |||
"configurations": { | |||
"Release": { | |||
"buildOptions": { | |||
"define": [ "RELEASE" ], | |||
"nowarn": [ "CS1573", "CS1591" ], | |||
"optimize": true, | |||
"warningsAsErrors": true, | |||
"xmlDoc": true | |||
} | |||
} | |||
}, | |||
"dependencies": { | |||
"Discord.Net.Core": { | |||
"target": "project" | |||
}, | |||
"Discord.Net.Rest": { | |||
"target": "project" | |||
}, | |||
"NETStandard.Library": "1.6.0" | |||
}, | |||
"frameworks": { | |||
"netstandard1.6": { | |||
"imports": "dnxcore50" | |||
"netstandard1.3": { | |||
"imports": [ | |||
"dotnet5.4", | |||
"dnxcore50", | |||
"portable-net45+win8" | |||
] | |||
} | |||
} | |||
} |
@@ -1,26 +0,0 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | |||
<PropertyGroup> | |||
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects> | |||
<HasSharedItems>true</HasSharedItems> | |||
<SharedGUID>2b75119c-9893-4aaa-8d38-6176eeb09060</SharedGUID> | |||
</PropertyGroup> | |||
<PropertyGroup Label="Configuration"> | |||
<Import_RootNamespace>Discord</Import_RootNamespace> | |||
</PropertyGroup> | |||
<ItemGroup> | |||
<Compile Include="$(MSBuildThisFileDirectory)AsyncEvent.cs" /> | |||
<Compile Include="$(MSBuildThisFileDirectory)ConcurrentHashSet.cs" /> | |||
<Compile Include="$(MSBuildThisFileDirectory)DateTimeUtils.cs" /> | |||
<Compile Include="$(MSBuildThisFileDirectory)Extensions\CollectionExtensions.cs" /> | |||
<Compile Include="$(MSBuildThisFileDirectory)Extensions\TaskCompletionSourceExtensions.cs" /> | |||
<Compile Include="$(MSBuildThisFileDirectory)Logging\Logger.cs" /> | |||
<Compile Include="$(MSBuildThisFileDirectory)Logging\LogManager.cs" /> | |||
<Compile Include="$(MSBuildThisFileDirectory)MentionsHelper.cs" /> | |||
<Compile Include="$(MSBuildThisFileDirectory)Paging\Page.cs" /> | |||
<Compile Include="$(MSBuildThisFileDirectory)Paging\PagedEnumerator.cs" /> | |||
<Compile Include="$(MSBuildThisFileDirectory)Paging\PageInfo.cs" /> | |||
<Compile Include="$(MSBuildThisFileDirectory)Permissions.cs" /> | |||
<Compile Include="$(MSBuildThisFileDirectory)Preconditions.cs" /> | |||
</ItemGroup> | |||
</Project> |
@@ -1,13 +0,0 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | |||
<PropertyGroup Label="Globals"> | |||
<ProjectGuid>2b75119c-9893-4aaa-8d38-6176eeb09060</ProjectGuid> | |||
<MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion> | |||
</PropertyGroup> | |||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> | |||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" /> | |||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" /> | |||
<PropertyGroup /> | |||
<Import Project="Discord.Net.Utils.projitems" Label="Shared" /> | |||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" /> | |||
</Project> |
@@ -4,6 +4,7 @@ using Discord.API.Rest; | |||
using Discord.Net.Queue; | |||
using Discord.Net.Rest; | |||
using Discord.Net.WebSockets; | |||
using Discord.WebSocket; | |||
using Newtonsoft.Json; | |||
using System; | |||
using System.Collections.Generic; | |||
@@ -31,8 +32,8 @@ namespace Discord.API | |||
public ConnectionState ConnectionState { get; private set; } | |||
public DiscordSocketApiClient(RestClientProvider restClientProvider, WebSocketProvider webSocketProvider, JsonSerializer serializer = null, RequestQueue requestQueue = null) | |||
: base(restClientProvider, serializer, requestQueue) | |||
public DiscordSocketApiClient(RestClientProvider restClientProvider, string userAgent, WebSocketProvider webSocketProvider, JsonSerializer serializer = null, RequestQueue requestQueue = null) | |||
: base(restClientProvider, userAgent, serializer, requestQueue) | |||
{ | |||
_gatewayClient = webSocketProvider(); | |||
//_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .Net 4.6+) | |||
@@ -0,0 +1,3 @@ | |||
using System.Runtime.CompilerServices; | |||
[assembly: InternalsVisibleTo("Discord.Net.Test")] |
@@ -5,6 +5,7 @@ using Discord.WebSocket; | |||
using Newtonsoft.Json; | |||
using Newtonsoft.Json.Linq; | |||
using System; | |||
using System.IO; | |||
using System.Linq; | |||
using System.Net; | |||
using System.Text; | |||
@@ -34,10 +35,7 @@ namespace Discord.Audio | |||
} | |||
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 | |||
private readonly Logger _audioLogger; | |||
internal readonly SemaphoreSlim _connectionLock; | |||
private readonly JsonSerializer _serializer; | |||
@@ -63,9 +61,6 @@ namespace Discord.Audio | |||
Guild = guild; | |||
_audioLogger = Discord.LogManager.CreateLogger($"Audio #{id}"); | |||
#if BENCHMARK | |||
_benchmarkLogger = logManager.CreateLogger("Benchmark"); | |||
#endif | |||
_connectionLock = new SemaphoreSlim(1, 1); | |||
@@ -181,11 +176,11 @@ namespace Discord.Audio | |||
ApiClient.SendAsync(data, count).ConfigureAwait(false); | |||
} | |||
public RTPWriteStream CreateOpusStream(int samplesPerFrame, int bufferSize = 4000) | |||
public Stream CreateOpusStream(int samplesPerFrame, int bufferSize = 4000) | |||
{ | |||
return new RTPWriteStream(this, _secretKey, samplesPerFrame, _ssrc, bufferSize = 4000); | |||
} | |||
public OpusEncodeStream CreatePCMStream(int samplesPerFrame, int? bitrate = null, | |||
public Stream CreatePCMStream(int samplesPerFrame, int? bitrate = null, | |||
OpusApplication application = OpusApplication.MusicOrMixed, int bufferSize = 4000) | |||
{ | |||
return new OpusEncodeStream(this, _secretKey, samplesPerFrame, _ssrc, bitrate, application, bufferSize); | |||
@@ -193,11 +188,6 @@ namespace Discord.Audio | |||
private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload) | |||
{ | |||
#if BENCHMARK | |||
Stopwatch stopwatch = Stopwatch.StartNew(); | |||
try | |||
{ | |||
#endif | |||
try | |||
{ | |||
switch (opCode) | |||
@@ -262,15 +252,6 @@ namespace Discord.Audio | |||
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) | |||
{ | |||
@@ -1,6 +1,6 @@ | |||
namespace Discord.Audio | |||
{ | |||
public class OpusDecodeStream : RTPReadStream | |||
internal class OpusDecodeStream : RTPReadStream | |||
{ | |||
private readonly byte[] _buffer; | |||
private readonly OpusDecoder _decoder; | |||
@@ -1,6 +1,6 @@ | |||
namespace Discord.Audio | |||
{ | |||
public class OpusEncodeStream : RTPWriteStream | |||
internal class OpusEncodeStream : RTPWriteStream | |||
{ | |||
public int SampleRate = 48000; | |||
public int Channels = 2; | |||
@@ -4,7 +4,7 @@ using System.IO; | |||
namespace Discord.Audio | |||
{ | |||
public class RTPReadStream : Stream | |||
internal class RTPReadStream : Stream | |||
{ | |||
private readonly BlockingCollection<byte[]> _queuedData; //TODO: Replace with max-length ring buffer | |||
private readonly AudioClient _audioClient; | |||
@@ -3,7 +3,7 @@ using System.IO; | |||
namespace Discord.Audio | |||
{ | |||
public class RTPWriteStream : Stream | |||
internal class RTPWriteStream : Stream | |||
{ | |||
private readonly AudioClient _audioClient; | |||
private readonly byte[] _nonce, _secretKey; | |||
@@ -12,37 +12,37 @@ namespace Discord.WebSocket | |||
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, SocketChannel> _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<SocketChannel> 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)) | |||
internal IReadOnlyCollection<IPrivateChannel> PrivateChannels => | |||
_dmChannels.Select(x => x.Value as IPrivateChannel).Concat( | |||
_groupChannels.Select(x => GetChannel(x) as IPrivateChannel)) | |||
.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)); | |||
_channels = new ConcurrentDictionary<ulong, SocketChannel>(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) | |||
internal SocketChannel GetChannel(ulong id) | |||
{ | |||
ISocketChannel channel; | |||
SocketChannel channel; | |||
if (_channels.TryGetValue(id, out channel)) | |||
return channel; | |||
return null; | |||
@@ -54,7 +54,7 @@ namespace Discord.WebSocket | |||
return channel; | |||
return null; | |||
} | |||
internal void AddChannel(ISocketChannel channel) | |||
internal void AddChannel(SocketChannel channel) | |||
{ | |||
_channels[channel.Id] = channel; | |||
@@ -68,9 +68,9 @@ namespace Discord.WebSocket | |||
_groupChannels.TryAdd(groupChannel.Id); | |||
} | |||
} | |||
internal ISocketChannel RemoveChannel(ulong id) | |||
internal SocketChannel RemoveChannel(ulong id) | |||
{ | |||
ISocketChannel channel; | |||
SocketChannel channel; | |||
if (_channels.TryRemove(id, out channel)) | |||
{ | |||
var dmChannel = channel as SocketDMChannel; | |||
@@ -4,18 +4,16 @@ | |||
<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> | |||
<RootNamespace>Discord.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> | |||
</Project> |
@@ -1,4 +1,5 @@ | |||
using Discord.API.Gateway; | |||
using Discord.API; | |||
using Discord.API.Gateway; | |||
using Discord.Audio; | |||
using Discord.Logging; | |||
using Discord.Net.Converters; | |||
@@ -11,24 +12,22 @@ using System; | |||
using System.Collections.Concurrent; | |||
using System.Collections.Generic; | |||
using System.Collections.Immutable; | |||
using System.IO; | |||
using System.Linq; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
namespace Discord.WebSocket | |||
{ | |||
public partial class DiscordSocketClient : DiscordRestClient, IDiscordClient | |||
public partial class DiscordSocketClient : DiscordClient, IDiscordClient | |||
{ | |||
private readonly ConcurrentQueue<ulong> _largeGuilds; | |||
private readonly ILogger _gatewayLogger; | |||
#if BENCHMARK | |||
private readonly ILogger _benchmarkLogger; | |||
#endif | |||
private readonly Logger _gatewayLogger; | |||
private readonly JsonSerializer _serializer; | |||
private string _sessionId; | |||
private int _lastSeq; | |||
private ImmutableDictionary<string, VoiceRegion> _voiceRegions; | |||
private ImmutableDictionary<string, RestVoiceRegion> _voiceRegions; | |||
private TaskCompletionSource<bool> _connectTask; | |||
private CancellationTokenSource _cancelToken, _reconnectCancelToken; | |||
private Task _heartbeatTask, _guildDownloadTask, _reconnectTask; | |||
@@ -54,16 +53,17 @@ namespace Discord.WebSocket | |||
internal int ConnectionTimeout { get; private set; } | |||
internal WebSocketProvider WebSocketProvider { get; private set; } | |||
public new API.DiscordSocketApiClient ApiClient => base.ApiClient as API.DiscordSocketApiClient; | |||
internal SocketSelfUser CurrentUser => _currentUser as SocketSelfUser; | |||
public new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; | |||
public new SocketSelfUser CurrentUser => base.CurrentUser as SocketSelfUser; | |||
public IReadOnlyCollection<IPrivateChannel> PrivateChannels => DataStore.PrivateChannels; | |||
internal IReadOnlyCollection<SocketGuild> Guilds => DataStore.Guilds; | |||
internal IReadOnlyCollection<VoiceRegion> VoiceRegions => _voiceRegions.ToReadOnlyCollection(); | |||
/// <summary> Creates a new REST/WebSocket discord client. </summary> | |||
public DiscordSocketClient() : this(new DiscordSocketConfig()) { } | |||
/// <summary> Creates a new REST/WebSocket discord client. </summary> | |||
public DiscordSocketClient(DiscordSocketConfig config) | |||
: base(config, CreateApiClient(config)) | |||
public DiscordSocketClient(DiscordSocketConfig config) : this(config, CreateApiClient(config)) { } | |||
private DiscordSocketClient(DiscordSocketConfig config, API.DiscordSocketApiClient client) | |||
: base(config, client) | |||
{ | |||
ShardId = config.ShardId; | |||
TotalShards = config.TotalShards; | |||
@@ -72,14 +72,10 @@ namespace Discord.WebSocket | |||
AudioMode = config.AudioMode; | |||
WebSocketProvider = config.WebSocketProvider; | |||
ConnectionTimeout = config.ConnectionTimeout; | |||
DataStore = new DataStore(0, 0); | |||
_nextAudioId = 1; | |||
_gatewayLogger = LogManager.CreateLogger("Gateway"); | |||
#if BENCHMARK | |||
_benchmarkLogger = _log.CreateLogger("Benchmark"); | |||
#endif | |||
_serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; | |||
_serializer.Error += (s, e) => | |||
@@ -107,25 +103,25 @@ namespace Discord.WebSocket | |||
GuildUnavailable += async g => await _gatewayLogger.VerboseAsync($"Disconnected from {g.Name}").ConfigureAwait(false); | |||
LatencyUpdated += async (old, val) => await _gatewayLogger.VerboseAsync($"Latency = {val} ms").ConfigureAwait(false); | |||
_voiceRegions = ImmutableDictionary.Create<string, VoiceRegion>(); | |||
_voiceRegions = ImmutableDictionary.Create<string, RestVoiceRegion>(); | |||
_largeGuilds = new ConcurrentQueue<ulong>(); | |||
} | |||
private static API.DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config) | |||
=> new API.DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, requestQueue: new RequestQueue()); | |||
=> new API.DiscordSocketApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent, config.WebSocketProvider, requestQueue: new RequestQueue()); | |||
protected override async Task OnLoginAsync(TokenType tokenType, string token) | |||
{ | |||
var voiceRegions = await ApiClient.GetVoiceRegionsAsync().ConfigureAwait(false); | |||
_voiceRegions = voiceRegions.Select(x => new VoiceRegion(x)).ToImmutableDictionary(x => x.Id); | |||
_voiceRegions = voiceRegions.Select(x => RestVoiceRegion.Create(this, x)).ToImmutableDictionary(x => x.Id); | |||
} | |||
protected override async Task OnLogoutAsync() | |||
{ | |||
if (ConnectionState != ConnectionState.Disconnected) | |||
await DisconnectInternalAsync(null, false).ConfigureAwait(false); | |||
_voiceRegions = ImmutableDictionary.Create<string, VoiceRegion>(); | |||
_voiceRegions = ImmutableDictionary.Create<string, RestVoiceRegion>(); | |||
} | |||
/// <inheritdoc /> | |||
public async Task ConnectAsync(bool waitForGuilds = true) | |||
{ | |||
@@ -319,127 +315,55 @@ namespace Discord.WebSocket | |||
} | |||
/// <inheritdoc /> | |||
public override Task<IVoiceRegion> GetVoiceRegionAsync(string id) | |||
{ | |||
VoiceRegion region; | |||
if (_voiceRegions.TryGetValue(id, out region)) | |||
return Task.FromResult<IVoiceRegion>(region); | |||
return Task.FromResult<IVoiceRegion>(null); | |||
} | |||
public Task<RestApplication> GetApplicationInfoAsync() | |||
=> ClientHelper.GetApplicationInfoAsync(this); | |||
/// <inheritdoc /> | |||
public override Task<IGuild> GetGuildAsync(ulong id) | |||
{ | |||
return Task.FromResult<IGuild>(DataStore.GetGuild(id)); | |||
} | |||
public override Task<GuildEmbed?> GetGuildEmbedAsync(ulong id) | |||
{ | |||
var guild = DataStore.GetGuild(id); | |||
if (guild != null) | |||
return Task.FromResult<GuildEmbed?>(new GuildEmbed(guild.IsEmbeddable, guild.EmbedChannelId)); | |||
else | |||
return Task.FromResult<GuildEmbed?>(null); | |||
} | |||
public override Task<IReadOnlyCollection<IUserGuild>> GetGuildSummariesAsync() | |||
{ | |||
return Task.FromResult<IReadOnlyCollection<IUserGuild>>(Guilds); | |||
} | |||
public override Task<IReadOnlyCollection<IGuild>> GetGuildsAsync() | |||
public SocketGuild GetGuild(ulong id) | |||
{ | |||
return Task.FromResult<IReadOnlyCollection<IGuild>>(Guilds); | |||
return DataStore.GetGuild(id); | |||
} | |||
internal SocketGuild AddGuild(ExtendedGuild model, DataStore dataStore) | |||
{ | |||
var guild = new SocketGuild(this, model, dataStore); | |||
dataStore.AddGuild(guild); | |||
if (model.Large) | |||
_largeGuilds.Enqueue(model.Id); | |||
return guild; | |||
} | |||
internal SocketGuild RemoveGuild(ulong id) | |||
{ | |||
var guild = DataStore.RemoveGuild(id); | |||
if (guild != null) | |||
{ | |||
foreach (var channel in guild.Channels) | |||
DataStore.RemoveChannel(id); | |||
foreach (var user in guild.Members) | |||
user.User.RemoveRef(this); | |||
} | |||
return guild; | |||
} | |||
/// <inheritdoc /> | |||
public override Task<IChannel> GetChannelAsync(ulong id) | |||
{ | |||
return Task.FromResult<IChannel>(DataStore.GetChannel(id)); | |||
} | |||
public override Task<IReadOnlyCollection<IPrivateChannel>> GetPrivateChannelsAsync() | |||
{ | |||
return Task.FromResult<IReadOnlyCollection<IPrivateChannel>>(DataStore.PrivateChannels); | |||
} | |||
internal ISocketChannel AddPrivateChannel(API.Channel model, DataStore dataStore) | |||
{ | |||
switch (model.Type) | |||
{ | |||
case ChannelType.DM: | |||
{ | |||
var recipients = model.Recipients.Value; | |||
var user = GetOrAddUser(recipients[0], dataStore); | |||
var channel = new SocketDMChannel(this, new SocketDMUser(user), model); | |||
dataStore.AddChannel(channel); | |||
return channel; | |||
} | |||
case ChannelType.Group: | |||
{ | |||
var channel = new SocketGroupChannel(this, model); | |||
channel.UpdateUsers(model.Recipients.Value, dataStore); | |||
dataStore.AddChannel(channel); | |||
return channel; | |||
} | |||
default: | |||
throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); | |||
} | |||
} | |||
internal ISocketChannel RemovePrivateChannel(ulong id) | |||
{ | |||
var channel = DataStore.RemoveChannel(id) as ISocketPrivateChannel; | |||
if (channel != null) | |||
{ | |||
foreach (var recipient in channel.Recipients) | |||
recipient.User.RemoveRef(this); | |||
} | |||
return channel; | |||
} | |||
public Task<RestGuild> CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null) | |||
=> ClientHelper.CreateGuildAsync(this, name, region, jpegIcon); | |||
/// <inheritdoc /> | |||
public override Task<IUser> GetUserAsync(ulong id) | |||
public IChannel GetChannel(ulong id) | |||
{ | |||
return Task.FromResult<IUser>(DataStore.GetUser(id)); | |||
return DataStore.GetChannel(id); | |||
} | |||
/// <inheritdoc /> | |||
public override Task<IUser> GetUserAsync(string username, string discriminator) | |||
{ | |||
return Task.FromResult<IUser>(DataStore.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault()); | |||
} | |||
public Task<IReadOnlyCollection<RestConnection>> GetConnectionsAsync() | |||
=> ClientHelper.GetConnectionsAsync(this); | |||
/// <inheritdoc /> | |||
public Task<RestInvite> GetInviteAsync(string inviteId) | |||
=> ClientHelper.GetInviteAsync(this, inviteId); | |||
/// <inheritdoc /> | |||
public override Task<ISelfUser> GetCurrentUserAsync() | |||
public IUser GetUser(ulong id) | |||
{ | |||
return Task.FromResult<ISelfUser>(_currentUser); | |||
return DataStore.GetUser(id); | |||
} | |||
internal SocketGlobalUser GetOrAddUser(API.User model, DataStore dataStore) | |||
/// <inheritdoc /> | |||
public IUser GetUser(string username, string discriminator) | |||
{ | |||
var user = dataStore.GetOrAddUser(model.Id, _ => new SocketGlobalUser(model)); | |||
user.AddRef(); | |||
return user; | |||
return DataStore.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault(); | |||
} | |||
internal SocketGlobalUser RemoveUser(ulong id) | |||
/// <inheritdoc /> | |||
public RestVoiceRegion GetVoiceRegion(string id) | |||
{ | |||
return DataStore.RemoveUser(id); | |||
RestVoiceRegion region; | |||
if (_voiceRegions.TryGetValue(id, out region)) | |||
return region; | |||
return null; | |||
} | |||
/// <summary> Downloads the users list for all large guilds. </summary> | |||
public Task DownloadAllUsersAsync() | |||
/*public Task DownloadAllUsersAsync() | |||
=> DownloadUsersAsync(DataStore.Guilds.Where(x => !x.HasAllMembers)); | |||
/// <summary> Downloads the users list for the provided guilds, if they don't have a complete list. </summary> | |||
public Task DownloadUsersAsync(IEnumerable<IGuild> guilds) | |||
@@ -490,20 +414,10 @@ namespace Discord.WebSocket | |||
else | |||
await Task.WhenAll(batchTasks).ConfigureAwait(false); | |||
} | |||
} | |||
}*/ | |||
public override Task<IReadOnlyCollection<IVoiceRegion>> GetVoiceRegionsAsync() | |||
{ | |||
return Task.FromResult<IReadOnlyCollection<IVoiceRegion>>(_voiceRegions.ToReadOnlyCollection()); | |||
} | |||
private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string type, object payload) | |||
{ | |||
#if BENCHMARK | |||
Stopwatch stopwatch = Stopwatch.StartNew(); | |||
try | |||
{ | |||
#endif | |||
if (seq != null) | |||
_lastSeq = seq.Value; | |||
try | |||
@@ -516,7 +430,7 @@ namespace Discord.WebSocket | |||
var data = (payload as JToken).ToObject<HelloEvent>(_serializer); | |||
_heartbeatTime = 0; | |||
_heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _cancelToken.Token, _clientLogger); | |||
_heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _cancelToken.Token, LogManager.ClientLogger); | |||
} | |||
break; | |||
case GatewayOpCode.Heartbeat: | |||
@@ -574,9 +488,9 @@ namespace Discord.WebSocket | |||
var data = (payload as JToken).ToObject<ReadyEvent>(_serializer); | |||
var dataStore = new DataStore(data.Guilds.Length, data.PrivateChannels.Length); | |||
var currentUser = new SocketSelfUser(this, data.User); | |||
var currentUser = SocketSelfUser.Create(this, data.User); | |||
int unavailableGuilds = 0; | |||
for (int i = 0; i < data.Guilds.Length; i++) | |||
/*for (int i = 0; i < data.Guilds.Length; i++) | |||
{ | |||
var model = data.Guilds[i]; | |||
var guild = AddGuild(model, dataStore); | |||
@@ -586,10 +500,10 @@ namespace Discord.WebSocket | |||
await _guildAvailableEvent.InvokeAsync(guild).ConfigureAwait(false); | |||
} | |||
for (int i = 0; i < data.PrivateChannels.Length; i++) | |||
AddPrivateChannel(data.PrivateChannels[i], dataStore); | |||
AddPrivateChannel(data.PrivateChannels[i], dataStore);*/ | |||
_sessionId = data.SessionId; | |||
_currentUser = currentUser; | |||
base.CurrentUser = currentUser; | |||
_unavailableGuilds = unavailableGuilds; | |||
DataStore = dataStore; | |||
} | |||
@@ -603,7 +517,7 @@ namespace Discord.WebSocket | |||
await SyncGuildsAsync().ConfigureAwait(false); | |||
_lastGuildAvailableTime = Environment.TickCount; | |||
_guildDownloadTask = WaitForGuildsAsync(_cancelToken.Token, _clientLogger); | |||
_guildDownloadTask = WaitForGuildsAsync(_cancelToken.Token, LogManager.ClientLogger); | |||
await _readyEvent.InvokeAsync().ConfigureAwait(false); | |||
@@ -611,7 +525,7 @@ namespace Discord.WebSocket | |||
await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false); | |||
} | |||
break; | |||
case "RESUMED": | |||
/*case "RESUMED": | |||
{ | |||
await _gatewayLogger.DebugAsync("Received Dispatch (RESUMED)").ConfigureAwait(false); | |||
@@ -1366,7 +1280,7 @@ namespace Discord.WebSocket | |||
} | |||
else | |||
{ | |||
before = new Presence(null, UserStatus.Offline); | |||
before = new SocketPresence(null, UserStatus.Offline); | |||
user = guild.AddOrUpdateUser(data, DataStore); | |||
} | |||
@@ -1430,7 +1344,7 @@ namespace Discord.WebSocket | |||
if (data.GuildId.HasValue) | |||
{ | |||
ISocketUser user; | |||
VoiceState before, after; | |||
SocketVoiceState before, after; | |||
if (data.GuildId != null) | |||
{ | |||
var guild = DataStore.GetGuild(data.GuildId.Value); | |||
@@ -1444,7 +1358,7 @@ namespace Discord.WebSocket | |||
if (data.ChannelId != null) | |||
{ | |||
before = guild.GetVoiceState(data.UserId)?.Clone() ?? new VoiceState(null, null, false, false, false); | |||
before = guild.GetVoiceState(data.UserId)?.Clone() ?? new SocketVoiceState(null, null, false, false, false); | |||
after = guild.AddOrUpdateVoiceState(data, DataStore); | |||
if (data.UserId == _currentUser.Id) | |||
{ | |||
@@ -1453,8 +1367,8 @@ namespace Discord.WebSocket | |||
} | |||
else | |||
{ | |||
before = guild.RemoveVoiceState(data.UserId) ?? new VoiceState(null, null, false, false, false); | |||
after = new VoiceState(null, data); | |||
before = guild.RemoveVoiceState(data.UserId) ?? new SocketVoiceState(null, null, false, false, false); | |||
after = new SocketVoiceState(null, data); | |||
} | |||
user = guild.GetUser(data.UserId); | |||
@@ -1472,13 +1386,13 @@ namespace Discord.WebSocket | |||
{ | |||
if (data.ChannelId != null) | |||
{ | |||
before = groupChannel.GetVoiceState(data.UserId)?.Clone() ?? new VoiceState(null, null, false, false, false); | |||
before = groupChannel.GetVoiceState(data.UserId)?.Clone() ?? new SocketVoiceState(null, null, false, false, false); | |||
after = groupChannel.AddOrUpdateVoiceState(data, DataStore); | |||
} | |||
else | |||
{ | |||
before = groupChannel.RemoveVoiceState(data.UserId) ?? new VoiceState(null, null, false, false, false); | |||
after = new VoiceState(null, data); | |||
before = groupChannel.RemoveVoiceState(data.UserId) ?? new SocketVoiceState(null, null, false, false, false); | |||
after = new SocketVoiceState(null, data); | |||
} | |||
user = groupChannel.GetUser(data.UserId); | |||
} | |||
@@ -1518,7 +1432,7 @@ namespace Discord.WebSocket | |||
} | |||
} | |||
return; | |||
return;*/ | |||
//Ignored (User only) | |||
case "CHANNEL_PINS_ACK": | |||
@@ -1550,18 +1464,9 @@ namespace Discord.WebSocket | |||
await _gatewayLogger.ErrorAsync($"Error handling {opCode}{(type != null ? $" ({type})" : "")}", 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 RunHeartbeatAsync(int intervalMillis, CancellationToken cancelToken, ILogger logger) | |||
private async Task RunHeartbeatAsync(int intervalMillis, CancellationToken cancelToken, Logger logger) | |||
{ | |||
try | |||
{ | |||
@@ -1601,7 +1506,7 @@ namespace Discord.WebSocket | |||
await logger.ErrorAsync("Heartbeat Errored", ex).ConfigureAwait(false); | |||
} | |||
} | |||
private async Task WaitForGuildsAsync(CancellationToken cancelToken, ILogger logger) | |||
private async Task WaitForGuildsAsync(CancellationToken cancelToken, Logger logger) | |||
{ | |||
//Wait for GUILD_AVAILABLEs | |||
try | |||
@@ -1626,5 +1531,42 @@ namespace Discord.WebSocket | |||
if (guildIds.Length > 0) | |||
await ApiClient.SendGuildSyncAsync(guildIds).ConfigureAwait(false); | |||
} | |||
//IDiscordClient | |||
DiscordRestApiClient IDiscordClient.ApiClient => ApiClient; | |||
Task IDiscordClient.ConnectAsync() | |||
=> ConnectAsync(); | |||
async Task<IApplication> IDiscordClient.GetApplicationInfoAsync() | |||
=> await GetApplicationInfoAsync().ConfigureAwait(false); | |||
Task<IChannel> IDiscordClient.GetChannelAsync(ulong id) | |||
=> Task.FromResult<IChannel>(GetChannel(id)); | |||
Task<IReadOnlyCollection<IPrivateChannel>> IDiscordClient.GetPrivateChannelsAsync() | |||
=> Task.FromResult<IReadOnlyCollection<IPrivateChannel>>(PrivateChannels); | |||
async Task<IReadOnlyCollection<IConnection>> IDiscordClient.GetConnectionsAsync() | |||
=> await GetConnectionsAsync(); | |||
async Task<IInvite> IDiscordClient.GetInviteAsync(string inviteId) | |||
=> await GetInviteAsync(inviteId); | |||
Task<IGuild> IDiscordClient.GetGuildAsync(ulong id) | |||
=> Task.FromResult<IGuild>(GetGuild(id)); | |||
Task<IReadOnlyCollection<IGuild>> IDiscordClient.GetGuildsAsync() | |||
=> Task.FromResult<IReadOnlyCollection<IGuild>>(Guilds); | |||
async Task<IGuild> IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon) | |||
=> await CreateGuildAsync(name, region, jpegIcon); | |||
Task<IUser> IDiscordClient.GetUserAsync(ulong id) | |||
=> Task.FromResult<IUser>(GetUser(id)); | |||
Task<IUser> IDiscordClient.GetUserAsync(string username, string discriminator) | |||
=> Task.FromResult<IUser>(GetUser(username, discriminator)); | |||
Task<IReadOnlyCollection<IVoiceRegion>> IDiscordClient.GetVoiceRegionsAsync() | |||
=> Task.FromResult<IReadOnlyCollection<IVoiceRegion>>(_voiceRegions.ToReadOnlyCollection()); | |||
Task<IVoiceRegion> IDiscordClient.GetVoiceRegionAsync(string id) | |||
=> Task.FromResult<IVoiceRegion>(GetVoiceRegion(id)); | |||
} | |||
} |
@@ -0,0 +1,45 @@ | |||
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.WebSocket | |||
{ | |||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
public abstract class SocketChannel : SocketEntity<ulong>, IChannel | |||
{ | |||
internal SocketChannel(DiscordSocketClient discord, ulong id) | |||
: base(discord, id) | |||
{ | |||
} | |||
internal static SocketChannel Create(DiscordSocketClient discord, Model model) | |||
{ | |||
switch (model.Type) | |||
{ | |||
case ChannelType.Text: | |||
return SocketTextChannel.Create(discord, model); | |||
case ChannelType.Voice: | |||
return SocketVoiceChannel.Create(discord, model); | |||
case ChannelType.DM: | |||
return SocketDMChannel.Create(discord, model); | |||
case ChannelType.Group: | |||
return SocketGroupChannel.Create(discord, model); | |||
default: | |||
throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); | |||
} | |||
} | |||
//IChannel | |||
IReadOnlyCollection<IUser> IChannel.CachedUsers => ImmutableArray.Create<IUser>(); | |||
IUser IChannel.GetCachedUser(ulong id) | |||
=> null; | |||
Task<IUser> IChannel.GetUserAsync(ulong id) | |||
=> Task.FromResult<IUser>(null); | |||
IAsyncEnumerable<IReadOnlyCollection<IUser>> IChannel.GetUsersAsync() | |||
=> ImmutableArray.Create<IReadOnlyCollection<IUser>>().ToAsyncEnumerable(); | |||
} | |||
} |
@@ -1,77 +1,138 @@ | |||
using System.Collections.Generic; | |||
using Discord.Rest; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Collections.Immutable; | |||
using System.Diagnostics; | |||
using System.IO; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
using MessageModel = Discord.API.Message; | |||
using Model = Discord.API.Channel; | |||
namespace Discord.WebSocket | |||
{ | |||
internal class DMChannel : IDMChannel, ISocketChannel, ISocketMessageChannel, ISocketPrivateChannel | |||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
public class SocketDMChannel : SocketChannel, IDMChannel | |||
{ | |||
private readonly MessageManager _messages; | |||
private readonly MessageCache _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 SocketUser Recipient { get; private set; } | |||
public SocketDMChannel(DiscordSocketClient discord, SocketDMUser recipient, Model model) | |||
: base(discord, recipient, model) | |||
public IReadOnlyCollection<SocketUser> Users => ImmutableArray.Create(Discord.CurrentUser, Recipient); | |||
internal SocketDMChannel(DiscordSocketClient discord, ulong id, ulong recipientId) | |||
: base(discord, id) | |||
{ | |||
Recipient = new SocketUser(Discord, recipientId); | |||
if (Discord.MessageCacheSize > 0) | |||
_messages = new MessageCache(Discord, this); | |||
else | |||
_messages = new MessageManager(Discord, this); | |||
} | |||
internal new static SocketDMChannel Create(DiscordSocketClient discord, Model model) | |||
{ | |||
var entity = new SocketDMChannel(discord, model.Id, model.Recipients.Value[0].Id); | |||
entity.Update(model); | |||
return entity; | |||
} | |||
internal void Update(Model model) | |||
{ | |||
Recipient.Update(model.Recipients.Value[0]); | |||
} | |||
public Task CloseAsync() | |||
=> ChannelHelper.DeleteAsync(this, Discord); | |||
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) | |||
public SocketUser GetUser(ulong id) | |||
{ | |||
var currentUser = Discord.CurrentUser; | |||
if (id == Recipient.Id) | |||
return Recipient; | |||
else if (id == currentUser.Id) | |||
return currentUser; | |||
else if (id == Discord.CurrentUser.Id) | |||
return Discord.CurrentUser as SocketSelfUser; | |||
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) | |||
public SocketMessage GetMessage(ulong id) | |||
=> _messages?.Get(id); | |||
public async Task<IMessage> GetMessageAsync(ulong id, bool allowDownload = true) | |||
{ | |||
return _messages.Create(author, model); | |||
IMessage msg = _messages?.Get(id); | |||
if (msg == null && allowDownload) | |||
msg = await ChannelHelper.GetMessageAsync(this, Discord, id); | |||
return msg; | |||
} | |||
public ISocketMessage AddMessage(ISocketUser author, MessageModel model) | |||
public IAsyncEnumerable<IReadOnlyCollection<IMessage>> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch) | |||
=> ChannelHelper.GetMessagesAsync(this, Discord, limit: limit); | |||
public IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) | |||
=> ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit); | |||
public IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) | |||
=> ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit); | |||
public Task<IReadOnlyCollection<RestMessage>> GetPinnedMessagesAsync() | |||
=> ChannelHelper.GetPinnedMessagesAsync(this, Discord); | |||
public Task<RestUserMessage> SendMessageAsync(string text, bool isTTS) | |||
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS); | |||
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS) | |||
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS); | |||
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS) | |||
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS); | |||
public Task DeleteMessagesAsync(IEnumerable<IMessage> messages) | |||
=> ChannelHelper.DeleteMessagesAsync(this, Discord, messages); | |||
public IDisposable EnterTypingState() | |||
=> ChannelHelper.EnterTypingState(this, Discord); | |||
internal SocketMessage AddMessage(SocketUser author, MessageModel model) | |||
{ | |||
var msg = _messages.Create(author, model); | |||
var msg = SocketMessage.Create(Discord, author, model); | |||
_messages.Add(msg); | |||
return msg; | |||
} | |||
public ISocketMessage GetMessage(ulong id) | |||
{ | |||
return _messages.Get(id); | |||
} | |||
public ISocketMessage RemoveMessage(ulong id) | |||
internal SocketMessage 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(); | |||
public override string ToString() => $"@{Recipient}"; | |||
private string DebuggerDisplay => $"@{Recipient} ({Id}, DM)"; | |||
//IDMChannel | |||
IUser IDMChannel.Recipient => Recipient; | |||
//IPrivateChannel | |||
IReadOnlyCollection<IUser> IPrivateChannel.Recipients => ImmutableArray.Create<IUser>(Recipient); | |||
//IMessageChannel | |||
IReadOnlyCollection<IMessage> IMessageChannel.CachedMessages => ImmutableArray.Create<IMessage>(); | |||
IMessage IMessageChannel.GetCachedMessage(ulong id) => null; | |||
async Task<IMessage> IMessageChannel.GetMessageAsync(ulong id) | |||
=> await GetMessageAsync(id); | |||
IAsyncEnumerable<IReadOnlyCollection<IMessage>> IMessageChannel.GetMessagesAsync(int limit) | |||
=> GetMessagesAsync(limit); | |||
IAsyncEnumerable<IReadOnlyCollection<IMessage>> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit) | |||
=> GetMessagesAsync(fromMessageId, dir, limit); | |||
async Task<IReadOnlyCollection<IMessage>> IMessageChannel.GetPinnedMessagesAsync() | |||
=> await GetPinnedMessagesAsync().ConfigureAwait(false); | |||
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS) | |||
=> await SendFileAsync(filePath, text, isTTS); | |||
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS) | |||
=> await SendFileAsync(stream, filename, text, isTTS); | |||
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS) | |||
=> await SendMessageAsync(text, isTTS); | |||
IDisposable IMessageChannel.EnterTypingState() | |||
=> EnterTypingState(); | |||
//IChannel | |||
IReadOnlyCollection<IUser> IChannel.CachedUsers => Users; | |||
IUser IChannel.GetCachedUser(ulong id) | |||
=> GetUser(id); | |||
Task<IUser> IChannel.GetUserAsync(ulong id) | |||
=> Task.FromResult<IUser>(GetUser(id)); | |||
IAsyncEnumerable<IReadOnlyCollection<IUser>> IChannel.GetUsersAsync() | |||
=> ImmutableArray.Create<IReadOnlyCollection<IUser>>().ToAsyncEnumerable(); | |||
} | |||
} |
@@ -1,7 +1,10 @@ | |||
using Discord.Rest; | |||
using System; | |||
using System.Collections.Concurrent; | |||
using System.Collections.Generic; | |||
using System.Collections.Immutable; | |||
using System.Diagnostics; | |||
using System.IO; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
using MessageModel = Discord.API.Message; | |||
@@ -11,134 +14,124 @@ using VoiceStateModel = Discord.API.VoiceState; | |||
namespace Discord.WebSocket | |||
{ | |||
internal class SocketGroupChannel : IGroupChannel, ISocketChannel, ISocketMessageChannel, ISocketPrivateChannel | |||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
public class SocketGroupChannel : SocketChannel, IGroupChannel | |||
{ | |||
internal override bool IsAttached => true; | |||
private readonly MessageCache _messages; | |||
private readonly MessageManager _messages; | |||
private ConcurrentDictionary<ulong, VoiceState> _voiceStates; | |||
private string _iconId; | |||
private ConcurrentDictionary<ulong, SocketGroupUser> _users; | |||
private ConcurrentDictionary<ulong, SocketVoiceState> _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 string Name { get; private set; } | |||
public SocketGroupChannel(DiscordSocketClient discord, Model model) | |||
: base(discord, model) | |||
public IReadOnlyCollection<SocketGroupUser> Users => _users.ToReadOnlyCollection(); | |||
public IReadOnlyCollection<SocketGroupUser> Recipients | |||
=> _users.Select(x => x.Value).Where(x => x.Id != Discord.CurrentUser.Id).ToReadOnlyCollection(() => _users.Count - 1); | |||
internal SocketGroupChannel(DiscordSocketClient discord, ulong id) | |||
: base(discord, id) | |||
{ | |||
if (Discord.MessageCacheSize > 0) | |||
_messages = new MessageCache(Discord, this); | |||
else | |||
_messages = new MessageManager(Discord, this); | |||
_voiceStates = new ConcurrentDictionary<ulong, VoiceState>(1, 5); | |||
_voiceStates = new ConcurrentDictionary<ulong, SocketVoiceState>(1, 5); | |||
_users = new ConcurrentDictionary<ulong, SocketGroupUser>(1, 5); | |||
} | |||
public override void Update(Model model) | |||
internal new static SocketGroupChannel Create(DiscordSocketClient discord, Model model) | |||
{ | |||
if (source == UpdateSource.Rest && IsAttached) return; | |||
base.Update(model, source); | |||
var entity = new SocketGroupChannel(discord, model.Id); | |||
entity.Update(model); | |||
return entity; | |||
} | |||
internal void Update(Model model) | |||
{ | |||
if (model.Name.IsSpecified) | |||
Name = model.Name.Value; | |||
if (model.Icon.IsSpecified) | |||
_iconId = model.Icon.Value; | |||
internal void UpdateUsers(UserModel[] models, DataStore dataStore) | |||
if (model.Recipients.IsSpecified) | |||
UpdateUsers(model.Recipients.Value); | |||
} | |||
internal virtual void UpdateUsers(API.User[] models) | |||
{ | |||
var users = new ConcurrentDictionary<ulong, GroupUser>(1, models.Length); | |||
var users = new ConcurrentDictionary<ulong, SocketGroupUser>(1, (int)(models.Length * 1.05)); | |||
for (int i = 0; i < models.Length; i++) | |||
{ | |||
var globalUser = Discord.GetOrAddUser(models[i], dataStore); | |||
users[models[i].Id] = new SocketGroupUser(this, globalUser); | |||
} | |||
users[models[i].Id] = SocketGroupUser.Create(Discord, models[i]); | |||
_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) | |||
public async Task UpdateAsync() | |||
=> Update(await ChannelHelper.GetAsync(this, Discord)); | |||
public Task LeaveAsync() | |||
=> ChannelHelper.DeleteAsync(this, Discord); | |||
public SocketGroupUser GetUser(ulong id) | |||
{ | |||
GroupUser user; | |||
SocketGroupUser 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 user; | |||
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 Task<RestMessage> GetMessageAsync(ulong id) | |||
=> ChannelHelper.GetMessageAsync(this, Discord, id); | |||
public IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch) | |||
=> ChannelHelper.GetMessagesAsync(this, Discord, limit: limit); | |||
public IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) | |||
=> ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit); | |||
public IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) | |||
=> ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit); | |||
public Task<IReadOnlyCollection<RestMessage>> GetPinnedMessagesAsync() | |||
=> ChannelHelper.GetPinnedMessagesAsync(this, Discord); | |||
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 Task<RestUserMessage> SendMessageAsync(string text, bool isTTS) | |||
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS); | |||
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS) | |||
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS); | |||
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS) | |||
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS); | |||
public Task DeleteMessagesAsync(IEnumerable<IMessage> messages) | |||
=> ChannelHelper.DeleteMessagesAsync(this, Discord, messages); | |||
public IDisposable EnterTypingState() | |||
=> ChannelHelper.EnterTypingState(this, Discord); | |||
//IPrivateChannel | |||
IReadOnlyCollection<IUser> IPrivateChannel.Recipients => Recipients; | |||
//IMessageChannel | |||
IReadOnlyCollection<IMessage> IMessageChannel.CachedMessages => ImmutableArray.Create<IMessage>(); | |||
IMessage IMessageChannel.GetCachedMessage(ulong id) | |||
=> null; | |||
async Task<IMessage> IMessageChannel.GetMessageAsync(ulong id) | |||
=> await GetMessageAsync(id); | |||
IAsyncEnumerable<IReadOnlyCollection<IMessage>> IMessageChannel.GetMessagesAsync(int limit) | |||
=> GetMessagesAsync(limit); | |||
IAsyncEnumerable<IReadOnlyCollection<IMessage>> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit) | |||
=> GetMessagesAsync(fromMessageId, dir, limit); | |||
async Task<IReadOnlyCollection<IMessage>> IMessageChannel.GetPinnedMessagesAsync() | |||
=> await GetPinnedMessagesAsync(); | |||
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS) | |||
=> await SendFileAsync(filePath, text, isTTS); | |||
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS) | |||
=> await SendFileAsync(stream, filename, text, isTTS); | |||
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS) | |||
=> await SendMessageAsync(text, isTTS); | |||
IDisposable IMessageChannel.EnterTypingState() | |||
=> EnterTypingState(); | |||
public SocketDMChannel Clone() => MemberwiseClone() as SocketDMChannel; | |||
//IChannel | |||
IReadOnlyCollection<IUser> IChannel.CachedUsers => Users; | |||
IMessage IMessageChannel.GetCachedMessage(ulong id) => GetMessage(id); | |||
ISocketUser ISocketMessageChannel.GetUser(ulong id, bool skipCheck) => GetUser(id); | |||
ISocketChannel ISocketChannel.Clone() => Clone(); | |||
IUser IChannel.GetCachedUser(ulong id) | |||
=> GetUser(id); | |||
Task<IUser> IChannel.GetUserAsync(ulong id) | |||
=> Task.FromResult<IUser>(GetUser(id)); | |||
IAsyncEnumerable<IReadOnlyCollection<IUser>> IChannel.GetUsersAsync() | |||
=> ImmutableArray.Create<IReadOnlyCollection<IUser>>(Users).ToAsyncEnumerable(); | |||
} | |||
} |
@@ -1,4 +1,5 @@ | |||
using Discord.API.Rest; | |||
using Discord.Rest; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Collections.Immutable; | |||
@@ -7,89 +8,59 @@ using System.Linq; | |||
using System.Threading.Tasks; | |||
using Model = Discord.API.Channel; | |||
namespace Discord.Rest | |||
namespace Discord.WebSocket | |||
{ | |||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
internal abstract class SocketGuildChannel : ISnowflakeEntity, IGuildChannel | |||
public abstract class SocketGuildChannel : SocketChannel, IGuildChannel | |||
{ | |||
private List<Overwrite> _overwrites; //TODO: Is maintaining a list here too expensive? Is this threadsafe? | |||
private ImmutableArray<Overwrite> _overwrites; | |||
public string Name { get; private set; } | |||
public int Position { get; private set; } | |||
public IReadOnlyCollection<Overwrite> PermissionOverwrites => _overwrites; | |||
public Guild Guild { get; private set; } | |||
public ulong GuildId { get; } | |||
public override DiscordRestClient Discord => Guild.Discord; | |||
public string Name { get; private set; } | |||
public int Position { get; private set; } | |||
public GuildChannel(Guild guild, Model model) | |||
: base(model.Id) | |||
internal SocketGuildChannel(DiscordSocketClient discord, ulong id, ulong guildId) | |||
: base(discord, id) | |||
{ | |||
Guild = guild; | |||
Update(model); | |||
GuildId = guildId; | |||
} | |||
public virtual void Update(Model model) | |||
internal new static SocketGuildChannel Create(DiscordSocketClient discord, Model model) | |||
{ | |||
switch (model.Type) | |||
{ | |||
case ChannelType.Text: | |||
return SocketTextChannel.Create(discord, model); | |||
case ChannelType.Voice: | |||
return SocketVoiceChannel.Create(discord, model); | |||
default: | |||
throw new InvalidOperationException("Unknown guild channel type"); | |||
} | |||
} | |||
internal 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); | |||
var newOverwrites = ImmutableArray.CreateBuilder<Overwrite>(overwrites.Length); | |||
for (int i = 0; i < overwrites.Length; i++) | |||
newOverwrites.Add(new Overwrite(overwrites[i])); | |||
_overwrites = newOverwrites; | |||
_overwrites = newOverwrites.ToImmutable(); | |||
} | |||
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); | |||
} | |||
=> Update(await ChannelHelper.GetAsync(this, Discord)); | |||
public Task ModifyAsync(Action<ModifyGuildChannelParams> func) | |||
=> ChannelHelper.ModifyAsync(this, Discord, func); | |||
public Task DeleteAsync() | |||
=> ChannelHelper.DeleteAsync(this, Discord); | |||
public OverwritePermissions? GetPermissionOverwrite(IUser user) | |||
{ | |||
for (int i = 0; i < _overwrites.Count; i++) | |||
for (int i = 0; i < _overwrites.Length; i++) | |||
{ | |||
if (_overwrites[i].TargetId == user.Id) | |||
return _overwrites[i].Permissions; | |||
@@ -98,60 +69,91 @@ namespace Discord.Rest | |||
} | |||
public OverwritePermissions? GetPermissionOverwrite(IRole role) | |||
{ | |||
for (int i = 0; i < _overwrites.Count; i++) | |||
for (int i = 0; i < _overwrites.Length; 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 })); | |||
await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, user, perms).ConfigureAwait(false); | |||
_overwrites = _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); | |||
await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, role, perms).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); | |||
await ChannelHelper.RemovePermissionOverwriteAsync(this, Discord, user).ConfigureAwait(false); | |||
for (int i = 0; i < _overwrites.Count; i++) | |||
for (int i = 0; i < _overwrites.Length; i++) | |||
{ | |||
if (_overwrites[i].TargetId == user.Id) | |||
{ | |||
_overwrites.RemoveAt(i); | |||
_overwrites = _overwrites.RemoveAt(i); | |||
return; | |||
} | |||
} | |||
} | |||
public async Task RemovePermissionOverwriteAsync(IRole role) | |||
{ | |||
await Discord.ApiClient.DeleteChannelPermissionAsync(Id, role.Id).ConfigureAwait(false); | |||
await ChannelHelper.RemovePermissionOverwriteAsync(this, Discord, role).ConfigureAwait(false); | |||
for (int i = 0; i < _overwrites.Count; i++) | |||
for (int i = 0; i < _overwrites.Length; i++) | |||
{ | |||
if (_overwrites[i].TargetId == role.Id) | |||
{ | |||
_overwrites.RemoveAt(i); | |||
_overwrites = _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); | |||
public async Task<IReadOnlyCollection<RestInviteMetadata>> GetInvitesAsync() | |||
=> await ChannelHelper.GetInvitesAsync(this, Discord); | |||
public async Task<RestInviteMetadata> CreateInviteAsync(int? maxAge = 3600, int? maxUses = null, bool isTemporary = true) | |||
=> await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary); | |||
//IGuildChannel | |||
async Task<IReadOnlyCollection<IInviteMetadata>> IGuildChannel.GetInvitesAsync() | |||
=> await GetInvitesAsync(); | |||
async Task<IInviteMetadata> IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary) | |||
=> await CreateInviteAsync(maxAge, maxUses, isTemporary); | |||
OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IRole role) | |||
=> GetPermissionOverwrite(role); | |||
OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IUser user) | |||
=> GetPermissionOverwrite(user); | |||
async Task IGuildChannel.AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions) | |||
=> await AddPermissionOverwriteAsync(role, permissions); | |||
async Task IGuildChannel.AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions) | |||
=> await AddPermissionOverwriteAsync(user, permissions); | |||
async Task IGuildChannel.RemovePermissionOverwriteAsync(IRole role) | |||
=> await RemovePermissionOverwriteAsync(role); | |||
async Task IGuildChannel.RemovePermissionOverwriteAsync(IUser user) | |||
=> await RemovePermissionOverwriteAsync(user); | |||
IReadOnlyCollection<IGuildUser> IGuildChannel.CachedUsers | |||
=> ImmutableArray.Create<IGuildUser>(); | |||
IAsyncEnumerable<IReadOnlyCollection<IGuildUser>> IGuildChannel.GetUsersAsync() | |||
=> ImmutableArray.Create<IReadOnlyCollection<IGuildUser>>().ToAsyncEnumerable(); //Overriden in Text/Voice //TODO: Does this actually override? | |||
Task<IGuildUser> IGuildChannel.GetUserAsync(ulong id) | |||
=> Task.FromResult<IGuildUser>(null); //Overriden in Text/Voice //TODO: Does this actually override? | |||
IGuildUser IGuildChannel.GetCachedUser(ulong id) | |||
=> null; | |||
//IChannel | |||
IReadOnlyCollection<IUser> IChannel.CachedUsers | |||
=> ImmutableArray.Create<IUser>(); | |||
IUser IChannel.GetCachedUser(ulong id) | |||
=> null; | |||
IAsyncEnumerable<IReadOnlyCollection<IUser>> IChannel.GetUsersAsync() | |||
=> ImmutableArray.Create<IReadOnlyCollection<IUser>>().ToAsyncEnumerable(); //Overriden in Text/Voice //TODO: Does this actually override? | |||
Task<IUser> IChannel.GetUserAsync(ulong id) | |||
=> Task.FromResult<IUser>(null); //Overriden in Text/Voice //TODO: Does this actually override? | |||
} | |||
} |
@@ -1,6 +1,10 @@ | |||
using Discord.Rest; | |||
using Discord.API.Rest; | |||
using Discord.Rest; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Collections.Immutable; | |||
using System.Diagnostics; | |||
using System.IO; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
using MessageModel = Discord.API.Message; | |||
@@ -8,81 +12,105 @@ using Model = Discord.API.Channel; | |||
namespace Discord.WebSocket | |||
{ | |||
internal class SocketTextChannel : TextChannel, ISocketGuildChannel, ISocketMessageChannel | |||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
public class SocketTextChannel : SocketGuildChannel, ITextChannel | |||
{ | |||
internal override bool IsAttached => true; | |||
private readonly MessageCache _messages; | |||
private readonly MessageManager _messages; | |||
public string Topic { get; private set; } | |||
public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; | |||
public new SocketGuild Guild => base.Guild as SocketGuild; | |||
public string Mention => MentionUtils.MentionChannel(Id); | |||
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) | |||
internal SocketTextChannel(DiscordSocketClient discord, ulong id, ulong guildId) | |||
: base(discord, id, guildId) | |||
{ | |||
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) | |||
internal new static SocketTextChannel Create(DiscordSocketClient discord, Model model) | |||
{ | |||
return await _messages.DownloadAsync(null, Direction.Before, limit).ConfigureAwait(false); | |||
var entity = new SocketTextChannel(discord, model.Id, model.GuildId.Value); | |||
entity.Update(model); | |||
return entity; | |||
} | |||
public override async Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) | |||
internal override void Update(Model model) | |||
{ | |||
return await _messages.DownloadAsync(fromMessageId, dir, limit).ConfigureAwait(false); | |||
} | |||
base.Update(model); | |||
public ISocketMessage CreateMessage(ISocketUser author, MessageModel model) | |||
{ | |||
return _messages.Create(author, model); | |||
Topic = model.Topic.Value; | |||
} | |||
public ISocketMessage AddMessage(ISocketUser author, MessageModel model) | |||
public Task ModifyAsync(Action<ModifyTextChannelParams> func) | |||
=> ChannelHelper.ModifyAsync(this, Discord, func); | |||
public Task<RestGuildUser> GetUserAsync(ulong id) | |||
=> ChannelHelper.GetUserAsync(this, Discord, id); | |||
public IAsyncEnumerable<IReadOnlyCollection<RestGuildUser>> GetUsersAsync() | |||
=> ChannelHelper.GetUsersAsync(this, Discord); | |||
public Task<RestMessage> GetMessageAsync(ulong id) | |||
=> ChannelHelper.GetMessageAsync(this, Discord, id); | |||
public IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch) | |||
=> ChannelHelper.GetMessagesAsync(this, Discord, limit: limit); | |||
public IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) | |||
=> ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit); | |||
public IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) | |||
=> ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit); | |||
public Task<IReadOnlyCollection<RestMessage>> GetPinnedMessagesAsync() | |||
=> ChannelHelper.GetPinnedMessagesAsync(this, Discord); | |||
public Task<RestUserMessage> SendMessageAsync(string text, bool isTTS) | |||
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS); | |||
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS) | |||
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS); | |||
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS) | |||
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS); | |||
public Task DeleteMessagesAsync(IEnumerable<IMessage> messages) | |||
=> ChannelHelper.DeleteMessagesAsync(this, Discord, messages); | |||
public IDisposable EnterTypingState() | |||
=> ChannelHelper.EnterTypingState(this, Discord); | |||
internal SocketMessage AddMessage(SocketUser author, MessageModel model) | |||
{ | |||
var msg = _messages.Create(author, model); | |||
var msg = SocketMessage.Create(Discord, author, model); | |||
_messages.Add(msg); | |||
return msg; | |||
} | |||
public ISocketMessage GetMessage(ulong id) | |||
{ | |||
return _messages.Get(id); | |||
} | |||
public ISocketMessage RemoveMessage(ulong id) | |||
internal SocketMessage RemoveMessage(ulong id) | |||
{ | |||
return _messages.Remove(id); | |||
} | |||
public SocketTextChannel Clone() => MemberwiseClone() as SocketTextChannel; | |||
public override string ToString() => Name; | |||
private string DebuggerDisplay => $"@{Name} ({Id}, Text)"; | |||
//IGuildChannel | |||
async Task<IGuildUser> IGuildChannel.GetUserAsync(ulong id) | |||
=> await GetUserAsync(id); | |||
IAsyncEnumerable<IReadOnlyCollection<IGuildUser>> IGuildChannel.GetUsersAsync() | |||
=> GetUsersAsync(); | |||
IReadOnlyCollection<ISocketUser> ISocketMessageChannel.Users => Members; | |||
//IMessageChannel | |||
IReadOnlyCollection<IMessage> IMessageChannel.CachedMessages => ImmutableArray.Create<IMessage>(); | |||
IMessage IMessageChannel.GetCachedMessage(ulong id) => null; | |||
IMessage IMessageChannel.GetCachedMessage(ulong id) => GetMessage(id); | |||
ISocketUser ISocketMessageChannel.GetUser(ulong id, bool skipCheck) => GetUser(id, skipCheck); | |||
ISocketChannel ISocketChannel.Clone() => Clone(); | |||
async Task<IMessage> IMessageChannel.GetMessageAsync(ulong id) | |||
=> await GetMessageAsync(id); | |||
IAsyncEnumerable<IReadOnlyCollection<IMessage>> IMessageChannel.GetMessagesAsync(int limit) | |||
=> GetMessagesAsync(limit); | |||
IAsyncEnumerable<IReadOnlyCollection<IMessage>> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit) | |||
=> GetMessagesAsync(fromMessageId, dir, limit); | |||
async Task<IReadOnlyCollection<IMessage>> IMessageChannel.GetPinnedMessagesAsync() | |||
=> await GetPinnedMessagesAsync().ConfigureAwait(false); | |||
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS) | |||
=> await SendFileAsync(filePath, text, isTTS); | |||
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS) | |||
=> await SendFileAsync(stream, filename, text, isTTS); | |||
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS) | |||
=> await SendMessageAsync(text, isTTS); | |||
IDisposable IMessageChannel.EnterTypingState() | |||
=> EnterTypingState(); | |||
} | |||
} |
@@ -1,54 +1,50 @@ | |||
using Discord.Audio; | |||
using Discord.API.Rest; | |||
using Discord.Audio; | |||
using Discord.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.WebSocket | |||
{ | |||
internal class SocketVoiceChannel : VoiceChannel, ISocketGuildChannel | |||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
public class SocketVoiceChannel : SocketGuildChannel, IVoiceChannel | |||
{ | |||
internal override bool IsAttached => true; | |||
public int Bitrate { get; private set; } | |||
public int UserLimit { get; private set; } | |||
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) | |||
internal SocketVoiceChannel(DiscordSocketClient discord, ulong id, ulong guildId) | |||
: base(discord, id, guildId) | |||
{ | |||
} | |||
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) | |||
internal new static SocketVoiceChannel Create(DiscordSocketClient discord, Model model) | |||
{ | |||
var user = Guild.GetUser(id); | |||
if (user != null && user.VoiceChannel.Id == Id) | |||
return user; | |||
return null; | |||
var entity = new SocketVoiceChannel(discord, model.Id, model.GuildId.Value); | |||
entity.Update(model); | |||
return entity; | |||
} | |||
public override async Task<IAudioClient> ConnectAsync() | |||
internal override void Update(Model model) | |||
{ | |||
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); | |||
base.Update(model); | |||
Bitrate = model.Bitrate.Value; | |||
UserLimit = model.UserLimit.Value; | |||
} | |||
public SocketVoiceChannel Clone() => MemberwiseClone() as SocketVoiceChannel; | |||
public Task ModifyAsync(Action<ModifyVoiceChannelParams> func) | |||
=> ChannelHelper.ModifyAsync(this, Discord, func); | |||
//IVoiceChannel | |||
Task<IAudioClient> IVoiceChannel.ConnectAsync() { throw new NotSupportedException(); } | |||
ISocketChannel ISocketChannel.Clone() => Clone(); | |||
//IGuildChannel | |||
Task<IGuildUser> IGuildChannel.GetUserAsync(ulong id) | |||
=> Task.FromResult<IGuildUser>(null); | |||
IAsyncEnumerable<IReadOnlyCollection<IGuildUser>> IGuildChannel.GetUsersAsync() | |||
=> ImmutableArray.Create<IReadOnlyCollection<IGuildUser>>().ToAsyncEnumerable(); | |||
} | |||
} |
@@ -1,4 +1,5 @@ | |||
using Discord.Audio; | |||
using Discord.API.Rest; | |||
using Discord.Audio; | |||
using Discord.Rest; | |||
using System; | |||
using System.Collections.Concurrent; | |||
@@ -19,402 +20,204 @@ using VoiceStateModel = Discord.API.VoiceState; | |||
namespace Discord.WebSocket | |||
{ | |||
internal class SocketGuild : Guild, IGuild, IUserGuild | |||
public class SocketGuild : SocketEntity<ulong>, IGuild | |||
{ | |||
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; | |||
private ImmutableDictionary<ulong, RestRole> _roles; | |||
private ImmutableArray<Emoji> _emojis; | |||
private ImmutableArray<string> _features; | |||
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); | |||
public string Name { get; private set; } | |||
public int AFKTimeout { get; private set; } | |||
public bool IsEmbeddable { get; private set; } | |||
public VerificationLevel VerificationLevel { get; private set; } | |||
public MfaLevel MfaLevel { get; private set; } | |||
public DefaultMessageNotifications DefaultMessageNotifications { get; private set; } | |||
public ulong? AFKChannelId { get; private set; } | |||
public ulong? EmbedChannelId { get; private set; } | |||
public ulong OwnerId { get; private set; } | |||
public string VoiceRegionId { get; private set; } | |||
public string IconId { get; private set; } | |||
public string SplashId { get; private set; } | |||
public ulong DefaultChannelId => Id; | |||
public string IconUrl => API.CDN.GetGuildIconUrl(Id, IconId); | |||
public string SplashUrl => API.CDN.GetGuildSplashUrl(Id, SplashId); | |||
public bool IsSynced => false; | |||
public RestRole EveryoneRole => GetRole(Id); | |||
public IReadOnlyCollection<RestRole> Roles => _roles.ToReadOnlyCollection(); | |||
public IReadOnlyCollection<Emoji> Emojis => _emojis; | |||
public IReadOnlyCollection<string> Features => _features; | |||
internal SocketGuild(DiscordSocketClient client, ulong id) | |||
: base(client, id) | |||
{ | |||
} | |||
internal static SocketGuild Create(DiscordSocketClient discord, Model model) | |||
{ | |||
var entity = new SocketGuild(discord, model.Id); | |||
entity.Update(model); | |||
return entity; | |||
} | |||
internal void Update(Model model) | |||
{ | |||
AFKChannelId = model.AFKChannelId; | |||
EmbedChannelId = model.EmbedChannelId; | |||
AFKTimeout = model.AFKTimeout; | |||
IsEmbeddable = model.EmbedEnabled; | |||
IconId = model.Icon; | |||
Name = model.Name; | |||
OwnerId = model.OwnerId; | |||
VoiceRegionId = model.Region; | |||
SplashId = model.Splash; | |||
VerificationLevel = model.VerificationLevel; | |||
MfaLevel = model.MfaLevel; | |||
DefaultMessageNotifications = model.DefaultMessageNotifications; | |||
if (model.Emojis != null) | |||
{ | |||
var emojis = ImmutableArray.CreateBuilder<Emoji>(model.Emojis.Length); | |||
for (int i = 0; i < model.Emojis.Length; i++) | |||
emojis.Add(Emoji.Create(model.Emojis[i])); | |||
_emojis = emojis.ToImmutableArray(); | |||
} | |||
_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 ISocketGuildChannel AddChannel(ChannelModel model, DataStore dataStore, ConcurrentHashSet<ulong> channels = null) | |||
{ | |||
var channel = ToChannel(model); | |||
(channels ?? _channels).TryAdd(model.Id); | |||
dataStore.AddChannel(channel); | |||
return 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; | |||
_emojis = ImmutableArray.Create<Emoji>(); | |||
SocketGuildUser member; | |||
if (members.TryGetValue(model.User.Id, out member)) | |||
member.Update(model, UpdateSource.WebSocket); | |||
if (model.Features != null) | |||
_features = model.Features.ToImmutableArray(); | |||
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); | |||
_features = ImmutableArray.Create<string>(); | |||
var roles = ImmutableDictionary.CreateBuilder<ulong, RestRole>(); | |||
if (model.Roles != null) | |||
{ | |||
throw new NotImplementedException(); | |||
} | |||
_roles = roles.ToImmutable(); | |||
} | |||
//General | |||
public async Task UpdateAsync() | |||
=> Update(await Discord.ApiClient.GetGuildAsync(Id)); | |||
public Task DeleteAsync() | |||
=> GuildHelper.DeleteAsync(this, Discord); | |||
public Task ModifyAsync(Action<ModifyGuildParams> func) | |||
=> GuildHelper.ModifyAsync(this, Discord, func); | |||
public Task ModifyEmbedAsync(Action<ModifyGuildEmbedParams> func) | |||
=> GuildHelper.ModifyEmbedAsync(this, Discord, func); | |||
public Task ModifyChannelsAsync(IEnumerable<ModifyGuildChannelsParams> args) | |||
=> GuildHelper.ModifyChannelsAsync(this, Discord, args); | |||
public Task ModifyRolesAsync(IEnumerable<ModifyGuildRolesParams> args) | |||
=> GuildHelper.ModifyRolesAsync(this, Discord, args); | |||
public Task LeaveAsync() | |||
=> GuildHelper.LeaveAsync(this, Discord); | |||
//Bans | |||
public Task<IReadOnlyCollection<RestBan>> GetBansAsync() | |||
=> GuildHelper.GetBansAsync(this, Discord); | |||
public Task AddBanAsync(IUser user, int pruneDays = 0) | |||
=> GuildHelper.AddBanAsync(this, Discord, user.Id, pruneDays); | |||
public Task AddBanAsync(ulong userId, int pruneDays = 0) | |||
=> GuildHelper.AddBanAsync(this, Discord, userId, pruneDays); | |||
public Task RemoveBanAsync(IUser user) | |||
=> GuildHelper.RemoveBanAsync(this, Discord, user.Id); | |||
public Task RemoveBanAsync(ulong userId) | |||
=> GuildHelper.RemoveBanAsync(this, Discord, userId); | |||
//Channels | |||
public Task<IReadOnlyCollection<RestGuildChannel>> GetChannelsAsync() | |||
=> GuildHelper.GetChannelsAsync(this, Discord); | |||
public Task<RestGuildChannel> GetChannelAsync(ulong id) | |||
=> GuildHelper.GetChannelAsync(this, Discord, id); | |||
public Task<RestTextChannel> CreateTextChannelAsync(string name) | |||
=> GuildHelper.CreateTextChannelAsync(this, Discord, name); | |||
public Task<RestVoiceChannel> CreateVoiceChannelAsync(string name) | |||
=> GuildHelper.CreateVoiceChannelAsync(this, Discord, name); | |||
//Integrations | |||
public Task<IReadOnlyCollection<RestGuildIntegration>> GetIntegrationsAsync() | |||
=> GuildHelper.GetIntegrationsAsync(this, Discord); | |||
public Task<RestGuildIntegration> CreateIntegrationAsync(ulong id, string type) | |||
=> GuildHelper.CreateIntegrationAsync(this, Discord, id, type); | |||
//Invites | |||
public Task<IReadOnlyCollection<RestInviteMetadata>> GetInvitesAsync() | |||
=> GuildHelper.GetInvitesAsync(this, Discord); | |||
//Roles | |||
public RestRole GetRole(ulong id) | |||
{ | |||
RestRole value; | |||
if (_roles.TryGetValue(id, out value)) | |||
return value; | |||
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) | |||
public async Task<IRole> CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), bool isHoisted = false) | |||
{ | |||
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}"); | |||
} | |||
var role = await GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted); | |||
_roles = _roles.Add(role.Id, role); | |||
return role; | |||
} | |||
bool IUserGuild.IsOwner => OwnerId == Discord.CurrentUser.Id; | |||
GuildPermissions IUserGuild.Permissions => CurrentUser.GuildPermissions; | |||
IAudioClient IGuild.AudioClient => AudioClient; | |||
//Users | |||
public Task<IReadOnlyCollection<RestGuildUser>> GetUsersAsync() | |||
=> GuildHelper.GetUsersAsync(this, Discord); | |||
public Task<RestGuildUser> GetUserAsync(ulong id) | |||
=> GuildHelper.GetUserAsync(this, Discord, id); | |||
public Task<RestGuildUser> GetCurrentUserAsync() | |||
=> GuildHelper.GetUserAsync(this, Discord, Discord.CurrentUser.Id); | |||
public Task<int> PruneUsersAsync(int days = 30, bool simulate = false) | |||
=> GuildHelper.PruneUsersAsync(this, Discord, days, simulate); | |||
//IGuild | |||
bool IGuild.Available => true; | |||
IAudioClient IGuild.AudioClient => null; | |||
IReadOnlyCollection<IGuildUser> IGuild.CachedUsers => ImmutableArray.Create<IGuildUser>(); | |||
IRole IGuild.EveryoneRole => EveryoneRole; | |||
IReadOnlyCollection<IRole> IGuild.Roles => Roles; | |||
async Task<IReadOnlyCollection<IBan>> IGuild.GetBansAsync() | |||
=> await GetBansAsync(); | |||
async Task<IReadOnlyCollection<IGuildChannel>> IGuild.GetChannelsAsync() | |||
=> await GetChannelsAsync(); | |||
async Task<IGuildChannel> IGuild.GetChannelAsync(ulong id) | |||
=> await GetChannelAsync(id); | |||
IGuildChannel IGuild.GetCachedChannel(ulong id) | |||
=> null; | |||
async Task<ITextChannel> IGuild.CreateTextChannelAsync(string name) | |||
=> await CreateTextChannelAsync(name); | |||
async Task<IVoiceChannel> IGuild.CreateVoiceChannelAsync(string name) | |||
=> await CreateVoiceChannelAsync(name); | |||
async Task<IReadOnlyCollection<IGuildIntegration>> IGuild.GetIntegrationsAsync() | |||
=> await GetIntegrationsAsync(); | |||
async Task<IGuildIntegration> IGuild.CreateIntegrationAsync(ulong id, string type) | |||
=> await CreateIntegrationAsync(id, type); | |||
async Task<IReadOnlyCollection<IInviteMetadata>> IGuild.GetInvitesAsync() | |||
=> await GetInvitesAsync(); | |||
IRole IGuild.GetRole(ulong id) | |||
=> GetRole(id); | |||
async Task<IReadOnlyCollection<IGuildUser>> IGuild.GetUsersAsync() | |||
=> await GetUsersAsync(); | |||
async Task<IGuildUser> IGuild.GetUserAsync(ulong id) | |||
=> await GetUserAsync(id); | |||
IGuildUser IGuild.GetCachedUser(ulong id) | |||
=> null; | |||
async Task<IGuildUser> IGuild.GetCurrentUserAsync() | |||
=> await GetCurrentUserAsync(); | |||
Task IGuild.DownloadUsersAsync() { throw new NotSupportedException(); } | |||
} | |||
} |
@@ -1,76 +0,0 @@ | |||
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; | |||
} | |||
} |
@@ -1,13 +0,0 @@ | |||
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(); | |||
} | |||
} |
@@ -1,4 +1,6 @@ | |||
using System; | |||
using Discord.Rest; | |||
using Discord.WebSocket; | |||
using System; | |||
using System.Collections.Concurrent; | |||
using System.Collections.Generic; | |||
using System.Collections.Immutable; | |||
@@ -7,53 +9,52 @@ using System.Threading.Tasks; | |||
namespace Discord.WebSocket | |||
{ | |||
internal class MessageCache : MessageManager | |||
internal class MessageCache | |||
{ | |||
private readonly ConcurrentDictionary<ulong, ISocketMessage> _messages; | |||
private readonly ConcurrentDictionary<ulong, SocketMessage> _messages; | |||
private readonly ConcurrentQueue<ulong> _orderedMessages; | |||
private readonly int _size; | |||
public override IReadOnlyCollection<ISocketMessage> Messages => _messages.ToReadOnlyCollection(); | |||
public IReadOnlyCollection<SocketMessage> Messages => _messages.ToReadOnlyCollection(); | |||
public MessageCache(DiscordSocketClient discord, ISocketMessageChannel channel) | |||
: base(discord, channel) | |||
public MessageCache(DiscordSocketClient discord, IChannel channel) | |||
{ | |||
_size = discord.MessageCacheSize; | |||
_messages = new ConcurrentDictionary<ulong, ISocketMessage>(1, (int)(_size * 1.05)); | |||
_messages = new ConcurrentDictionary<ulong, SocketMessage>(1, (int)(_size * 1.05)); | |||
_orderedMessages = new ConcurrentQueue<ulong>(); | |||
} | |||
public override void Add(ISocketMessage message) | |||
public void Add(SocketMessage message) | |||
{ | |||
if (_messages.TryAdd(message.Id, message)) | |||
{ | |||
_orderedMessages.Enqueue(message.Id); | |||
ulong msgId; | |||
ISocketMessage msg; | |||
SocketMessage msg; | |||
while (_orderedMessages.Count > _size && _orderedMessages.TryDequeue(out msgId)) | |||
_messages.TryRemove(msgId, out msg); | |||
} | |||
} | |||
public override ISocketMessage Remove(ulong id) | |||
public SocketMessage Remove(ulong id) | |||
{ | |||
ISocketMessage msg; | |||
SocketMessage msg; | |||
_messages.TryRemove(id, out msg); | |||
return msg; | |||
} | |||
public override ISocketMessage Get(ulong id) | |||
public SocketMessage Get(ulong id) | |||
{ | |||
ISocketMessage result; | |||
SocketMessage result; | |||
if (_messages.TryGetValue(id, out result)) | |||
return result; | |||
return null; | |||
} | |||
public override IImmutableList<ISocketMessage> GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) | |||
public IImmutableList<SocketMessage> GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) | |||
{ | |||
if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); | |||
if (limit == 0) return ImmutableArray<ISocketMessage>.Empty; | |||
if (limit == 0) return ImmutableArray<SocketMessage>.Empty; | |||
IEnumerable<ulong> cachedMessageIds; | |||
if (fromMessageId == null) | |||
@@ -67,7 +68,7 @@ namespace Discord.WebSocket | |||
.Take(limit) | |||
.Select(x => | |||
{ | |||
ISocketMessage msg; | |||
SocketMessage msg; | |||
if (_messages.TryGetValue(x, out msg)) | |||
return msg; | |||
return null; | |||
@@ -75,13 +76,5 @@ namespace Discord.WebSocket | |||
.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,64 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Collections.Immutable; | |||
using System.Diagnostics; | |||
using System.Threading.Tasks; | |||
using Model = Discord.API.Message; | |||
namespace Discord.WebSocket | |||
{ | |||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
public abstract class SocketMessage : SocketEntity<ulong>, IMessage, IUpdateable | |||
{ | |||
private long _timestampTicks; | |||
public ulong ChannelId { get; } | |||
public SocketUser Author { get; } | |||
public string Content { get; private set; } | |||
public virtual bool IsTTS => false; | |||
public virtual bool IsPinned => false; | |||
public virtual DateTimeOffset? EditedTimestamp => null; | |||
public virtual IReadOnlyCollection<IAttachment> Attachments => ImmutableArray.Create<IAttachment>(); | |||
public virtual IReadOnlyCollection<IEmbed> Embeds => ImmutableArray.Create<IEmbed>(); | |||
public virtual IReadOnlyCollection<ulong> MentionedChannelIds => ImmutableArray.Create<ulong>(); | |||
public virtual IReadOnlyCollection<IRole> MentionedRoles => ImmutableArray.Create<IRole>(); | |||
public virtual IReadOnlyCollection<IUser> MentionedUsers => ImmutableArray.Create<IUser>(); | |||
public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); | |||
internal SocketMessage(DiscordSocketClient discord, ulong id, ulong channelId, SocketUser author) | |||
: base(discord, id) | |||
{ | |||
ChannelId = channelId; | |||
Author = author; | |||
} | |||
internal static SocketMessage Create(DiscordSocketClient discord, SocketUser author, Model model) | |||
{ | |||
if (model.Type == MessageType.Default) | |||
return SocketUserMessage.Create(discord, author, model); | |||
else | |||
return SocketSystemMessage.Create(discord, author, model); | |||
} | |||
internal virtual void Update(Model model) | |||
{ | |||
if (model.Timestamp.IsSpecified) | |||
_timestampTicks = model.Timestamp.Value.UtcTicks; | |||
if (model.Content.IsSpecified) | |||
Content = model.Content.Value; | |||
} | |||
public async Task UpdateAsync() | |||
{ | |||
var model = await Discord.ApiClient.GetChannelMessageAsync(ChannelId, Id).ConfigureAwait(false); | |||
Update(model); | |||
} | |||
//IMessage | |||
IUser IMessage.Author => Author; | |||
MessageType IMessage.Type => MessageType.Default; | |||
} | |||
} |
@@ -3,18 +3,25 @@ using Model = Discord.API.Message; | |||
namespace Discord.WebSocket | |||
{ | |||
internal class SocketSystemMessage : SystemMessage, ISocketMessage | |||
internal class SocketSystemMessage : SocketMessage, ISystemMessage | |||
{ | |||
internal override bool IsAttached => true; | |||
public MessageType Type { get; private set; } | |||
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) | |||
internal SocketSystemMessage(DiscordSocketClient discord, ulong id, ulong channelId, SocketUser author) | |||
: base(discord, id, channelId, author) | |||
{ | |||
} | |||
internal new static SocketSystemMessage Create(DiscordSocketClient discord, SocketUser author, Model model) | |||
{ | |||
var entity = new SocketSystemMessage(discord, model.Id, model.ChannelId, author); | |||
entity.Update(model); | |||
return entity; | |||
} | |||
internal override void Update(Model model) | |||
{ | |||
base.Update(model); | |||
public ISocketMessage Clone() => MemberwiseClone() as ISocketMessage; | |||
Type = model.Type; | |||
} | |||
} | |||
} |
@@ -1,20 +1,133 @@ | |||
using Discord.Rest; | |||
using Discord.API.Rest; | |||
using Discord.Rest; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Collections.Immutable; | |||
using System.Threading.Tasks; | |||
using Model = Discord.API.Message; | |||
namespace Discord.WebSocket | |||
{ | |||
internal class SocketUserMessage : UserMessage, ISocketMessage | |||
internal class SocketUserMessage : SocketMessage, IUserMessage | |||
{ | |||
internal override bool IsAttached => true; | |||
private bool _isMentioningEveryone, _isTTS, _isPinned; | |||
private long? _editedTimestampTicks; | |||
private ImmutableArray<RestAttachment> _attachments; | |||
private ImmutableArray<RestEmbed> _embeds; | |||
private ImmutableArray<ulong> _mentionedChannelIds; | |||
private ImmutableArray<RestRole> _mentionedRoles; | |||
private ImmutableArray<RestUser> _mentionedUsers; | |||
public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; | |||
public new ISocketMessageChannel Channel => base.Channel as ISocketMessageChannel; | |||
public override bool IsTTS => _isTTS; | |||
public override bool IsPinned => _isPinned; | |||
public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks); | |||
public SocketUserMessage(ISocketMessageChannel channel, IUser author, Model model) | |||
: base(channel, author, model) | |||
public override IReadOnlyCollection<IAttachment> Attachments => _attachments; | |||
public override IReadOnlyCollection<IEmbed> Embeds => _embeds; | |||
public override IReadOnlyCollection<ulong> MentionedChannelIds => _mentionedChannelIds; | |||
public override IReadOnlyCollection<IRole> MentionedRoles => _mentionedRoles; | |||
public override IReadOnlyCollection<IUser> MentionedUsers => _mentionedUsers; | |||
internal SocketUserMessage(DiscordSocketClient discord, ulong id, ulong channelId, SocketUser author) | |||
: base(discord, id, channelId, author) | |||
{ | |||
} | |||
internal new static SocketUserMessage Create(DiscordSocketClient discord, SocketUser author, Model model) | |||
{ | |||
var entity = new SocketUserMessage(discord, model.Id, model.ChannelId, author); | |||
entity.Update(model); | |||
return entity; | |||
} | |||
internal override void Update(Model model) | |||
{ | |||
base.Update(model); | |||
if (model.IsTextToSpeech.IsSpecified) | |||
_isTTS = model.IsTextToSpeech.Value; | |||
if (model.Pinned.IsSpecified) | |||
_isPinned = model.Pinned.Value; | |||
if (model.EditedTimestamp.IsSpecified) | |||
_editedTimestampTicks = model.EditedTimestamp.Value?.UtcTicks; | |||
if (model.MentionEveryone.IsSpecified) | |||
_isMentioningEveryone = model.MentionEveryone.Value; | |||
if (model.Attachments.IsSpecified) | |||
{ | |||
var value = model.Attachments.Value; | |||
if (value.Length > 0) | |||
{ | |||
var attachments = ImmutableArray.CreateBuilder<RestAttachment>(value.Length); | |||
for (int i = 0; i < value.Length; i++) | |||
attachments.Add(RestAttachment.Create(value[i])); | |||
_attachments = attachments.ToImmutable(); | |||
} | |||
else | |||
_attachments = ImmutableArray.Create<RestAttachment>(); | |||
} | |||
if (model.Embeds.IsSpecified) | |||
{ | |||
var value = model.Embeds.Value; | |||
if (value.Length > 0) | |||
{ | |||
var embeds = ImmutableArray.CreateBuilder<RestEmbed>(value.Length); | |||
for (int i = 0; i < value.Length; i++) | |||
embeds.Add(RestEmbed.Create(value[i])); | |||
_embeds = embeds.ToImmutable(); | |||
} | |||
else | |||
_embeds = ImmutableArray.Create<RestEmbed>(); | |||
} | |||
ImmutableArray<RestUser> mentions = ImmutableArray.Create<RestUser>(); | |||
if (model.Mentions.IsSpecified) | |||
{ | |||
var value = model.Mentions.Value; | |||
if (value.Length > 0) | |||
{ | |||
var newMentions = ImmutableArray.CreateBuilder<RestUser>(value.Length); | |||
for (int i = 0; i < value.Length; i++) | |||
newMentions.Add(RestUser.Create(Discord, value[i])); | |||
mentions = newMentions.ToImmutable(); | |||
} | |||
} | |||
if (model.Content.IsSpecified) | |||
{ | |||
var text = model.Content.Value; | |||
_mentionedUsers = MentionsHelper.GetUserMentions(text, null, mentions); | |||
_mentionedChannelIds = MentionsHelper.GetChannelMentions(text, null); | |||
_mentionedRoles = MentionsHelper.GetRoleMentions<RestRole>(text, null); | |||
model.Content = text; | |||
} | |||
} | |||
public ISocketMessage Clone() => MemberwiseClone() as ISocketMessage; | |||
public Task ModifyAsync(Action<ModifyMessageParams> func) | |||
=> MessageHelper.ModifyAsync(this, Discord, func); | |||
public Task DeleteAsync() | |||
=> MessageHelper.DeleteAsync(this, Discord); | |||
public Task PinAsync() | |||
=> MessageHelper.PinAsync(this, Discord); | |||
public Task UnpinAsync() | |||
=> MessageHelper.UnpinAsync(this, Discord); | |||
public string Resolve(UserMentionHandling userHandling = UserMentionHandling.Name, ChannelMentionHandling channelHandling = ChannelMentionHandling.Name, | |||
RoleMentionHandling roleHandling = RoleMentionHandling.Name, EveryoneMentionHandling everyoneHandling = EveryoneMentionHandling.Ignore) | |||
=> Resolve(Content, userHandling, channelHandling, roleHandling, everyoneHandling); | |||
public string Resolve(int startIndex, int length, UserMentionHandling userHandling = UserMentionHandling.Name, ChannelMentionHandling channelHandling = ChannelMentionHandling.Name, | |||
RoleMentionHandling roleHandling = RoleMentionHandling.Name, EveryoneMentionHandling everyoneHandling = EveryoneMentionHandling.Ignore) | |||
=> Resolve(Content.Substring(startIndex, length), userHandling, channelHandling, roleHandling, everyoneHandling); | |||
public string Resolve(string text, UserMentionHandling userHandling, ChannelMentionHandling channelHandling, | |||
RoleMentionHandling roleHandling, EveryoneMentionHandling everyoneHandling) | |||
{ | |||
text = MentionsHelper.ResolveUserMentions(text, null, MentionedUsers, userHandling); | |||
text = MentionsHelper.ResolveChannelMentions(text, null, channelHandling); | |||
text = MentionsHelper.ResolveRoleMentions(text, MentionedRoles, roleHandling); | |||
text = MentionsHelper.ResolveEveryoneMentions(text, everyoneHandling); | |||
return text; | |||
} | |||
} | |||
} |
@@ -0,0 +1,19 @@ | |||
using System; | |||
namespace Discord.WebSocket | |||
{ | |||
public abstract class SocketEntity<T> : IEntity<T> | |||
where T : IEquatable<T> | |||
{ | |||
public DiscordSocketClient Discord { get; } | |||
public T Id { get; } | |||
internal SocketEntity(DiscordSocketClient discord, T id) | |||
{ | |||
Discord = discord; | |||
Id = id; | |||
} | |||
IDiscordClient IEntity<T>.Discord => Discord; | |||
} | |||
} |
@@ -1,9 +0,0 @@ | |||
namespace Discord.WebSocket | |||
{ | |||
internal interface ISocketUser : IUser, IEntity<ulong> | |||
{ | |||
SocketGlobalUser User { get; } | |||
ISocketUser Clone(); | |||
} | |||
} |
@@ -1,17 +0,0 @@ | |||
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; | |||
} | |||
} |
@@ -1,46 +0,0 @@ | |||
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})"; | |||
} | |||
} |
@@ -1,61 +1,10 @@ | |||
using Discord.Rest; | |||
using System; | |||
using Model = Discord.API.User; | |||
using PresenceModel = Discord.API.Presence; | |||
namespace Discord.WebSocket | |||
namespace Discord.WebSocket | |||
{ | |||
internal class SocketGlobalUser : User, ISocketUser | |||
internal class SocketGlobalUser : SocketUser | |||
{ | |||
internal override bool IsAttached => true; | |||
private readonly object _lockObj = new object(); | |||
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) | |||
internal SocketGlobalUser(DiscordSocketClient discord, ulong id) | |||
: base(discord, id) | |||
{ | |||
} | |||
public void AddRef() | |||
{ | |||
checked | |||
{ | |||
lock (_lockObj) | |||
_references++; | |||
} | |||
} | |||
public void RemoveRef(DiscordSocketClient discord) | |||
{ | |||
lock (_lockObj) | |||
{ | |||
if (--_references == 0) | |||
discord.RemoveUser(Id); | |||
} | |||
} | |||
public override void Update(Model model) | |||
{ | |||
lock (_lockObj) | |||
base.Update(model, source); | |||
} | |||
public void Update(PresenceModel model) | |||
{ | |||
//Race conditions are okay here. Multiple shards racing already cant guarantee presence in order. | |||
//lock (_lockObj) | |||
//{ | |||
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(); | |||
} | |||
} |
@@ -1,36 +1,30 @@ | |||
using Discord.Rest; | |||
using System.Diagnostics; | |||
using Model = Discord.API.User; | |||
namespace Discord.WebSocket | |||
{ | |||
[DebuggerDisplay("{DebuggerDisplay,nq}")] | |||
internal class SocketGroupUser : GroupUser, ISocketUser | |||
public class SocketGroupUser : SocketUser, IGroupUser | |||
{ | |||
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) | |||
internal SocketGroupUser(DiscordSocketClient discord, ulong id) | |||
: base(discord, id) | |||
{ | |||
} | |||
internal new static SocketGroupUser Create(DiscordSocketClient discord, Model model) | |||
{ | |||
var entity = new SocketGroupUser(discord, model.Id); | |||
entity.Update(model); | |||
return entity; | |||
} | |||
public SocketGroupUser Clone() => MemberwiseClone() as SocketGroupUser; | |||
ISocketUser ISocketUser.Clone() => Clone(); | |||
public override string ToString() => $"{Username}#{Discriminator}"; | |||
private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id})"; | |||
//IVoiceState | |||
bool IVoiceState.IsDeafened => false; | |||
bool IVoiceState.IsMuted => false; | |||
bool IVoiceState.IsSelfDeafened => false; | |||
bool IVoiceState.IsSelfMuted => false; | |||
bool IVoiceState.IsSuppressed => false; | |||
IVoiceChannel IVoiceState.VoiceChannel => null; | |||
string IVoiceState.VoiceSessionId => null; | |||
} | |||
} |
@@ -1,53 +1,74 @@ | |||
using Discord.Rest; | |||
using Discord.API.Rest; | |||
using Discord.Rest; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Collections.Immutable; | |||
using System.Threading.Tasks; | |||
using Model = Discord.API.GuildMember; | |||
using PresenceModel = Discord.API.Presence; | |||
namespace Discord.WebSocket | |||
{ | |||
internal class SocketGuildUser : GuildUser, ISocketUser, IVoiceState | |||
internal class SocketGuildUser : SocketUser, IGuildUser | |||
{ | |||
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) | |||
private long? _joinedAtTicks; | |||
private ImmutableArray<ulong> _roleIds; | |||
public string Nickname { get; private set; } | |||
public ulong GuildId { get; private set; } | |||
public IReadOnlyCollection<ulong> RoleIds => _roleIds; | |||
public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); | |||
internal SocketGuildUser(DiscordSocketClient discord, ulong id) | |||
: base(discord, id) | |||
{ | |||
//Presence = new Presence(null, UserStatus.Offline); | |||
} | |||
public SocketGuildUser(SocketGuild guild, SocketGlobalUser user, PresenceModel model) | |||
: base(guild, user, model) | |||
internal static SocketGuildUser Create(DiscordSocketClient discord, Model model) | |||
{ | |||
var entity = new SocketGuildUser(discord, model.User.Id); | |||
entity.Update(model); | |||
return entity; | |||
} | |||
public override void Update(PresenceModel model) | |||
internal void Update(Model model) | |||
{ | |||
base.Update(model, source); | |||
_joinedAtTicks = model.JoinedAt.UtcTicks; | |||
if (model.Nick.IsSpecified) | |||
Nickname = model.Nick.Value; | |||
UpdateRoles(model.Roles); | |||
} | |||
private void UpdateRoles(ulong[] roleIds) | |||
{ | |||
var roles = ImmutableArray.CreateBuilder<ulong>(roleIds.Length + 1); | |||
roles.Add(GuildId); | |||
for (int i = 0; i < roleIds.Length; i++) | |||
roles.Add(roleIds[i]); | |||
_roleIds = roles.ToImmutable(); | |||
} | |||
var game = model.Game != null ? new Game(model.Game) : null; | |||
//Presence = new Presence(game, model.Status); | |||
public override async Task UpdateAsync() | |||
=> Update(await UserHelper.GetAsync(this, Discord)); | |||
public Task ModifyAsync(Action<ModifyGuildMemberParams> func) | |||
=> UserHelper.ModifyAsync(this, Discord, func); | |||
public Task KickAsync() | |||
=> UserHelper.KickAsync(this, Discord); | |||
User.Update(model, source); | |||
public ChannelPermissions GetPermissions(IGuildChannel channel) | |||
{ | |||
throw new NotImplementedException(); //TODO: Impl | |||
} | |||
IVoiceChannel IVoiceState.VoiceChannel => VoiceState?.VoiceChannel; | |||
//IGuildUser | |||
IReadOnlyCollection<ulong> IGuildUser.RoleIds => RoleIds; | |||
public SocketGuildUser Clone() => MemberwiseClone() as SocketGuildUser; | |||
ISocketUser ISocketUser.Clone() => Clone(); | |||
//IVoiceState | |||
bool IVoiceState.IsDeafened => false; | |||
bool IVoiceState.IsMuted => false; | |||
bool IVoiceState.IsSelfDeafened => false; | |||
bool IVoiceState.IsSelfMuted => false; | |||
bool IVoiceState.IsSuppressed => false; | |||
IVoiceChannel IVoiceState.VoiceChannel => null; | |||
string IVoiceState.VoiceSessionId => null; | |||
} | |||
} |