| @@ -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; | |||
| } | |||
| } | |||