diff --git a/Discord.Net.sln b/Discord.Net.sln index 13e9585f9..48d0dbf93 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -3,7 +3,23 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 VisualStudioVersion = 14.0.25420.1 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net", "src\Discord.Net\Discord.Net.xproj", "{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F7F3E124-93C7-4846-AE87-9CE12BD82859}" + ProjectSection(SolutionItems) = preProject + global.json = global.json + README.md = README.md + EndProjectSection +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net", "src\Discord.Net\Discord.Net.xproj", "{496DB20A-A455-4D01-B6BC-90FE6D7C6B81}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.Core", "src\Discord.Net.Core\Discord.Net.Core.xproj", "{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Impls", "Impls", "{288C363D-A636-4EAE-9AC1-4698B641B26E}" +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}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.Rpc", "src\Discord.Net.Rpc\Discord.Net.Rpc.xproj", "{5688A353-121E-40A1-8BFA-B17B91FB48FB}" EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.Commands", "src\Discord.Net.Commands\Discord.Net.Commands.xproj", "{078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}" EndProject @@ -13,10 +29,26 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Debug|Any CPU.Build.0 = Debug|Any CPU + {496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Release|Any CPU.ActiveCfg = Release|Any CPU + {496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Release|Any CPU.Build.0 = Release|Any CPU {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|Any CPU.Build.0 = Debug|Any CPU {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|Any CPU.ActiveCfg = Release|Any CPU {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|Any CPU.Build.0 = Release|Any CPU + {BFC6DC28-0351-4573-926A-D4124244C04F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BFC6DC28-0351-4573-926A-D4124244C04F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BFC6DC28-0351-4573-926A-D4124244C04F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BFC6DC28-0351-4573-926A-D4124244C04F}.Release|Any CPU.Build.0 = Release|Any CPU + {22AB6C66-536C-4AC2-BBDB-A8BC4EB6B14D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {22AB6C66-536C-4AC2-BBDB-A8BC4EB6B14D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {22AB6C66-536C-4AC2-BBDB-A8BC4EB6B14D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {22AB6C66-536C-4AC2-BBDB-A8BC4EB6B14D}.Release|Any CPU.Build.0 = Release|Any CPU + {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|Any CPU.Build.0 = Release|Any CPU {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Debug|Any CPU.Build.0 = Debug|Any CPU {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -25,4 +57,9 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {BFC6DC28-0351-4573-926A-D4124244C04F} = {288C363D-A636-4EAE-9AC1-4698B641B26E} + {22AB6C66-536C-4AC2-BBDB-A8BC4EB6B14D} = {288C363D-A636-4EAE-9AC1-4698B641B26E} + {5688A353-121E-40A1-8BFA-B17B91FB48FB} = {288C363D-A636-4EAE-9AC1-4698B641B26E} + EndGlobalSection EndGlobal diff --git a/src/Discord.Net.Commands/AssemblyInfo.cs b/src/Discord.Net.Commands/AssemblyInfo.cs new file mode 100644 index 000000000..c6b5997b4 --- /dev/null +++ b/src/Discord.Net.Commands/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Discord.Net.Tests")] \ No newline at end of file diff --git a/src/Discord.Net.Commands/Attributes/CommandAttribute.cs b/src/Discord.Net.Commands/Attributes/CommandAttribute.cs index 014668405..baac75ff9 100644 --- a/src/Discord.Net.Commands/Attributes/CommandAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/CommandAttribute.cs @@ -6,6 +6,7 @@ namespace Discord.Commands public class CommandAttribute : Attribute { public string Text { get; } + public RunMode RunMode { get; set; } = RunMode.Sync; public CommandAttribute() { diff --git a/src/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs b/src/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs new file mode 100644 index 000000000..d6a1c646e --- /dev/null +++ b/src/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace Discord.Commands +{ + [AttributeUsage(AttributeTargets.Class)] + public class DontAutoLoadAttribute : Attribute + { + } +} diff --git a/src/Discord.Net.Commands/Attributes/ModuleAttribute.cs b/src/Discord.Net.Commands/Attributes/ModuleAttribute.cs deleted file mode 100644 index 297cce84c..000000000 --- a/src/Discord.Net.Commands/Attributes/ModuleAttribute.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace Discord.Commands -{ - [AttributeUsage(AttributeTargets.Class)] - public class ModuleAttribute : Attribute - { - public string Prefix { get; } - public bool AutoLoad { get; set; } - - public ModuleAttribute() - { - Prefix = null; - AutoLoad = true; - } - public ModuleAttribute(string prefix) - { - Prefix = prefix; - AutoLoad = true; - } - } -} diff --git a/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs b/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs index ae9457b92..067c8e93b 100644 --- a/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs @@ -6,6 +6,6 @@ namespace Discord.Commands [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = true)] public abstract class PreconditionAttribute : Attribute { - public abstract Task CheckPermissions(IUserMessage context, Command executingCommand, object moduleInstance); + public abstract Task CheckPermissions(CommandContext context, CommandInfo command, IDependencyMap map); } } diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs index 48ada73d9..1cd32e72e 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs @@ -21,7 +21,7 @@ namespace Discord.Commands Contexts = contexts; } - public override Task CheckPermissions(IUserMessage context, Command executingCommand, object moduleInstance) + public override Task CheckPermissions(CommandContext context, CommandInfo command, IDependencyMap map) { bool isValid = false; diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequirePermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequirePermissionAttribute.cs index b3215c419..26aeac5ec 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequirePermissionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequirePermissionAttribute.cs @@ -20,9 +20,9 @@ namespace Discord.Commands GuildPermission = null; } - public override Task CheckPermissions(IUserMessage context, Command executingCommand, object moduleInstance) + public override Task CheckPermissions(CommandContext context, CommandInfo command, IDependencyMap map) { - var guildUser = context.Author as IGuildUser; + var guildUser = context.User as IGuildUser; if (GuildPermission.HasValue) { diff --git a/src/Discord.Net.Commands/CommandContext.cs b/src/Discord.Net.Commands/CommandContext.cs new file mode 100644 index 000000000..555141801 --- /dev/null +++ b/src/Discord.Net.Commands/CommandContext.cs @@ -0,0 +1,18 @@ +namespace Discord.Commands +{ + public struct CommandContext + { + public IGuild Guild { get; } + public IMessageChannel Channel { get; } + public IUser User { get; } + public IUserMessage Message { get; } + + public CommandContext(IGuild guild, IMessageChannel channel, IUser user, IUserMessage msg) + { + Guild = guild; + Channel = channel; + User = user; + Message = msg; + } + } +} diff --git a/src/Discord.Net.Commands/Command.cs b/src/Discord.Net.Commands/CommandInfo.cs similarity index 81% rename from src/Discord.Net.Commands/Command.cs rename to src/Discord.Net.Commands/CommandInfo.cs index b7b7da401..75107a80c 100644 --- a/src/Discord.Net.Commands/Command.cs +++ b/src/Discord.Net.Commands/CommandInfo.cs @@ -10,38 +10,38 @@ using System.Threading.Tasks; namespace Discord.Commands { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class Command + public class CommandInfo { - private static readonly MethodInfo _convertParamsMethod = typeof(Command).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList)); + private static readonly MethodInfo _convertParamsMethod = typeof(CommandInfo).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList)); private static readonly ConcurrentDictionary, object>> _arrayConverters = new ConcurrentDictionary, object>>(); - - private readonly object _instance; - private readonly Func, Task> _action; + + private readonly Func _action; public MethodInfo Source { get; } - public Module Module { get; } + public ModuleInfo Module { get; } public string Name { get; } public string Summary { get; } public string Remarks { get; } public string Text { get; } public int Priority { get; } public bool HasVarArgs { get; } + public RunMode RunMode { get; } public IReadOnlyList Aliases { get; } public IReadOnlyList Parameters { get; } public IReadOnlyList Preconditions { get; } - internal Command(MethodInfo source, Module module, object instance, CommandAttribute attribute, string groupPrefix) + internal CommandInfo(MethodInfo source, ModuleInfo module, CommandAttribute attribute, string groupPrefix) { try { Source = source; Module = module; - _instance = instance; Name = source.Name; if (attribute.Text == null) Text = groupPrefix; + RunMode = attribute.RunMode; if (groupPrefix != "") groupPrefix += " "; @@ -85,18 +85,18 @@ namespace Discord.Commands } } - public async Task CheckPreconditions(IUserMessage context) + public async Task CheckPreconditions(CommandContext context, IDependencyMap map = null) { foreach (PreconditionAttribute precondition in Module.Preconditions) { - var result = await precondition.CheckPermissions(context, this, Module.Instance).ConfigureAwait(false); + var result = await precondition.CheckPermissions(context, this, map).ConfigureAwait(false); if (!result.IsSuccess) return result; } foreach (PreconditionAttribute precondition in Preconditions) { - var result = await precondition.CheckPermissions(context, this, Module.Instance).ConfigureAwait(false); + var result = await precondition.CheckPermissions(context, this, map).ConfigureAwait(false); if (!result.IsSuccess) return result; } @@ -104,7 +104,7 @@ namespace Discord.Commands return PreconditionResult.FromSuccess(); } - public async Task Parse(IUserMessage context, SearchResult searchResult, PreconditionResult? preconditionResult = null) + public async Task Parse(CommandContext context, SearchResult searchResult, PreconditionResult? preconditionResult = null) { if (!searchResult.IsSuccess) return ParseResult.FromError(searchResult); @@ -125,7 +125,7 @@ namespace Discord.Commands return await CommandParser.ParseArgs(this, context, input, 0).ConfigureAwait(false); } - public Task Execute(IUserMessage context, ParseResult parseResult) + public Task Execute(CommandContext context, ParseResult parseResult) { if (!parseResult.IsSuccess) return Task.FromResult(ExecuteResult.FromError(parseResult)); @@ -148,11 +148,23 @@ namespace Discord.Commands return Execute(context, argList, paramList); } - public async Task Execute(IUserMessage context, IEnumerable argList, IEnumerable paramList) + public async Task Execute(CommandContext context, IEnumerable argList, IEnumerable paramList) { try { - await _action.Invoke(context, GenerateArgs(argList, paramList)).ConfigureAwait(false);//Note: This code may need context + var args = GenerateArgs(argList, paramList); + switch (RunMode) + { + case RunMode.Sync: //Always sync + await _action(context, args).ConfigureAwait(false); + break; + case RunMode.Mixed: //Sync until first await statement + var t1 = _action(context, args); + break; + case RunMode.Async: //Always async + var t2 = Task.Run(() => _action(context, args)); + break; + } return ExecuteResult.FromSuccess(); } catch (Exception ex) @@ -169,11 +181,9 @@ namespace Discord.Commands private IReadOnlyList BuildParameters(MethodInfo methodInfo) { var parameters = methodInfo.GetParameters(); - if (parameters.Length == 0 || parameters[0].ParameterType != typeof(IUserMessage)) - throw new InvalidOperationException($"The first parameter of a command must be {nameof(IUserMessage)}."); - var paramBuilder = ImmutableArray.CreateBuilder(parameters.Length - 1); - for (int i = 1; i < parameters.Length; i++) + var paramBuilder = ImmutableArray.CreateBuilder(parameters.Length); + for (int i = 0; i < parameters.Length; i++) { var parameter = parameters[i]; var type = parameter.ParameterType; @@ -209,19 +219,23 @@ namespace Discord.Commands } return paramBuilder.ToImmutable(); } - private Func, Task> BuildAction(MethodInfo methodInfo) + private Func BuildAction(MethodInfo methodInfo) { if (methodInfo.ReturnType != typeof(Task)) throw new InvalidOperationException("Commands must return a non-generic Task."); - return (msg, args) => + return (context, args) => { - object[] newArgs = new object[args.Count + 1]; - newArgs[0] = msg; - for (int i = 0; i < args.Count; i++) - newArgs[i + 1] = args[i]; - var result = methodInfo.Invoke(_instance, newArgs); - return result as Task ?? Task.CompletedTask; + var instance = Module.CreateInstance(); + instance.Context = context; + try + { + return methodInfo.Invoke(instance, args) as Task ?? Task.CompletedTask; + } + finally + { + (instance as IDisposable)?.Dispose(); + } }; } diff --git a/src/Discord.Net.Commands/CommandParameter.cs b/src/Discord.Net.Commands/CommandParameter.cs index f074876cf..1edf42bf1 100644 --- a/src/Discord.Net.Commands/CommandParameter.cs +++ b/src/Discord.Net.Commands/CommandParameter.cs @@ -32,7 +32,7 @@ namespace Discord.Commands DefaultValue = defaultValue; } - public async Task Parse(IUserMessage context, string input) + public async Task Parse(CommandContext context, string input) { return await _reader.Read(context, input).ConfigureAwait(false); } diff --git a/src/Discord.Net.Commands/CommandParser.cs b/src/Discord.Net.Commands/CommandParser.cs index 77e19109a..1808b705d 100644 --- a/src/Discord.Net.Commands/CommandParser.cs +++ b/src/Discord.Net.Commands/CommandParser.cs @@ -13,7 +13,7 @@ namespace Discord.Commands QuotedParameter } - public static async Task ParseArgs(Command command, IUserMessage context, string input, int startPos) + public static async Task ParseArgs(CommandInfo command, CommandContext context, string input, int startPos) { CommandParameter curParam = null; StringBuilder argBuilder = new StringBuilder(input.Length); diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 344950e68..a76c7bdc3 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -11,21 +11,25 @@ namespace Discord.Commands { public class CommandService { + private static readonly TypeInfo _moduleTypeInfo = typeof(ModuleBase).GetTypeInfo(); + private readonly SemaphoreSlim _moduleLock; - private readonly ConcurrentDictionary _modules; + private readonly ConcurrentDictionary _moduleDefs; private readonly ConcurrentDictionary _typeReaders; private readonly CommandMap _map; - public IEnumerable Modules => _modules.Select(x => x.Value); - public IEnumerable Commands => _modules.SelectMany(x => x.Value.Commands); + public IEnumerable Modules => _moduleDefs.Select(x => x.Value); + public IEnumerable Commands => _moduleDefs.SelectMany(x => x.Value.Commands); public CommandService() { _moduleLock = new SemaphoreSlim(1, 1); - _modules = new ConcurrentDictionary(); + _moduleDefs = new ConcurrentDictionary(); _map = new CommandMap(); _typeReaders = new ConcurrentDictionary { + [typeof(bool)] = new SimpleTypeReader(), + [typeof(char)] = new SimpleTypeReader(), [typeof(string)] = new SimpleTypeReader(), [typeof(byte)] = new SimpleTypeReader(), [typeof(sbyte)] = new SimpleTypeReader(), @@ -43,7 +47,6 @@ namespace Discord.Commands [typeof(IMessage)] = new MessageTypeReader(), [typeof(IUserMessage)] = new MessageTypeReader(), - //[typeof(ISystemMessage)] = new MessageTypeReader(), [typeof(IChannel)] = new ChannelTypeReader(), [typeof(IDMChannel)] = new ChannelTypeReader(), [typeof(IGroupChannel)] = new ChannelTypeReader(), @@ -53,120 +56,99 @@ namespace Discord.Commands [typeof(ITextChannel)] = new ChannelTypeReader(), [typeof(IVoiceChannel)] = new ChannelTypeReader(), - //[typeof(IGuild)] = new GuildTypeReader(), - [typeof(IRole)] = new RoleTypeReader(), - //[typeof(IInvite)] = new InviteTypeReader(), - //[typeof(IInviteMetadata)] = new InviteTypeReader(), - [typeof(IUser)] = new UserTypeReader(), [typeof(IGroupUser)] = new UserTypeReader(), [typeof(IGuildUser)] = new UserTypeReader(), }; } - public void AddTypeReader(TypeReader reader) - { - _typeReaders[typeof(T)] = reader; - } - public void AddTypeReader(Type type, TypeReader reader) - { - _typeReaders[type] = reader; - } - internal TypeReader GetTypeReader(Type type) - { - TypeReader reader; - if (_typeReaders.TryGetValue(type, out reader)) - return reader; - return null; - } - - public async Task Load(object moduleInstance) + //Modules + public async Task AddModule(IDependencyMap dependencyMap = null) { await _moduleLock.WaitAsync().ConfigureAwait(false); try { - if (_modules.ContainsKey(moduleInstance.GetType())) - throw new ArgumentException($"This module has already been loaded."); + if (_moduleDefs.ContainsKey(typeof(T))) + throw new ArgumentException($"This module has already been added."); - var typeInfo = moduleInstance.GetType().GetTypeInfo(); - var moduleAttr = typeInfo.GetCustomAttribute(); - if (moduleAttr == null) - throw new ArgumentException($"Modules must be marked with ModuleAttribute."); + var typeInfo = typeof(T).GetTypeInfo(); + if (!_moduleTypeInfo.IsAssignableFrom(typeInfo)) + throw new ArgumentException($"Modules must inherit ModuleBase."); - return LoadInternal(moduleInstance, moduleAttr, typeInfo, null); + return AddModuleInternal(typeInfo, dependencyMap); } finally { _moduleLock.Release(); } } - private Module LoadInternal(object moduleInstance, ModuleAttribute moduleAttr, TypeInfo typeInfo, IDependencyMap dependencyMap) - { - if (_modules.ContainsKey(moduleInstance.GetType())) - return _modules[moduleInstance.GetType()]; - - var loadedModule = new Module(typeInfo, this, moduleInstance, moduleAttr, dependencyMap); - _modules[moduleInstance.GetType()] = loadedModule; - - foreach (var cmd in loadedModule.Commands) - _map.AddCommand(cmd); - - return loadedModule; - } - public async Task> LoadAssembly(Assembly assembly, IDependencyMap dependencyMap = null) + public async Task> AddModules(Assembly assembly, IDependencyMap dependencyMap = null) { - var modules = ImmutableArray.CreateBuilder(); + var moduleDefs = ImmutableArray.CreateBuilder(); await _moduleLock.WaitAsync().ConfigureAwait(false); try { foreach (var type in assembly.ExportedTypes) { - var typeInfo = type.GetTypeInfo(); - var moduleAttr = typeInfo.GetCustomAttribute(); - if (moduleAttr != null && moduleAttr.AutoLoad) + if (!_moduleDefs.ContainsKey(type)) { - var moduleInstance = ReflectionUtils.CreateObject(typeInfo, this, dependencyMap); - modules.Add(LoadInternal(moduleInstance, moduleAttr, typeInfo, dependencyMap)); + var typeInfo = type.GetTypeInfo(); + if (_moduleTypeInfo.IsAssignableFrom(typeInfo)) + { + var dontAutoLoad = typeInfo.GetCustomAttribute(); + if (dontAutoLoad == null) + moduleDefs.Add(AddModuleInternal(typeInfo, dependencyMap)); + } } } - return modules.ToImmutable(); + return moduleDefs.ToImmutable(); } finally { _moduleLock.Release(); } } + private ModuleInfo AddModuleInternal(TypeInfo typeInfo, IDependencyMap dependencyMap) + { + var moduleDef = new ModuleInfo(typeInfo, this, dependencyMap); + _moduleDefs[typeInfo.BaseType] = moduleDef; + + foreach (var cmd in moduleDef.Commands) + _map.AddCommand(cmd); + + return moduleDef; + } - public async Task Unload(Module module) + public async Task RemoveModule(ModuleInfo module) { await _moduleLock.WaitAsync().ConfigureAwait(false); try { - return UnloadInternal(module.Instance); + return RemoveModuleInternal(module.Source.BaseType); } finally { _moduleLock.Release(); } } - public async Task Unload(object moduleInstance) + public async Task RemoveModule() { await _moduleLock.WaitAsync().ConfigureAwait(false); try { - return UnloadInternal(moduleInstance); + return RemoveModuleInternal(typeof(T)); } finally { _moduleLock.Release(); } } - private bool UnloadInternal(object module) + private bool RemoveModuleInternal(Type type) { - Module unloadedModule; - if (_modules.TryRemove(module.GetType(), out unloadedModule)) + ModuleInfo unloadedModule; + if (_moduleDefs.TryRemove(type, out unloadedModule)) { foreach (var cmd in unloadedModule.Commands) _map.RemoveCommand(cmd); @@ -176,8 +158,26 @@ namespace Discord.Commands return false; } - public SearchResult Search(IUserMessage message, int argPos) => Search(message, message.Content.Substring(argPos)); - public SearchResult Search(IUserMessage message, string input) + //Type Readers + public void AddTypeReader(TypeReader reader) + { + _typeReaders[typeof(T)] = reader; + } + public void AddTypeReader(Type type, TypeReader reader) + { + _typeReaders[type] = reader; + } + internal TypeReader GetTypeReader(Type type) + { + TypeReader reader; + if (_typeReaders.TryGetValue(type, out reader)) + return reader; + return null; + } + + //Execution + public SearchResult Search(CommandContext context, int argPos) => Search(context, context.Message.Content.Substring(argPos)); + public SearchResult Search(CommandContext context, string input) { string lowerInput = input.ToLowerInvariant(); var matches = _map.GetCommands(input).OrderByDescending(x => x.Priority).ToImmutableArray(); @@ -188,18 +188,18 @@ namespace Discord.Commands return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command."); } - public Task Execute(IUserMessage message, int argPos, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) - => Execute(message, message.Content.Substring(argPos), multiMatchHandling); - public async Task Execute(IUserMessage message, string input, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) + public Task Execute(CommandContext context, int argPos, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) + => Execute(context, context.Message.Content.Substring(argPos), multiMatchHandling); + public async Task Execute(CommandContext context, string input, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) { - var searchResult = Search(message, input); + var searchResult = Search(context, input); if (!searchResult.IsSuccess) return searchResult; var commands = searchResult.Commands; for (int i = commands.Count - 1; i >= 0; i--) { - var preconditionResult = await commands[i].CheckPreconditions(message); + var preconditionResult = await commands[i].CheckPreconditions(context).ConfigureAwait(false); if (!preconditionResult.IsSuccess) { if (commands.Count == 1) @@ -208,17 +208,17 @@ namespace Discord.Commands continue; } - var parseResult = await commands[i].Parse(message, searchResult, preconditionResult); + var parseResult = await commands[i].Parse(context, searchResult, preconditionResult).ConfigureAwait(false); if (!parseResult.IsSuccess) { if (parseResult.Error == CommandError.MultipleMatches) { - TypeReaderValue[] argList, paramList; + IReadOnlyList argList, paramList; switch (multiMatchHandling) { case MultiMatchHandling.Best: - argList = parseResult.ArgValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToArray(); - paramList = parseResult.ParamValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToArray(); + argList = parseResult.ArgValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); + paramList = parseResult.ParamValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); parseResult = ParseResult.FromSuccess(argList, paramList); break; } @@ -233,7 +233,7 @@ namespace Discord.Commands } } - return await commands[i].Execute(message, parseResult); + return await commands[i].Execute(context, parseResult).ConfigureAwait(false); } return SearchResult.FromError(CommandError.UnknownCommand, "This input does not match any overload."); diff --git a/src/Discord.Net.Commands/Extensions/MessageExtensions.cs b/src/Discord.Net.Commands/Extensions/MessageExtensions.cs index 05da07187..4354cbb88 100644 --- a/src/Discord.Net.Commands/Extensions/MessageExtensions.cs +++ b/src/Discord.Net.Commands/Extensions/MessageExtensions.cs @@ -32,7 +32,7 @@ if (text.Length < endPos + 2 || text[endPos + 1] != ' ') return false; //Must end in "> " ulong userId; - if (!MentionUtils.TryParseUser(text.Substring(0, endPos + 2), out userId)) return false; + if (!MentionUtils.TryParseUser(text.Substring(0, endPos + 1), out userId)) return false; if (userId == user.Id) { argPos = endPos + 2; diff --git a/src/Discord.Net.Commands/Map/CommandMap.cs b/src/Discord.Net.Commands/Map/CommandMap.cs index a5a1f8bc4..e809c1b70 100644 --- a/src/Discord.Net.Commands/Map/CommandMap.cs +++ b/src/Discord.Net.Commands/Map/CommandMap.cs @@ -16,7 +16,7 @@ namespace Discord.Commands _nodes = new ConcurrentDictionary(); } - public void AddCommand(Command command) + public void AddCommand(CommandInfo command) { foreach (string text in command.Aliases) { @@ -35,7 +35,7 @@ namespace Discord.Commands } } } - public void RemoveCommand(Command command) + public void RemoveCommand(CommandInfo command) { foreach (string text in command.Aliases) { @@ -60,7 +60,7 @@ namespace Discord.Commands } } - public IEnumerable GetCommands(string text) + public IEnumerable GetCommands(string text) { int nextSpace = NextWhitespace(text); string name; @@ -76,7 +76,7 @@ namespace Discord.Commands if (_nodes.TryGetValue(name, out nextNode)) return nextNode.GetCommands(text, nextSpace + 1); else - return Enumerable.Empty(); + return Enumerable.Empty(); } } diff --git a/src/Discord.Net.Commands/Map/CommandMapNode.cs b/src/Discord.Net.Commands/Map/CommandMapNode.cs index 5ef42544e..a348a8cb4 100644 --- a/src/Discord.Net.Commands/Map/CommandMapNode.cs +++ b/src/Discord.Net.Commands/Map/CommandMapNode.cs @@ -9,7 +9,7 @@ namespace Discord.Commands private readonly ConcurrentDictionary _nodes; private readonly string _name; private readonly object _lockObj = new object(); - private ImmutableArray _commands; + private ImmutableArray _commands; public bool IsEmpty => _commands.Length == 0 && _nodes.Count == 0; @@ -17,10 +17,10 @@ namespace Discord.Commands { _name = name; _nodes = new ConcurrentDictionary(); - _commands = ImmutableArray.Create(); + _commands = ImmutableArray.Create(); } - public void AddCommand(string text, int index, Command command) + public void AddCommand(string text, int index, CommandInfo command) { int nextSpace = text.IndexOf(' ', index); string name; @@ -41,7 +41,7 @@ namespace Discord.Commands } } } - public void RemoveCommand(string text, int index, Command command) + public void RemoveCommand(string text, int index, CommandInfo command) { int nextSpace = text.IndexOf(' ', index); string name; @@ -68,7 +68,7 @@ namespace Discord.Commands } } - public IEnumerable GetCommands(string text, int index) + public IEnumerable GetCommands(string text, int index) { int nextSpace = text.IndexOf(' ', index); string name; diff --git a/src/Discord.Net.Commands/ModuleBase.cs b/src/Discord.Net.Commands/ModuleBase.cs new file mode 100644 index 000000000..544e37f65 --- /dev/null +++ b/src/Discord.Net.Commands/ModuleBase.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; + +namespace Discord.Commands +{ + public abstract class ModuleBase + { + public IDiscordClient Client { get; internal set; } + public CommandContext Context { get; internal set; } + + protected virtual async Task ReplyAsync(string message, bool isTTS = false, RequestOptions options = null) + { + await Context.Channel.SendMessageAsync(message, isTTS, options).ConfigureAwait(false); + } + } +} diff --git a/src/Discord.Net.Commands/Module.cs b/src/Discord.Net.Commands/ModuleInfo.cs similarity index 62% rename from src/Discord.Net.Commands/Module.cs rename to src/Discord.Net.Commands/ModuleInfo.cs index 9d61ca522..c061e3de4 100644 --- a/src/Discord.Net.Commands/Module.cs +++ b/src/Discord.Net.Commands/ModuleInfo.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Reflection; @@ -6,26 +7,31 @@ using System.Reflection; namespace Discord.Commands { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class Module + public class ModuleInfo { + internal readonly Func _builder; + public TypeInfo Source { get; } public CommandService Service { get; } public string Name { get; } public string Prefix { get; } public string Summary { get; } public string Remarks { get; } - public IEnumerable Commands { get; } - internal object Instance { get; } - + public IEnumerable Commands { get; } public IReadOnlyList Preconditions { get; } - internal Module(TypeInfo source, CommandService service, object instance, ModuleAttribute moduleAttr, IDependencyMap dependencyMap) + internal ModuleInfo(TypeInfo source, CommandService service, IDependencyMap dependencyMap) { Source = source; Service = service; Name = source.Name; - Prefix = moduleAttr.Prefix ?? ""; - Instance = instance; + _builder = ReflectionUtils.CreateBuilder(source, Service, dependencyMap); + + var groupAttr = source.GetCustomAttribute(); + if (groupAttr != null) + Prefix = groupAttr.Prefix; + else + Prefix = ""; var nameAttr = source.GetCustomAttribute(); if (nameAttr != null) @@ -39,20 +45,19 @@ namespace Discord.Commands if (remarksAttr != null) Remarks = remarksAttr.Text; - List commands = new List(); - SearchClass(source, instance, commands, Prefix, dependencyMap); + List commands = new List(); + SearchClass(source, commands, Prefix, dependencyMap); Commands = commands; - Preconditions = BuildPreconditions(); + Preconditions = Source.GetCustomAttributes().ToImmutableArray(); } - - private void SearchClass(TypeInfo parentType, object instance, List commands, string groupPrefix, IDependencyMap dependencyMap) + private void SearchClass(TypeInfo parentType, List commands, string groupPrefix, IDependencyMap dependencyMap) { foreach (var method in parentType.DeclaredMethods) { var cmdAttr = method.GetCustomAttribute(); if (cmdAttr != null) - commands.Add(new Command(method, this, instance, cmdAttr, groupPrefix)); + commands.Add(new CommandInfo(method, this, cmdAttr, groupPrefix)); } foreach (var type in parentType.DeclaredNestedTypes) { @@ -66,15 +71,13 @@ namespace Discord.Commands else nextGroupPrefix = groupAttrib.Prefix ?? type.Name.ToLowerInvariant(); - SearchClass(type, ReflectionUtils.CreateObject(type, Service, dependencyMap), commands, nextGroupPrefix, dependencyMap); + SearchClass(type, commands, nextGroupPrefix, dependencyMap); } } } - private IReadOnlyList BuildPreconditions() - { - return Source.GetCustomAttributes().ToImmutableArray(); - } + internal ModuleBase CreateInstance() + => _builder(); public override string ToString() => Name; private string DebuggerDisplay => Name; diff --git a/src/Discord.Net.Commands/PrimitiveParsers.cs b/src/Discord.Net.Commands/PrimitiveParsers.cs index ac705764e..5e3dcd68a 100644 --- a/src/Discord.Net.Commands/PrimitiveParsers.cs +++ b/src/Discord.Net.Commands/PrimitiveParsers.cs @@ -13,7 +13,7 @@ namespace Discord.Commands static PrimitiveParsers() { var parserBuilder = ImmutableDictionary.CreateBuilder(); - parserBuilder[typeof(string)] = (TryParseDelegate)delegate(string str, out string value) { value = str; return true; }; + parserBuilder[typeof(bool)] = (TryParseDelegate)bool.TryParse; parserBuilder[typeof(sbyte)] = (TryParseDelegate)sbyte.TryParse; parserBuilder[typeof(byte)] = (TryParseDelegate)byte.TryParse; parserBuilder[typeof(short)] = (TryParseDelegate)short.TryParse; @@ -27,6 +27,12 @@ namespace Discord.Commands parserBuilder[typeof(decimal)] = (TryParseDelegate)decimal.TryParse; parserBuilder[typeof(DateTime)] = (TryParseDelegate)DateTime.TryParse; parserBuilder[typeof(DateTimeOffset)] = (TryParseDelegate)DateTimeOffset.TryParse; + parserBuilder[typeof(char)] = (TryParseDelegate)char.TryParse; + parserBuilder[typeof(string)] = (TryParseDelegate)delegate (string str, out string value) + { + value = str; + return true; + }; _parsers = parserBuilder.ToImmutable(); } diff --git a/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs b/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs index 463a9de4f..e05c02abb 100644 --- a/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs @@ -9,23 +9,21 @@ namespace Discord.Commands internal class ChannelTypeReader : TypeReader where T : class, IChannel { - public override async Task Read(IUserMessage context, string input) + public override async Task Read(CommandContext context, string input) { - var guild = (context.Channel as IGuildChannel)?.Guild; - - if (guild != null) + if (context.Guild != null) { var results = new Dictionary(); - var channels = await guild.GetChannelsAsync().ConfigureAwait(false); + var channels = await context.Guild.GetChannelsAsync(CacheMode.CacheOnly).ConfigureAwait(false); ulong id; //By Mention (1.0) if (MentionUtils.TryParseChannel(input, out id)) - AddResult(results, await guild.GetChannelAsync(id).ConfigureAwait(false) as T, 1.00f); + AddResult(results, await context.Guild.GetChannelAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 1.00f); //By Id (0.9) if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id)) - AddResult(results, await guild.GetChannelAsync(id).ConfigureAwait(false) as T, 0.90f); + AddResult(results, await context.Guild.GetChannelAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 0.90f); //By Name (0.7-0.8) foreach (var channel in channels.Where(x => string.Equals(input, x.Name, StringComparison.OrdinalIgnoreCase))) diff --git a/src/Discord.Net.Commands/Readers/EnumTypeReader.cs b/src/Discord.Net.Commands/Readers/EnumTypeReader.cs index 54efa8024..dca845704 100644 --- a/src/Discord.Net.Commands/Readers/EnumTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/EnumTypeReader.cs @@ -42,7 +42,7 @@ namespace Discord.Commands _enumsByValue = byValueBuilder.ToImmutable(); } - public override Task Read(IUserMessage context, string input) + public override Task Read(CommandContext context, string input) { T baseValue; object enumValue; diff --git a/src/Discord.Net.Commands/Readers/MessageTypeReader.cs b/src/Discord.Net.Commands/Readers/MessageTypeReader.cs index b509fc025..57bfc21cd 100644 --- a/src/Discord.Net.Commands/Readers/MessageTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/MessageTypeReader.cs @@ -6,19 +6,19 @@ namespace Discord.Commands internal class MessageTypeReader : TypeReader where T : class, IMessage { - public override Task Read(IUserMessage context, string input) + public override async Task Read(CommandContext context, string input) { ulong id; //By Id (1.0) if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id)) { - var msg = context.Channel.GetCachedMessage(id) as T; + var msg = await context.Channel.GetMessageAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T; if (msg != null) - return Task.FromResult(TypeReaderResult.FromSuccess(msg)); + return TypeReaderResult.FromSuccess(msg); } - return Task.FromResult(TypeReaderResult.FromError(CommandError.ObjectNotFound, "Message not found.")); + return TypeReaderResult.FromError(CommandError.ObjectNotFound, "Message not found."); } } } diff --git a/src/Discord.Net.Commands/Readers/RoleTypeReader.cs b/src/Discord.Net.Commands/Readers/RoleTypeReader.cs index b386aba3c..66b76b7e7 100644 --- a/src/Discord.Net.Commands/Readers/RoleTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/RoleTypeReader.cs @@ -9,23 +9,22 @@ namespace Discord.Commands internal class RoleTypeReader : TypeReader where T : class, IRole { - public override Task Read(IUserMessage context, string input) + public override Task Read(CommandContext context, string input) { - var guild = (context.Channel as IGuildChannel)?.Guild; ulong id; - if (guild != null) + if (context.Guild != null) { var results = new Dictionary(); - var roles = guild.Roles; + var roles = context.Guild.Roles; //By Mention (1.0) if (MentionUtils.TryParseRole(input, out id)) - AddResult(results, guild.GetRole(id) as T, 1.00f); + AddResult(results, context.Guild.GetRole(id) as T, 1.00f); //By Id (0.9) if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id)) - AddResult(results, guild.GetRole(id) as T, 0.90f); + AddResult(results, context.Guild.GetRole(id) as T, 0.90f); //By Name (0.7-0.8) foreach (var role in roles.Where(x => string.Equals(input, x.Name, StringComparison.OrdinalIgnoreCase))) diff --git a/src/Discord.Net.Commands/Readers/SimpleTypeReader.cs b/src/Discord.Net.Commands/Readers/SimpleTypeReader.cs index 72c729a3b..ad939e59d 100644 --- a/src/Discord.Net.Commands/Readers/SimpleTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/SimpleTypeReader.cs @@ -11,7 +11,7 @@ namespace Discord.Commands _tryParse = PrimitiveParsers.Get(); } - public override Task Read(IUserMessage context, string input) + public override Task Read(CommandContext context, string input) { T value; if (_tryParse(input, out value)) diff --git a/src/Discord.Net.Commands/Readers/TypeReader.cs b/src/Discord.Net.Commands/Readers/TypeReader.cs index 4d467ce55..23562cb16 100644 --- a/src/Discord.Net.Commands/Readers/TypeReader.cs +++ b/src/Discord.Net.Commands/Readers/TypeReader.cs @@ -4,6 +4,6 @@ namespace Discord.Commands { public abstract class TypeReader { - public abstract Task Read(IUserMessage context, string input); + public abstract Task Read(CommandContext context, string input); } } diff --git a/src/Discord.Net.Commands/Readers/UserTypeReader.cs b/src/Discord.Net.Commands/Readers/UserTypeReader.cs index 46aaa777c..31bdd0b58 100644 --- a/src/Discord.Net.Commands/Readers/UserTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/UserTypeReader.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Threading.Tasks; @@ -9,33 +10,32 @@ namespace Discord.Commands internal class UserTypeReader : TypeReader where T : class, IUser { - public override async Task Read(IUserMessage context, string input) + public override async Task Read(CommandContext context, string input) { var results = new Dictionary(); - var guild = (context.Channel as IGuildChannel)?.Guild; - IReadOnlyCollection channelUsers = await context.Channel.GetUsersAsync().ConfigureAwait(false); + IReadOnlyCollection channelUsers = (await context.Channel.GetUsersAsync(CacheMode.CacheOnly).Flatten().ConfigureAwait(false)).ToArray(); //TODO: must be a better way? IReadOnlyCollection guildUsers = null; ulong id; - if (guild != null) - guildUsers = await guild.GetUsersAsync().ConfigureAwait(false); + if (context.Guild != null) + guildUsers = await context.Guild.GetUsersAsync(CacheMode.CacheOnly).ConfigureAwait(false); //By Mention (1.0) if (MentionUtils.TryParseUser(input, out id)) { - if (guild != null) - AddResult(results, await guild.GetUserAsync(id).ConfigureAwait(false) as T, 1.00f); + if (context.Guild != null) + AddResult(results, await context.Guild.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 1.00f); else - AddResult(results, await context.Channel.GetUserAsync(id).ConfigureAwait(false) as T, 1.00f); + AddResult(results, await context.Channel.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 1.00f); } //By Id (0.9) if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id)) { - if (guild != null) - AddResult(results, await guild.GetUserAsync(id).ConfigureAwait(false) as T, 0.90f); + if (context.Guild != null) + AddResult(results, await context.Guild.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 0.90f); else - AddResult(results, await context.Channel.GetUserAsync(id).ConfigureAwait(false) as T, 0.90f); + AddResult(results, await context.Channel.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 0.90f); } //By Username + Discriminator (0.7-0.85) @@ -75,7 +75,7 @@ namespace Discord.Commands } if (results.Count > 0) - return TypeReaderResult.FromSuccess(results.Values.ToArray()); + return TypeReaderResult.FromSuccess(results.Values.ToImmutableArray()); return TypeReaderResult.FromError(CommandError.ObjectNotFound, "User not found."); } diff --git a/src/Discord.Net.Commands/ReflectionUtils.cs b/src/Discord.Net.Commands/ReflectionUtils.cs index 4de883731..e84a037ef 100644 --- a/src/Discord.Net.Commands/ReflectionUtils.cs +++ b/src/Discord.Net.Commands/ReflectionUtils.cs @@ -6,7 +6,10 @@ namespace Discord.Commands { internal class ReflectionUtils { - internal static object CreateObject(TypeInfo typeInfo, CommandService service, IDependencyMap map = null) + internal static T CreateObject(TypeInfo typeInfo, CommandService service, IDependencyMap map = null) + => CreateBuilder(typeInfo, service, map)(); + + internal static Func CreateBuilder(TypeInfo typeInfo, CommandService service, IDependencyMap map = null) { var constructors = typeInfo.DeclaredConstructors.Where(x => !x.IsStatic).ToArray(); if (constructors.Length == 0) @@ -14,7 +17,7 @@ namespace Discord.Commands else if (constructors.Length > 1) throw new InvalidOperationException($"Multiple constructors found for \"{typeInfo.FullName}\""); - var constructor = constructors[0]; + var constructor = constructors[0]; ParameterInfo[] parameters = constructor.GetParameters(); object[] args = new object[parameters.Length]; @@ -34,14 +37,17 @@ namespace Discord.Commands args[i] = arg; } - try - { - return constructor.Invoke(args); - } - catch (Exception ex) + return () => { - throw new Exception($"Failed to create \"{typeInfo.FullName}\"", ex); - } + try + { + return (T)constructor.Invoke(args); + } + catch (Exception ex) + { + throw new Exception($"Failed to create \"{typeInfo.FullName}\"", ex); + } + }; } } } diff --git a/src/Discord.Net.Commands/Results/SearchResult.cs b/src/Discord.Net.Commands/Results/SearchResult.cs index 962834c03..17942b61a 100644 --- a/src/Discord.Net.Commands/Results/SearchResult.cs +++ b/src/Discord.Net.Commands/Results/SearchResult.cs @@ -7,14 +7,14 @@ namespace Discord.Commands public struct SearchResult : IResult { public string Text { get; } - public IReadOnlyList Commands { get; } + public IReadOnlyList Commands { get; } public CommandError? Error { get; } public string ErrorReason { get; } public bool IsSuccess => !Error.HasValue; - private SearchResult(string text, IReadOnlyList commands, CommandError? error, string errorReason) + private SearchResult(string text, IReadOnlyList commands, CommandError? error, string errorReason) { Text = text; Commands = commands; @@ -22,7 +22,7 @@ namespace Discord.Commands ErrorReason = errorReason; } - public static SearchResult FromSuccess(string text, IReadOnlyList commands) + public static SearchResult FromSuccess(string text, IReadOnlyList commands) => new SearchResult(text, commands, null, null); public static SearchResult FromError(CommandError error, string reason) => new SearchResult(null, null, error, reason); diff --git a/src/Discord.Net.Commands/Results/TypeReaderResult.cs b/src/Discord.Net.Commands/Results/TypeReaderResult.cs index 20a9c4a22..68bc359c6 100644 --- a/src/Discord.Net.Commands/Results/TypeReaderResult.cs +++ b/src/Discord.Net.Commands/Results/TypeReaderResult.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; -using System.Linq; namespace Discord.Commands { diff --git a/src/Discord.Net.Commands/RunMode.cs b/src/Discord.Net.Commands/RunMode.cs new file mode 100644 index 000000000..0799f825c --- /dev/null +++ b/src/Discord.Net.Commands/RunMode.cs @@ -0,0 +1,9 @@ +namespace Discord.Commands +{ + public enum RunMode + { + Sync, + Mixed, + Async + } +} diff --git a/src/Discord.Net.Commands/project.json b/src/Discord.Net.Commands/project.json index 6505ea3c8..8e5d861d8 100644 --- a/src/Discord.Net.Commands/project.json +++ b/src/Discord.Net.Commands/project.json @@ -13,24 +13,22 @@ } }, - "buildOptions": { - "allowUnsafe": true, - "warningsAsErrors": false, - "xmlDoc": true - }, - "configurations": { "Release": { "buildOptions": { "define": [ "RELEASE" ], "nowarn": [ "CS1573", "CS1591" ], - "optimize": true + "optimize": true, + "warningsAsErrors": true, + "xmlDoc": true } } }, "dependencies": { - "Discord.Net": "1.0.0-*" + "Discord.Net.Core": { + "target": "project" + } }, "frameworks": { diff --git a/src/Discord.Net/API/CDN.cs b/src/Discord.Net.Core/API/CDN.cs similarity index 86% rename from src/Discord.Net/API/CDN.cs rename to src/Discord.Net.Core/API/CDN.cs index 3973344db..e4fcbc8c4 100644 --- a/src/Discord.Net/API/CDN.cs +++ b/src/Discord.Net.Core/API/CDN.cs @@ -1,6 +1,6 @@ namespace Discord.API { - internal static class CDN + public static class CDN { public static string GetApplicationIconUrl(ulong appId, string iconId) => iconId != null ? $"{DiscordConfig.CDNUrl}app-icons/{appId}/{iconId}.jpg" : null; @@ -12,5 +12,7 @@ => splashId != null ? $"{DiscordConfig.CDNUrl}splashes/{guildId}/{splashId}.jpg" : null; public static string GetChannelIconUrl(ulong channelId, string iconId) => iconId != null ? $"{DiscordConfig.CDNUrl}channel-icons/{channelId}/{iconId}.jpg" : null; + public static string GetEmojiUrl(ulong emojiId) + => $"{DiscordConfig.CDNUrl}emojis/{emojiId}.png"; } } diff --git a/src/Discord.Net/API/Common/Application.cs b/src/Discord.Net.Core/API/Common/Application.cs similarity index 85% rename from src/Discord.Net/API/Common/Application.cs rename to src/Discord.Net.Core/API/Common/Application.cs index f9fabe57d..e72c6ce79 100644 --- a/src/Discord.Net/API/Common/Application.cs +++ b/src/Discord.Net.Core/API/Common/Application.cs @@ -11,13 +11,14 @@ namespace Discord.API public string[] RPCOrigins { get; set; } [JsonProperty("name")] public string Name { get; set; } - [JsonProperty("flags"), Int53] - public ulong Flags { get; set; } - [JsonProperty("owner")] - public User Owner { get; set; } [JsonProperty("id")] public ulong Id { get; set; } [JsonProperty("icon")] public string Icon { get; set; } + + [JsonProperty("flags"), Int53] + public Optional Flags { get; set; } + [JsonProperty("owner")] + public Optional Owner { get; set; } } } diff --git a/src/Discord.Net/API/Common/Attachment.cs b/src/Discord.Net.Core/API/Common/Attachment.cs similarity index 100% rename from src/Discord.Net/API/Common/Attachment.cs rename to src/Discord.Net.Core/API/Common/Attachment.cs diff --git a/src/Discord.Net/API/Common/Ban.cs b/src/Discord.Net.Core/API/Common/Ban.cs similarity index 100% rename from src/Discord.Net/API/Common/Ban.cs rename to src/Discord.Net.Core/API/Common/Ban.cs diff --git a/src/Discord.Net/API/Common/Channel.cs b/src/Discord.Net.Core/API/Common/Channel.cs similarity index 100% rename from src/Discord.Net/API/Common/Channel.cs rename to src/Discord.Net.Core/API/Common/Channel.cs diff --git a/src/Discord.Net/API/Common/Connection.cs b/src/Discord.Net.Core/API/Common/Connection.cs similarity index 100% rename from src/Discord.Net/API/Common/Connection.cs rename to src/Discord.Net.Core/API/Common/Connection.cs diff --git a/src/Discord.Net/API/Common/Embed.cs b/src/Discord.Net.Core/API/Common/Embed.cs similarity index 100% rename from src/Discord.Net/API/Common/Embed.cs rename to src/Discord.Net.Core/API/Common/Embed.cs diff --git a/src/Discord.Net/API/Common/EmbedProvider.cs b/src/Discord.Net.Core/API/Common/EmbedProvider.cs similarity index 100% rename from src/Discord.Net/API/Common/EmbedProvider.cs rename to src/Discord.Net.Core/API/Common/EmbedProvider.cs diff --git a/src/Discord.Net/API/Common/EmbedThumbnail.cs b/src/Discord.Net.Core/API/Common/EmbedThumbnail.cs similarity index 100% rename from src/Discord.Net/API/Common/EmbedThumbnail.cs rename to src/Discord.Net.Core/API/Common/EmbedThumbnail.cs diff --git a/src/Discord.Net/API/Common/Emoji.cs b/src/Discord.Net.Core/API/Common/Emoji.cs similarity index 100% rename from src/Discord.Net/API/Common/Emoji.cs rename to src/Discord.Net.Core/API/Common/Emoji.cs diff --git a/src/Discord.Net/API/Common/Game.cs b/src/Discord.Net.Core/API/Common/Game.cs similarity index 100% rename from src/Discord.Net/API/Common/Game.cs rename to src/Discord.Net.Core/API/Common/Game.cs diff --git a/src/Discord.Net/API/Common/Guild.cs b/src/Discord.Net.Core/API/Common/Guild.cs similarity index 100% rename from src/Discord.Net/API/Common/Guild.cs rename to src/Discord.Net.Core/API/Common/Guild.cs diff --git a/src/Discord.Net/API/Common/GuildEmbed.cs b/src/Discord.Net.Core/API/Common/GuildEmbed.cs similarity index 100% rename from src/Discord.Net/API/Common/GuildEmbed.cs rename to src/Discord.Net.Core/API/Common/GuildEmbed.cs diff --git a/src/Discord.Net/API/Common/GuildMember.cs b/src/Discord.Net.Core/API/Common/GuildMember.cs similarity index 100% rename from src/Discord.Net/API/Common/GuildMember.cs rename to src/Discord.Net.Core/API/Common/GuildMember.cs diff --git a/src/Discord.Net/API/Common/Integration.cs b/src/Discord.Net.Core/API/Common/Integration.cs similarity index 100% rename from src/Discord.Net/API/Common/Integration.cs rename to src/Discord.Net.Core/API/Common/Integration.cs diff --git a/src/Discord.Net/API/Common/IntegrationAccount.cs b/src/Discord.Net.Core/API/Common/IntegrationAccount.cs similarity index 100% rename from src/Discord.Net/API/Common/IntegrationAccount.cs rename to src/Discord.Net.Core/API/Common/IntegrationAccount.cs diff --git a/src/Discord.Net/API/Common/Invite.cs b/src/Discord.Net.Core/API/Common/Invite.cs similarity index 100% rename from src/Discord.Net/API/Common/Invite.cs rename to src/Discord.Net.Core/API/Common/Invite.cs diff --git a/src/Discord.Net/API/Common/InviteChannel.cs b/src/Discord.Net.Core/API/Common/InviteChannel.cs similarity index 100% rename from src/Discord.Net/API/Common/InviteChannel.cs rename to src/Discord.Net.Core/API/Common/InviteChannel.cs diff --git a/src/Discord.Net/API/Common/InviteGuild.cs b/src/Discord.Net.Core/API/Common/InviteGuild.cs similarity index 100% rename from src/Discord.Net/API/Common/InviteGuild.cs rename to src/Discord.Net.Core/API/Common/InviteGuild.cs diff --git a/src/Discord.Net/API/Common/InviteMetadata.cs b/src/Discord.Net.Core/API/Common/InviteMetadata.cs similarity index 100% rename from src/Discord.Net/API/Common/InviteMetadata.cs rename to src/Discord.Net.Core/API/Common/InviteMetadata.cs diff --git a/src/Discord.Net/API/Common/Message.cs b/src/Discord.Net.Core/API/Common/Message.cs similarity index 82% rename from src/Discord.Net/API/Common/Message.cs rename to src/Discord.Net.Core/API/Common/Message.cs index e22c26028..52de6f97b 100644 --- a/src/Discord.Net/API/Common/Message.cs +++ b/src/Discord.Net.Core/API/Common/Message.cs @@ -12,6 +12,8 @@ namespace Discord.API public MessageType Type { get; set; } [JsonProperty("channel_id")] public ulong ChannelId { get; set; } + [JsonProperty("webhook_id")] + public Optional WebhookId { get; set; } [JsonProperty("author")] public Optional Author { get; set; } [JsonProperty("content")] @@ -25,7 +27,9 @@ namespace Discord.API [JsonProperty("mention_everyone")] public Optional MentionEveryone { get; set; } [JsonProperty("mentions")] - public Optional Mentions { get; set; } + public Optional[]> UserMentions { get; set; } + [JsonProperty("mention_roles")] + public Optional RoleMentions { get; set; } [JsonProperty("attachments")] public Optional Attachments { get; set; } [JsonProperty("embeds")] diff --git a/src/Discord.Net/API/Common/Overwrite.cs b/src/Discord.Net.Core/API/Common/Overwrite.cs similarity index 100% rename from src/Discord.Net/API/Common/Overwrite.cs rename to src/Discord.Net.Core/API/Common/Overwrite.cs diff --git a/src/Discord.Net/API/Common/Presence.cs b/src/Discord.Net.Core/API/Common/Presence.cs similarity index 100% rename from src/Discord.Net/API/Common/Presence.cs rename to src/Discord.Net.Core/API/Common/Presence.cs diff --git a/src/Discord.Net/API/Common/ReadState.cs b/src/Discord.Net.Core/API/Common/ReadState.cs similarity index 100% rename from src/Discord.Net/API/Common/ReadState.cs rename to src/Discord.Net.Core/API/Common/ReadState.cs diff --git a/src/Discord.Net/API/Common/Relationship.cs b/src/Discord.Net.Core/API/Common/Relationship.cs similarity index 100% rename from src/Discord.Net/API/Common/Relationship.cs rename to src/Discord.Net.Core/API/Common/Relationship.cs diff --git a/src/Discord.Net/API/Common/RelationshipType.cs b/src/Discord.Net.Core/API/Common/RelationshipType.cs similarity index 71% rename from src/Discord.Net/API/Common/RelationshipType.cs rename to src/Discord.Net.Core/API/Common/RelationshipType.cs index 7cd1efd2a..94f0f73b4 100644 --- a/src/Discord.Net/API/Common/RelationshipType.cs +++ b/src/Discord.Net.Core/API/Common/RelationshipType.cs @@ -5,6 +5,7 @@ namespace Discord.API { Friend = 1, Blocked = 2, - Pending = 4 + IncomingPending = 3, + OutgoingPending = 4 } } diff --git a/src/Discord.Net/API/Common/Role.cs b/src/Discord.Net.Core/API/Common/Role.cs similarity index 88% rename from src/Discord.Net/API/Common/Role.cs rename to src/Discord.Net.Core/API/Common/Role.cs index 7442d8c75..6a3659489 100644 --- a/src/Discord.Net/API/Common/Role.cs +++ b/src/Discord.Net.Core/API/Common/Role.cs @@ -13,6 +13,8 @@ namespace Discord.API public uint Color { get; set; } [JsonProperty("hoist")] public bool Hoist { get; set; } + [JsonProperty("mentionable")] + public bool Mentionable { get; set; } [JsonProperty("position")] public int Position { get; set; } [JsonProperty("permissions"), Int53] diff --git a/src/Discord.Net/API/Common/User.cs b/src/Discord.Net.Core/API/Common/User.cs similarity index 100% rename from src/Discord.Net/API/Common/User.cs rename to src/Discord.Net.Core/API/Common/User.cs diff --git a/src/Discord.Net/API/Common/UserGuild.cs b/src/Discord.Net.Core/API/Common/UserGuild.cs similarity index 100% rename from src/Discord.Net/API/Common/UserGuild.cs rename to src/Discord.Net.Core/API/Common/UserGuild.cs diff --git a/src/Discord.Net/API/Common/VoiceRegion.cs b/src/Discord.Net.Core/API/Common/VoiceRegion.cs similarity index 100% rename from src/Discord.Net/API/Common/VoiceRegion.cs rename to src/Discord.Net.Core/API/Common/VoiceRegion.cs diff --git a/src/Discord.Net/API/Common/VoiceState.cs b/src/Discord.Net.Core/API/Common/VoiceState.cs similarity index 100% rename from src/Discord.Net/API/Common/VoiceState.cs rename to src/Discord.Net.Core/API/Common/VoiceState.cs diff --git a/src/Discord.Net.Core/API/DiscordRestApiClient.cs b/src/Discord.Net.Core/API/DiscordRestApiClient.cs new file mode 100644 index 000000000..93dbabef4 --- /dev/null +++ b/src/Discord.Net.Core/API/DiscordRestApiClient.cs @@ -0,0 +1,1140 @@ +#pragma warning disable CS1591 +using Discord.API.Rest; +using Discord.Net; +using Discord.Net.Converters; +using Discord.Net.Queue; +using Discord.Net.Rest; +using Newtonsoft.Json; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using System.Net; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.API +{ + public class DiscordRestApiClient : IDisposable + { + private static readonly ConcurrentDictionary> _bucketIdGenerators = new ConcurrentDictionary>(); + + public event Func SentRequest { add { _sentRequestEvent.Add(value); } remove { _sentRequestEvent.Remove(value); } } + private readonly AsyncEvent> _sentRequestEvent = new AsyncEvent>(); + + protected readonly JsonSerializer _serializer; + protected readonly SemaphoreSlim _stateLock; + private readonly RestClientProvider _restClientProvider; + private readonly string _userAgent; + + protected string _authToken; + protected bool _isDisposed; + private CancellationTokenSource _loginCancelToken; + private IRestClient _restClient; + + public LoginState LoginState { get; private set; } + public TokenType AuthTokenType { get; private set; } + public User CurrentUser { get; private set; } + public RequestQueue RequestQueue { get; private set; } + internal bool FetchCurrentUser { get; set; } + + public DiscordRestApiClient(RestClientProvider restClientProvider, string userAgent, JsonSerializer serializer = null, RequestQueue requestQueue = null) + { + _restClientProvider = restClientProvider; + _userAgent = userAgent; + _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; + RequestQueue = requestQueue; + FetchCurrentUser = true; + + _stateLock = new SemaphoreSlim(1, 1); + + SetBaseUrl(DiscordConfig.ClientAPIUrl); + } + internal void SetBaseUrl(string baseUrl) + { + _restClient = _restClientProvider(baseUrl); + _restClient.SetHeader("accept", "*/*"); + _restClient.SetHeader("user-agent", _userAgent); + _restClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, _authToken)); + } + internal static string GetPrefixedToken(TokenType tokenType, string token) + { + switch (tokenType) + { + case TokenType.Bot: + return $"Bot {token}"; + case TokenType.Bearer: + return $"Bearer {token}"; + case TokenType.User: + return token; + default: + throw new ArgumentException("Unknown OAuth token type", nameof(tokenType)); + } + } + internal virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + _loginCancelToken?.Dispose(); + (_restClient as IDisposable)?.Dispose(); + } + _isDisposed = true; + } + } + public void Dispose() => Dispose(true); + + public async Task LoginAsync(TokenType tokenType, string token, RequestOptions options = null) + { + await _stateLock.WaitAsync().ConfigureAwait(false); + try + { + await LoginInternalAsync(tokenType, token, options).ConfigureAwait(false); + } + finally { _stateLock.Release(); } + } + private async Task LoginInternalAsync(TokenType tokenType, string token, RequestOptions options = null) + { + if (LoginState != LoginState.LoggedOut) + await LogoutInternalAsync().ConfigureAwait(false); + LoginState = LoginState.LoggingIn; + + try + { + _loginCancelToken = new CancellationTokenSource(); + + AuthTokenType = TokenType.User; + _authToken = null; + await RequestQueue.SetCancelTokenAsync(_loginCancelToken.Token).ConfigureAwait(false); + _restClient.SetCancelToken(_loginCancelToken.Token); + + AuthTokenType = tokenType; + RequestQueue.TokenType = tokenType; + _authToken = token; + _restClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, _authToken)); + + if (FetchCurrentUser) + CurrentUser = await GetMyUserAsync(new RequestOptions { IgnoreState = true }).ConfigureAwait(false); + + LoginState = LoginState.LoggedIn; + } + catch (Exception) + { + await LogoutInternalAsync().ConfigureAwait(false); + throw; + } + } + + public async Task LogoutAsync() + { + await _stateLock.WaitAsync().ConfigureAwait(false); + try + { + await LogoutInternalAsync().ConfigureAwait(false); + } + finally { _stateLock.Release(); } + } + private async Task LogoutInternalAsync() + { + //An exception here will lock the client into the unusable LoggingOut state, but that's probably fine since our client is in an undefined state too. + if (LoginState == LoginState.LoggedOut) return; + LoginState = LoginState.LoggingOut; + + try { _loginCancelToken?.Cancel(false); } + catch { } + + await DisconnectInternalAsync().ConfigureAwait(false); + await RequestQueue.ClearAsync().ConfigureAwait(false); + + await RequestQueue.SetCancelTokenAsync(CancellationToken.None).ConfigureAwait(false); + _restClient.SetCancelToken(CancellationToken.None); + + CurrentUser = null; + LoginState = LoginState.LoggedOut; + } + + internal virtual Task ConnectInternalAsync() => Task.CompletedTask; + internal virtual Task DisconnectInternalAsync() => Task.CompletedTask; + + //Core + public async Task SendAsync(string method, string endpoint, string bucketId, RequestOptions options) + { + options.HeaderOnly = true; + var request = new RestRequest(_restClient, method, endpoint, bucketId, options); + await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); + } + public async Task SendJsonAsync(string method, string endpoint, string bucketId, object payload, RequestOptions options) + { + options.HeaderOnly = true; + var json = payload != null ? SerializeJson(payload) : null; + var request = new JsonRestRequest(_restClient, method, endpoint, bucketId, json, options); + await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); + } + public async Task SendMultipartAsync(string method, string endpoint, string bucketId, IReadOnlyDictionary multipartArgs, RequestOptions options) + { + options.HeaderOnly = true; + var request = new MultipartRestRequest(_restClient, method, endpoint, bucketId, multipartArgs, options); + await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); + } + public async Task SendAsync(string method, string endpoint, string bucketId, RequestOptions options) where TResponse : class + { + var request = new RestRequest(_restClient, method, endpoint, bucketId, options); + return DeserializeJson(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false)); + } + public async Task SendJsonAsync(string method, string endpoint, string bucketId, object payload, RequestOptions options) where TResponse : class + { + var json = payload != null ? SerializeJson(payload) : null; + var request = new JsonRestRequest(_restClient, method, endpoint, bucketId, json, options); + return DeserializeJson(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false)); + } + public async Task SendMultipartAsync(string method, string endpoint, string bucketId, IReadOnlyDictionary multipartArgs, RequestOptions options) + { + var request = new MultipartRestRequest(_restClient, method, endpoint, bucketId, multipartArgs, options); + return DeserializeJson(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false)); + } + + internal Task SendAsync(string method, Expression> endpointExpr, BucketIds ids, + RequestOptions options, [CallerMemberName] string funcName = null) + => SendAsync(method, GetEndpoint(endpointExpr), GetBucketId(ids, endpointExpr, funcName), options); + internal Task SendJsonAsync(string method, Expression> endpointExpr, object payload, BucketIds ids, + RequestOptions options, [CallerMemberName] string funcName = null) + => SendJsonAsync(method, GetEndpoint(endpointExpr), GetBucketId(ids, endpointExpr, funcName), payload, options); + internal Task SendMultipartAsync(string method, Expression> endpointExpr, IReadOnlyDictionary multipartArgs, BucketIds ids, + RequestOptions options, [CallerMemberName] string funcName = null) + => SendMultipartAsync(method, GetEndpoint(endpointExpr), GetBucketId(ids, endpointExpr, funcName), multipartArgs, options); + internal Task SendAsync(string method, Expression> endpointExpr, BucketIds ids, + RequestOptions options, [CallerMemberName] string funcName = null) where TResponse : class + => SendAsync(method, GetEndpoint(endpointExpr), GetBucketId(ids, endpointExpr, funcName), options); + internal Task SendJsonAsync(string method, Expression> endpointExpr, object payload, BucketIds ids, + RequestOptions options, [CallerMemberName] string funcName = null) where TResponse : class + => SendJsonAsync(method, GetEndpoint(endpointExpr), GetBucketId(ids, endpointExpr, funcName), payload, options); + internal Task SendMultipartAsync(string method, Expression> endpointExpr, IReadOnlyDictionary multipartArgs, BucketIds ids, + RequestOptions options, [CallerMemberName] string funcName = null) + => SendMultipartAsync(method, GetEndpoint(endpointExpr), GetBucketId(ids, endpointExpr, funcName), multipartArgs, options); + + private async Task SendInternalAsync(string method, string endpoint, RestRequest request) + { + if (!request.Options.IgnoreState) + CheckState(); + + var stopwatch = Stopwatch.StartNew(); + var responseStream = await RequestQueue.SendAsync(request).ConfigureAwait(false); + stopwatch.Stop(); + + double milliseconds = ToMilliseconds(stopwatch); + await _sentRequestEvent.InvokeAsync(method, endpoint, milliseconds).ConfigureAwait(false); + + return responseStream; + } + + //Auth + public async Task ValidateTokenAsync(RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + await SendAsync("GET", () => "auth/login", new BucketIds(), options: options).ConfigureAwait(false); + } + + //Channels + public async Task GetChannelAsync(ulong channelId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + options = RequestOptions.CreateOrClone(options); + + try + { + var ids = new BucketIds(channelId: channelId); + return await SendAsync("GET", () => $"channels/{channelId}", ids, options: options).ConfigureAwait(false); + } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } + } + public async Task GetChannelAsync(ulong guildId, ulong channelId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + options = RequestOptions.CreateOrClone(options); + + try + { + var ids = new BucketIds(channelId: channelId); + var model = await SendAsync("GET", () => $"channels/{channelId}", ids, options: options).ConfigureAwait(false); + if (!model.GuildId.IsSpecified || model.GuildId.Value != guildId) + return null; + return model; + } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } + } + public async Task> GetGuildChannelsAsync(ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendAsync>("GET", () => $"guilds/{guildId}/channels", ids, options: options).ConfigureAwait(false); + } + public async Task CreateGuildChannelAsync(ulong guildId, CreateGuildChannelParams args, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.GreaterThan(args.Bitrate, 0, nameof(args.Bitrate)); + Preconditions.NotNullOrWhitespace(args.Name, nameof(args.Name)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendJsonAsync("POST", () => $"guilds/{guildId}/channels", args, ids, options: options).ConfigureAwait(false); + } + public async Task DeleteChannelAsync(ulong channelId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + return await SendAsync("DELETE", () => $"channels/{channelId}", ids, options: options).ConfigureAwait(false); + } + public async Task ModifyGuildChannelAsync(ulong channelId, ModifyGuildChannelParams args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.AtLeast(args.Position, 0, nameof(args.Position)); + Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + return await SendJsonAsync("PATCH", () => $"channels/{channelId}", args, ids, options: options).ConfigureAwait(false); + } + public async Task ModifyGuildChannelAsync(ulong channelId, ModifyTextChannelParams args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.AtLeast(args.Position, 0, nameof(args.Position)); + Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + return await SendJsonAsync("PATCH", () => $"channels/{channelId}", args, ids, options: options).ConfigureAwait(false); + } + public async Task ModifyGuildChannelAsync(ulong channelId, ModifyVoiceChannelParams args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.GreaterThan(args.Bitrate, 0, nameof(args.Bitrate)); + Preconditions.AtLeast(args.UserLimit, 0, nameof(args.Bitrate)); + Preconditions.AtLeast(args.Position, 0, nameof(args.Position)); + Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + return await SendJsonAsync("PATCH", () => $"channels/{channelId}", args, ids, options: options).ConfigureAwait(false); + } + public async Task ModifyGuildChannelsAsync(ulong guildId, IEnumerable args, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotNull(args, nameof(args)); + options = RequestOptions.CreateOrClone(options); + + var channels = args.ToArray(); + switch (channels.Length) + { + case 0: + return; + case 1: + await ModifyGuildChannelAsync(channels[0].Id, new ModifyGuildChannelParams { Position = channels[0].Position }).ConfigureAwait(false); + break; + default: + var ids = new BucketIds(guildId: guildId); + await SendJsonAsync("PATCH", () => $"guilds/{guildId}/channels", channels, ids, options: options).ConfigureAwait(false); + break; + } + } + + //Channel Messages + public async Task GetChannelMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + options = RequestOptions.CreateOrClone(options); + + try + { + var ids = new BucketIds(channelId: channelId); + return await SendAsync("GET", () => $"channels/{channelId}/messages/{messageId}", ids, options: options).ConfigureAwait(false); + } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } + } + public async Task> GetChannelMessagesAsync(ulong channelId, GetChannelMessagesParams args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.AtLeast(args.Limit, 0, nameof(args.Limit)); + Preconditions.AtMost(args.Limit, DiscordConfig.MaxMessagesPerBatch, nameof(args.Limit)); + options = RequestOptions.CreateOrClone(options); + + int limit = args.Limit.GetValueOrDefault(DiscordConfig.MaxMessagesPerBatch); + ulong? relativeId = args.RelativeMessageId.IsSpecified ? args.RelativeMessageId.Value : (ulong?)null; + string relativeDir; + + switch (args.RelativeDirection.GetValueOrDefault(Direction.Before)) + { + case Direction.Before: + default: + relativeDir = "before"; + break; + case Direction.After: + relativeDir = "after"; + break; + case Direction.Around: + relativeDir = "around"; + break; + } + + var ids = new BucketIds(channelId: channelId); + Expression> endpoint; + if (relativeId != null) + endpoint = () => $"channels/{channelId}/messages?limit={limit}&{relativeDir}={relativeId}"; + else + endpoint = () =>$"channels/{channelId}/messages?limit={limit}"; + return await SendAsync>("GET", endpoint, ids, options: options).ConfigureAwait(false); + } + public async Task CreateMessageAsync(ulong channelId, CreateMessageParams args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); + if (args.Content.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + return await SendJsonAsync("POST", () => $"channels/{channelId}/messages", args, ids, options: options).ConfigureAwait(false); + } + public async Task UploadFileAsync(ulong channelId, UploadFileParams args, RequestOptions options = null) + { + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + options = RequestOptions.CreateOrClone(options); + + if (args.Content.GetValueOrDefault(null) == null) + args.Content = ""; + else if (args.Content.IsSpecified) + { + if (args.Content.Value == null) + args.Content = ""; + if (args.Content.Value?.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); + } + + var ids = new BucketIds(channelId: channelId); + return await SendMultipartAsync("POST", () => $"channels/{channelId}/messages", args.ToDictionary(), ids, options: options).ConfigureAwait(false); + } + public async Task DeleteMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + await SendAsync("DELETE", () => $"channels/{channelId}/messages/{messageId}", ids, options: options).ConfigureAwait(false); + } + public async Task DeleteMessagesAsync(ulong channelId, DeleteMessagesParams args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotNull(args.MessageIds, nameof(args.MessageIds)); + Preconditions.AtMost(args.MessageIds.Length, 100, nameof(args.MessageIds.Length)); + options = RequestOptions.CreateOrClone(options); + + switch (args.MessageIds.Length) + { + case 0: + return; + case 1: + await DeleteMessageAsync(channelId, args.MessageIds[0]).ConfigureAwait(false); + break; + default: + var ids = new BucketIds(channelId: channelId); + await SendJsonAsync("POST", () => $"channels/{channelId}/messages/bulk-delete", args, ids, options: options).ConfigureAwait(false); + break; + } + } + public async Task ModifyMessageAsync(ulong channelId, ulong messageId, ModifyMessageParams args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + Preconditions.NotNull(args, nameof(args)); + if (args.Content.IsSpecified) + { + Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); + if (args.Content.Value.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); + } + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + return await SendJsonAsync("PATCH", () => $"channels/{channelId}/messages/{messageId}", args, ids, options: options).ConfigureAwait(false); + } + public async Task AckMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + await SendAsync("POST", () => $"channels/{channelId}/messages/{messageId}/ack", ids, options: options).ConfigureAwait(false); + } + public async Task TriggerTypingIndicatorAsync(ulong channelId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + await SendAsync("POST", () => $"channels/{channelId}/typing", ids, options: options).ConfigureAwait(false); + } + + //Channel Permissions + public async Task ModifyChannelPermissionsAsync(ulong channelId, ulong targetId, ModifyChannelPermissionsParams args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(targetId, 0, nameof(targetId)); + Preconditions.NotNull(args, nameof(args)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + await SendJsonAsync("PUT", () => $"channels/{channelId}/permissions/{targetId}", args, ids, options: options).ConfigureAwait(false); + } + public async Task DeleteChannelPermissionAsync(ulong channelId, ulong targetId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(targetId, 0, nameof(targetId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + await SendAsync("DELETE", () => $"channels/{channelId}/permissions/{targetId}", ids, options: options).ConfigureAwait(false); + } + + //Channel Pins + public async Task AddPinAsync(ulong channelId, ulong messageId, RequestOptions options = null) + { + Preconditions.GreaterThan(channelId, 0, nameof(channelId)); + Preconditions.GreaterThan(messageId, 0, nameof(messageId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + await SendAsync("PUT", () => $"channels/{channelId}/pins/{messageId}", ids, options: options).ConfigureAwait(false); + + } + public async Task RemovePinAsync(ulong channelId, ulong messageId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + await SendAsync("DELETE", () => $"channels/{channelId}/pins/{messageId}", ids, options: options).ConfigureAwait(false); + } + public async Task> GetPinsAsync(ulong channelId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + return await SendAsync>("GET", () => $"channels/{channelId}/pins", ids, options: options).ConfigureAwait(false); + } + + //Channel Recipients + public async Task AddGroupRecipientAsync(ulong channelId, ulong userId, RequestOptions options = null) + { + Preconditions.GreaterThan(channelId, 0, nameof(channelId)); + Preconditions.GreaterThan(userId, 0, nameof(userId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + await SendAsync("PUT", () => $"channels/{channelId}/recipients/{userId}", ids, options: options).ConfigureAwait(false); + + } + public async Task RemoveGroupRecipientAsync(ulong channelId, ulong userId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(userId, 0, nameof(userId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + await SendAsync("DELETE", () => $"channels/{channelId}/recipients/{userId}", ids, options: options).ConfigureAwait(false); + } + + //Guilds + public async Task GetGuildAsync(ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + options = RequestOptions.CreateOrClone(options); + + try + { + var ids = new BucketIds(guildId: guildId); + return await SendAsync("GET", () => $"guilds/{guildId}", ids, options: options).ConfigureAwait(false); + } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } + } + public async Task CreateGuildAsync(CreateGuildParams args, RequestOptions options = null) + { + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotNullOrWhitespace(args.Name, nameof(args.Name)); + Preconditions.NotNullOrWhitespace(args.RegionId, nameof(args.RegionId)); + options = RequestOptions.CreateOrClone(options); + + return await SendJsonAsync("POST", () => "guilds", args, new BucketIds(), options: options).ConfigureAwait(false); + } + public async Task DeleteGuildAsync(ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendAsync("DELETE", () => $"guilds/{guildId}", ids, options: options).ConfigureAwait(false); + } + public async Task LeaveGuildAsync(ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendAsync("DELETE", () => $"users/@me/guilds/{guildId}", ids, options: options).ConfigureAwait(false); + } + public async Task ModifyGuildAsync(ulong guildId, ModifyGuildParams args, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEqual(args.AfkChannelId, 0, nameof(args.AfkChannelId)); + Preconditions.AtLeast(args.AfkTimeout, 0, nameof(args.AfkTimeout)); + Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name)); + Preconditions.GreaterThan(args.OwnerId, 0, nameof(args.OwnerId)); + Preconditions.NotNull(args.RegionId, nameof(args.RegionId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendJsonAsync("PATCH", () => $"guilds/{guildId}", args, ids, options: options).ConfigureAwait(false); + } + public async Task BeginGuildPruneAsync(ulong guildId, GuildPruneParams args, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.AtLeast(args.Days, 0, nameof(args.Days)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendJsonAsync("POST", () => $"guilds/{guildId}/prune", args, ids, options: options).ConfigureAwait(false); + } + public async Task GetGuildPruneCountAsync(ulong guildId, GuildPruneParams args, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.AtLeast(args.Days, 0, nameof(args.Days)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendJsonAsync("GET", () => $"guilds/{guildId}/prune", args, ids, options: options).ConfigureAwait(false); + } + + //Guild Bans + public async Task> GetGuildBansAsync(ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendAsync>("GET", () => $"guilds/{guildId}/bans", ids, options: options).ConfigureAwait(false); + } + public async Task CreateGuildBanAsync(ulong guildId, ulong userId, CreateGuildBanParams args, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(userId, 0, nameof(userId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.AtLeast(args.DeleteMessageDays, 0, nameof(args.DeleteMessageDays)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + await SendAsync("PUT", () => $"guilds/{guildId}/bans/{userId}?delete-message-days={args.DeleteMessageDays}", ids, options: options).ConfigureAwait(false); + } + public async Task RemoveGuildBanAsync(ulong guildId, ulong userId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(userId, 0, nameof(userId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + await SendAsync("DELETE", () => $"guilds/{guildId}/bans/{userId}", ids, options: options).ConfigureAwait(false); + } + + //Guild Embeds + public async Task GetGuildEmbedAsync(ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + options = RequestOptions.CreateOrClone(options); + + try + { + var ids = new BucketIds(guildId: guildId); + return await SendAsync("GET", () => $"guilds/{guildId}/embed", ids, options: options).ConfigureAwait(false); + } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } + } + public async Task ModifyGuildEmbedAsync(ulong guildId, ModifyGuildEmbedParams args, RequestOptions options = null) + { + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendJsonAsync("PATCH", () => $"guilds/{guildId}/embed", args, ids, options: options).ConfigureAwait(false); + } + + //Guild Integrations + public async Task> GetGuildIntegrationsAsync(ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendAsync>("GET", () => $"guilds/{guildId}/integrations", ids, options: options).ConfigureAwait(false); + } + public async Task CreateGuildIntegrationAsync(ulong guildId, CreateGuildIntegrationParams args, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEqual(args.Id, 0, nameof(args.Id)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendAsync("POST", () => $"guilds/{guildId}/integrations", ids, options: options).ConfigureAwait(false); + } + public async Task DeleteGuildIntegrationAsync(ulong guildId, ulong integrationId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(integrationId, 0, nameof(integrationId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendAsync("DELETE", () => $"guilds/{guildId}/integrations/{integrationId}", ids, options: options).ConfigureAwait(false); + } + public async Task ModifyGuildIntegrationAsync(ulong guildId, ulong integrationId, ModifyGuildIntegrationParams args, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(integrationId, 0, nameof(integrationId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.AtLeast(args.ExpireBehavior, 0, nameof(args.ExpireBehavior)); + Preconditions.AtLeast(args.ExpireGracePeriod, 0, nameof(args.ExpireGracePeriod)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendJsonAsync("PATCH", () => $"guilds/{guildId}/integrations/{integrationId}", args, ids, options: options).ConfigureAwait(false); + } + public async Task SyncGuildIntegrationAsync(ulong guildId, ulong integrationId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(integrationId, 0, nameof(integrationId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendAsync("POST", () => $"guilds/{guildId}/integrations/{integrationId}/sync", ids, options: options).ConfigureAwait(false); + } + + //Guild Invites + public async Task GetInviteAsync(string inviteId, RequestOptions options = null) + { + Preconditions.NotNullOrEmpty(inviteId, nameof(inviteId)); + options = RequestOptions.CreateOrClone(options); + + //Remove trailing slash + if (inviteId[inviteId.Length - 1] == '/') + inviteId = inviteId.Substring(0, inviteId.Length - 1); + //Remove leading URL + int index = inviteId.LastIndexOf('/'); + if (index >= 0) + inviteId = inviteId.Substring(index + 1); + + try + { + return await SendAsync("GET", () => $"invites/{inviteId}", new BucketIds(), options: options).ConfigureAwait(false); + } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } + } + public async Task> GetGuildInvitesAsync(ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendAsync>("GET", () => $"guilds/{guildId}/invites", ids, options: options).ConfigureAwait(false); + } + public async Task> GetChannelInvitesAsync(ulong channelId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + return await SendAsync>("GET", () => $"channels/{channelId}/invites", ids, options: options).ConfigureAwait(false); + } + public async Task CreateChannelInviteAsync(ulong channelId, CreateChannelInviteParams args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.AtLeast(args.MaxAge, 0, nameof(args.MaxAge)); + Preconditions.AtLeast(args.MaxUses, 0, nameof(args.MaxUses)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + return await SendJsonAsync("POST", () => $"channels/{channelId}/invites", args, ids, options: options).ConfigureAwait(false); + } + public async Task DeleteInviteAsync(string inviteId, RequestOptions options = null) + { + Preconditions.NotNullOrEmpty(inviteId, nameof(inviteId)); + options = RequestOptions.CreateOrClone(options); + + return await SendAsync("DELETE", () => $"invites/{inviteId}", new BucketIds(), options: options).ConfigureAwait(false); + } + public async Task AcceptInviteAsync(string inviteId, RequestOptions options = null) + { + Preconditions.NotNullOrEmpty(inviteId, nameof(inviteId)); + options = RequestOptions.CreateOrClone(options); + + await SendAsync("POST", () => $"invites/{inviteId}", new BucketIds(), options: options).ConfigureAwait(false); + } + + //Guild Members + public async Task GetGuildMemberAsync(ulong guildId, ulong userId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(userId, 0, nameof(userId)); + options = RequestOptions.CreateOrClone(options); + + try + { + var ids = new BucketIds(guildId: guildId); + return await SendAsync("GET", () => $"guilds/{guildId}/members/{userId}", ids, options: options).ConfigureAwait(false); + } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } + } + public async Task> GetGuildMembersAsync(ulong guildId, GetGuildMembersParams args, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.GreaterThan(args.Limit, 0, nameof(args.Limit)); + Preconditions.AtMost(args.Limit, DiscordConfig.MaxUsersPerBatch, nameof(args.Limit)); + Preconditions.GreaterThan(args.AfterUserId, 0, nameof(args.AfterUserId)); + options = RequestOptions.CreateOrClone(options); + + int limit = args.Limit.GetValueOrDefault(int.MaxValue); + ulong afterUserId = args.AfterUserId.GetValueOrDefault(0); + + var ids = new BucketIds(guildId: guildId); + Expression> endpoint = () => $"guilds/{guildId}/members?limit={limit}&after={afterUserId}"; + return await SendAsync>("GET", endpoint, ids, options: options).ConfigureAwait(false); + } + public async Task RemoveGuildMemberAsync(ulong guildId, ulong userId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(userId, 0, nameof(userId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + await SendAsync("DELETE", () => $"guilds/{guildId}/members/{userId}", ids, options: options).ConfigureAwait(false); + } + public async Task ModifyGuildMemberAsync(ulong guildId, ulong userId, ModifyGuildMemberParams args, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(userId, 0, nameof(userId)); + Preconditions.NotNull(args, nameof(args)); + options = RequestOptions.CreateOrClone(options); + + bool isCurrentUser = userId == CurrentUser.Id; + + if (isCurrentUser && args.Nickname.IsSpecified) + { + var nickArgs = new ModifyCurrentUserNickParams(args.Nickname.Value ?? ""); + await ModifyMyNickAsync(guildId, nickArgs).ConfigureAwait(false); + args.Nickname = Optional.Create(); //Remove + } + if (!isCurrentUser || args.Deaf.IsSpecified || args.Mute.IsSpecified || args.RoleIds.IsSpecified) + { + var ids = new BucketIds(guildId: guildId); + await SendJsonAsync("PATCH", () => $"guilds/{guildId}/members/{userId}", args, ids, options: options).ConfigureAwait(false); + } + } + + //Guild Roles + public async Task> GetGuildRolesAsync(ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendAsync>("GET", () => $"guilds/{guildId}/roles", ids, options: options).ConfigureAwait(false); + } + public async Task CreateGuildRoleAsync(ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendAsync("POST", () => $"guilds/{guildId}/roles", ids, options: options).ConfigureAwait(false); + } + public async Task DeleteGuildRoleAsync(ulong guildId, ulong roleId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(roleId, 0, nameof(roleId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + await SendAsync("DELETE", () => $"guilds/{guildId}/roles/{roleId}", ids, options: options).ConfigureAwait(false); + } + public async Task ModifyGuildRoleAsync(ulong guildId, ulong roleId, ModifyGuildRoleParams args, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(roleId, 0, nameof(roleId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.AtLeast(args.Color, 0, nameof(args.Color)); + Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name)); + Preconditions.AtLeast(args.Position, 0, nameof(args.Position)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendJsonAsync("PATCH", () => $"guilds/{guildId}/roles/{roleId}", args, ids, options: options).ConfigureAwait(false); + } + public async Task> ModifyGuildRolesAsync(ulong guildId, IEnumerable args, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotNull(args, nameof(args)); + options = RequestOptions.CreateOrClone(options); + + var roles = args.ToImmutableArray(); + switch (roles.Length) + { + case 0: + return ImmutableArray.Create(); + case 1: + return ImmutableArray.Create(await ModifyGuildRoleAsync(guildId, roles[0].Id, roles[0]).ConfigureAwait(false)); + default: + var ids = new BucketIds(guildId: guildId); + return await SendJsonAsync>("PATCH", () => $"guilds/{guildId}/roles", args, ids, options: options).ConfigureAwait(false); + } + } + + //Users + public async Task GetUserAsync(ulong userId, RequestOptions options = null) + { + Preconditions.NotEqual(userId, 0, nameof(userId)); + options = RequestOptions.CreateOrClone(options); + + try + { + return await SendAsync("GET", () => $"users/{userId}", new BucketIds(), options: options).ConfigureAwait(false); + } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } + } + + //Current User/DMs + public async Task GetMyUserAsync(RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + return await SendAsync("GET", () => "users/@me", new BucketIds(), options: options).ConfigureAwait(false); + } + public async Task> GetMyConnectionsAsync(RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + return await SendAsync>("GET", () => "users/@me/connections", new BucketIds(), options: options).ConfigureAwait(false); + } + public async Task> GetMyPrivateChannelsAsync(RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + return await SendAsync>("GET", () => "users/@me/channels", new BucketIds(), options: options).ConfigureAwait(false); + } + public async Task> GetMyGuildsAsync(RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + return await SendAsync>("GET", () => "users/@me/guilds", new BucketIds(), options: options).ConfigureAwait(false); + } + public async Task GetMyApplicationAsync(RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + return await SendAsync("GET", () => "oauth2/applications/@me", new BucketIds(), options: options).ConfigureAwait(false); + } + public async Task ModifySelfAsync(ModifyCurrentUserParams args, RequestOptions options = null) + { + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotNullOrEmpty(args.Username, nameof(args.Username)); + options = RequestOptions.CreateOrClone(options); + + return await SendJsonAsync("PATCH", () => "users/@me", args, new BucketIds(), options: options).ConfigureAwait(false); + } + public async Task ModifyMyNickAsync(ulong guildId, ModifyCurrentUserNickParams args, RequestOptions options = null) + { + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotNull(args.Nickname, nameof(args.Nickname)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + await SendJsonAsync("PATCH", () => $"guilds/{guildId}/members/@me/nick", args, ids, options: options).ConfigureAwait(false); + } + public async Task CreateDMChannelAsync(CreateDMChannelParams args, RequestOptions options = null) + { + Preconditions.NotNull(args, nameof(args)); + Preconditions.GreaterThan(args.RecipientId, 0, nameof(args.RecipientId)); + options = RequestOptions.CreateOrClone(options); + + return await SendJsonAsync("POST", () => "users/@me/channels", args, new BucketIds(), options: options).ConfigureAwait(false); + } + + //Voice Regions + public async Task> GetVoiceRegionsAsync(RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + return await SendAsync>("GET", () => "voice/regions", new BucketIds(), options: options).ConfigureAwait(false); + } + public async Task> GetGuildVoiceRegionsAsync(ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendAsync>("GET", () => $"guilds/{guildId}/regions", ids, options: options).ConfigureAwait(false); + } + + //Helpers + protected void CheckState() + { + if (LoginState != LoginState.LoggedIn) + throw new InvalidOperationException("Client is not logged in."); + } + protected static double ToMilliseconds(Stopwatch stopwatch) => Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); + protected string SerializeJson(object value) + { + var sb = new StringBuilder(256); + using (TextWriter text = new StringWriter(sb, CultureInfo.InvariantCulture)) + using (JsonWriter writer = new JsonTextWriter(text)) + _serializer.Serialize(writer, value); + return sb.ToString(); + } + protected T DeserializeJson(Stream jsonStream) + { + using (TextReader text = new StreamReader(jsonStream)) + using (JsonReader reader = new JsonTextReader(text)) + return _serializer.Deserialize(reader); + } + internal string GetBucketId(ulong guildId = 0, ulong channelId = 0, [CallerMemberName] string methodName = "") + { + if (guildId != 0) + { + if (channelId != 0) + return $"{methodName}({guildId}/{channelId})"; + else + return $"{methodName}({guildId})"; + } + else if (channelId != 0) + return $"{methodName}({channelId})"; + return $"{methodName}()"; + } + + internal class BucketIds + { + public ulong GuildId { get; } + public ulong ChannelId { get; } + + internal BucketIds(ulong guildId = 0, ulong channelId = 0) + { + GuildId = guildId; + ChannelId = channelId; + } + internal object[] ToArray() + => new object[] { GuildId, ChannelId }; + + internal static int? GetIndex(string name) + { + switch (name) + { + case "guildId": return 0; + case "channelId": return 1; + default: + return null; + } + } + } + + private static string GetEndpoint(Expression> endpointExpr) + { + return endpointExpr.Compile()(); + } + private static string GetBucketId(BucketIds ids, Expression> endpointExpr, string callingMethod) + { + return _bucketIdGenerators.GetOrAdd(callingMethod, x => CreateBucketId(endpointExpr))(ids); + } + + private static Func CreateBucketId(Expression> endpoint) + { + try + { + //Is this a constant string? + if (endpoint.Body.NodeType == ExpressionType.Constant) + return x => (endpoint.Body as ConstantExpression).Value.ToString(); + + var builder = new StringBuilder(); + var methodCall = endpoint.Body as MethodCallExpression; + var methodArgs = methodCall.Arguments.ToArray(); + string format = (methodArgs[0] as ConstantExpression).Value as string; + + int endIndex = format.IndexOf('?'); //Dont include params + if (endIndex == -1) + endIndex = format.Length; + + int lastIndex = 0; + while (true) + { + int leftIndex = format.IndexOf("{", lastIndex); + if (leftIndex == -1 || leftIndex > endIndex) + { + builder.Append(format, lastIndex, endIndex - lastIndex); + break; + } + builder.Append(format, lastIndex, leftIndex); + int rightIndex = format.IndexOf("}", leftIndex); + + int argId = int.Parse(format.Substring(leftIndex + 1, rightIndex - leftIndex - 1)); + string fieldName = GetFieldName(methodArgs[argId + 1]); + int? mappedId; + + mappedId = BucketIds.GetIndex(fieldName); + if(!mappedId.HasValue && rightIndex != endIndex && format[rightIndex + 1] == '/') //Ignore the next slash + rightIndex++; + + if (mappedId.HasValue) + builder.Append($"{{{mappedId.Value}}}"); + + lastIndex = rightIndex + 1; + } + + format = builder.ToString(); + return x => string.Format(format, x.ToArray()); + } + catch (Exception ex) + { + throw new InvalidOperationException("Failed to generate the bucket id for this operation", ex); + } + } + + private static string GetFieldName(Expression expr) + { + if (expr.NodeType == ExpressionType.Convert) + expr = (expr as UnaryExpression).Operand; + + if (expr.NodeType != ExpressionType.MemberAccess) + throw new InvalidOperationException("Unsupported expression"); + + return (expr as MemberExpression).Member.Name; + } + } +} diff --git a/src/Discord.Net/API/Image.cs b/src/Discord.Net.Core/API/Image.cs similarity index 93% rename from src/Discord.Net/API/Image.cs rename to src/Discord.Net.Core/API/Image.cs index b2357a0a6..5442bd30f 100644 --- a/src/Discord.Net/API/Image.cs +++ b/src/Discord.Net.Core/API/Image.cs @@ -2,7 +2,7 @@ namespace Discord.API { - internal struct Image + public struct Image { public Stream Stream { get; } public string Hash { get; } diff --git a/src/Discord.Net/API/Int53Attribute.cs b/src/Discord.Net.Core/API/Int53Attribute.cs similarity index 100% rename from src/Discord.Net/API/Int53Attribute.cs rename to src/Discord.Net.Core/API/Int53Attribute.cs diff --git a/src/Discord.Net.Core/API/ObjectOrId.cs b/src/Discord.Net.Core/API/ObjectOrId.cs new file mode 100644 index 000000000..813aff906 --- /dev/null +++ b/src/Discord.Net.Core/API/ObjectOrId.cs @@ -0,0 +1,19 @@ +namespace Discord.API +{ + public struct ObjectOrId + { + public ulong Id { get; } + public T Object { get; } + + public ObjectOrId(ulong id) + { + Id = id; + Object = default(T); + } + public ObjectOrId(T obj) + { + Id = 0; + Object = obj; + } + } +} diff --git a/src/Discord.Net.Core/API/Rest/CreateChannelInviteParams.cs b/src/Discord.Net.Core/API/Rest/CreateChannelInviteParams.cs new file mode 100644 index 000000000..8a619a8b7 --- /dev/null +++ b/src/Discord.Net.Core/API/Rest/CreateChannelInviteParams.cs @@ -0,0 +1,16 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + public class CreateChannelInviteParams + { + [JsonProperty("max_age")] + public Optional MaxAge { get; set; } + [JsonProperty("max_uses")] + public Optional MaxUses { get; set; } + [JsonProperty("temporary")] + public Optional IsTemporary { get; set; } + } +} diff --git a/src/Discord.Net/API/Rest/CreateDMChannelParams.cs b/src/Discord.Net.Core/API/Rest/CreateDMChannelParams.cs similarity index 56% rename from src/Discord.Net/API/Rest/CreateDMChannelParams.cs rename to src/Discord.Net.Core/API/Rest/CreateDMChannelParams.cs index f52b20ca1..83fe76e98 100644 --- a/src/Discord.Net/API/Rest/CreateDMChannelParams.cs +++ b/src/Discord.Net.Core/API/Rest/CreateDMChannelParams.cs @@ -7,8 +7,11 @@ namespace Discord.API.Rest public class CreateDMChannelParams { [JsonProperty("recipient_id")] - internal ulong _recipientId { get; set; } - public ulong RecipientId { set { _recipientId = value; } } - public IUser Recipient { set { _recipientId = value.Id; } } + public ulong RecipientId { get; } + + public CreateDMChannelParams(ulong recipientId) + { + RecipientId = recipientId; + } } } diff --git a/src/Discord.Net.Core/API/Rest/CreateGuildBanParams.cs b/src/Discord.Net.Core/API/Rest/CreateGuildBanParams.cs new file mode 100644 index 000000000..724112bc0 --- /dev/null +++ b/src/Discord.Net.Core/API/Rest/CreateGuildBanParams.cs @@ -0,0 +1,8 @@ +#pragma warning disable CS1591 +namespace Discord.API.Rest +{ + public class CreateGuildBanParams + { + public Optional DeleteMessageDays { get; set; } + } +} diff --git a/src/Discord.Net.Core/API/Rest/CreateGuildChannelParams.cs b/src/Discord.Net.Core/API/Rest/CreateGuildChannelParams.cs new file mode 100644 index 000000000..f0e06e3d2 --- /dev/null +++ b/src/Discord.Net.Core/API/Rest/CreateGuildChannelParams.cs @@ -0,0 +1,23 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + public class CreateGuildChannelParams + { + [JsonProperty("name")] + public string Name { get; } + [JsonProperty("type")] + public ChannelType Type { get; } + + [JsonProperty("bitrate")] + public Optional Bitrate { get; set; } + + public CreateGuildChannelParams(string name, ChannelType type) + { + Name = name; + Type = type; + } + } +} diff --git a/src/Discord.Net/API/Rest/CreateGuildIntegrationParams.cs b/src/Discord.Net.Core/API/Rest/CreateGuildIntegrationParams.cs similarity index 57% rename from src/Discord.Net/API/Rest/CreateGuildIntegrationParams.cs rename to src/Discord.Net.Core/API/Rest/CreateGuildIntegrationParams.cs index 8e8dfb76d..0d6e3a654 100644 --- a/src/Discord.Net/API/Rest/CreateGuildIntegrationParams.cs +++ b/src/Discord.Net.Core/API/Rest/CreateGuildIntegrationParams.cs @@ -7,9 +7,14 @@ namespace Discord.API.Rest public class CreateGuildIntegrationParams { [JsonProperty("id")] - public ulong Id { internal get; set; } - + public ulong Id { get; } [JsonProperty("type")] - public string Type { internal get; set; } + public string Type { get; } + + public CreateGuildIntegrationParams(ulong id, string type) + { + Id = id; + Type = type; + } } } diff --git a/src/Discord.Net/API/Rest/CreateGuildParams.cs b/src/Discord.Net.Core/API/Rest/CreateGuildParams.cs similarity index 52% rename from src/Discord.Net/API/Rest/CreateGuildParams.cs rename to src/Discord.Net.Core/API/Rest/CreateGuildParams.cs index 8b9bd6178..4bc18c28b 100644 --- a/src/Discord.Net/API/Rest/CreateGuildParams.cs +++ b/src/Discord.Net.Core/API/Rest/CreateGuildParams.cs @@ -1,6 +1,5 @@ #pragma warning disable CS1591 using Newtonsoft.Json; -using System.IO; namespace Discord.API.Rest { @@ -8,13 +7,17 @@ namespace Discord.API.Rest public class CreateGuildParams { [JsonProperty("name")] - public string Name { internal get; set; } - + public string Name { get; } [JsonProperty("region")] - public string Region { internal get; set; } + public string RegionId { get; } [JsonProperty("icon")] - internal Optional _icon { get; set; } - public Stream Icon { set { _icon = value != null ? new Image(value) : (Image?)null; } } + public Optional Icon { get; set; } + + public CreateGuildParams(string name, string regionId) + { + Name = name; + RegionId = regionId; + } } } diff --git a/src/Discord.Net.Core/API/Rest/CreateMessageParams.cs b/src/Discord.Net.Core/API/Rest/CreateMessageParams.cs new file mode 100644 index 000000000..9577ab579 --- /dev/null +++ b/src/Discord.Net.Core/API/Rest/CreateMessageParams.cs @@ -0,0 +1,22 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + public class CreateMessageParams + { + [JsonProperty("content")] + public string Content { get; } + + [JsonProperty("nonce")] + public Optional Nonce { get; set; } + [JsonProperty("tts")] + public Optional IsTTS { get; set; } + + public CreateMessageParams(string content) + { + Content = content; + } + } +} diff --git a/src/Discord.Net.Core/API/Rest/DeleteMessagesParams.cs b/src/Discord.Net.Core/API/Rest/DeleteMessagesParams.cs new file mode 100644 index 000000000..09b9a2bf1 --- /dev/null +++ b/src/Discord.Net.Core/API/Rest/DeleteMessagesParams.cs @@ -0,0 +1,17 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + public class DeleteMessagesParams + { + [JsonProperty("messages")] + public ulong[] MessageIds { get; } + + public DeleteMessagesParams(ulong[] messageIds) + { + MessageIds = messageIds; + } + } +} diff --git a/src/Discord.Net.Core/API/Rest/GetChannelMessagesParams.cs b/src/Discord.Net.Core/API/Rest/GetChannelMessagesParams.cs new file mode 100644 index 000000000..2d00833ca --- /dev/null +++ b/src/Discord.Net.Core/API/Rest/GetChannelMessagesParams.cs @@ -0,0 +1,10 @@ +#pragma warning disable CS1591 +namespace Discord.API.Rest +{ + public class GetChannelMessagesParams + { + public Optional Limit { get; set; } + public Optional RelativeDirection { get; set; } + public Optional RelativeMessageId { get; set; } + } +} diff --git a/src/Discord.Net/API/Rest/GetGatewayResponse.cs b/src/Discord.Net.Core/API/Rest/GetGatewayResponse.cs similarity index 100% rename from src/Discord.Net/API/Rest/GetGatewayResponse.cs rename to src/Discord.Net.Core/API/Rest/GetGatewayResponse.cs diff --git a/src/Discord.Net.Core/API/Rest/GetGuildMembersParams.cs b/src/Discord.Net.Core/API/Rest/GetGuildMembersParams.cs new file mode 100644 index 000000000..2bd34ddcb --- /dev/null +++ b/src/Discord.Net.Core/API/Rest/GetGuildMembersParams.cs @@ -0,0 +1,9 @@ +#pragma warning disable CS1591 +namespace Discord.API.Rest +{ + public class GetGuildMembersParams + { + public Optional Limit { get; set; } + public Optional AfterUserId { get; set; } + } +} diff --git a/src/Discord.Net/API/Rest/GetGuildPruneCountResponse.cs b/src/Discord.Net.Core/API/Rest/GetGuildPruneCountResponse.cs similarity index 100% rename from src/Discord.Net/API/Rest/GetGuildPruneCountResponse.cs rename to src/Discord.Net.Core/API/Rest/GetGuildPruneCountResponse.cs diff --git a/src/Discord.Net/API/Rest/GuildPruneParams.cs b/src/Discord.Net.Core/API/Rest/GuildPruneParams.cs similarity index 65% rename from src/Discord.Net/API/Rest/GuildPruneParams.cs rename to src/Discord.Net.Core/API/Rest/GuildPruneParams.cs index 9b8b3d6e1..9cff46992 100644 --- a/src/Discord.Net/API/Rest/GuildPruneParams.cs +++ b/src/Discord.Net.Core/API/Rest/GuildPruneParams.cs @@ -7,6 +7,11 @@ namespace Discord.API.Rest public class GuildPruneParams { [JsonProperty("days")] - public int Days { internal get; set; } + public int Days { get; } + + public GuildPruneParams(int days) + { + Days = days; + } } } diff --git a/src/Discord.Net/API/Rest/ModifyChannelPermissionsParams.cs b/src/Discord.Net.Core/API/Rest/ModifyChannelPermissionsParams.cs similarity index 51% rename from src/Discord.Net/API/Rest/ModifyChannelPermissionsParams.cs rename to src/Discord.Net.Core/API/Rest/ModifyChannelPermissionsParams.cs index a650eeefa..8676b22e7 100644 --- a/src/Discord.Net/API/Rest/ModifyChannelPermissionsParams.cs +++ b/src/Discord.Net.Core/API/Rest/ModifyChannelPermissionsParams.cs @@ -7,10 +7,17 @@ namespace Discord.API.Rest public class ModifyChannelPermissionsParams { [JsonProperty("type")] - public string Type { internal get; set; } + public string Type { get; } [JsonProperty("allow")] - public ulong Allow { internal get; set; } + public ulong Allow { get; } [JsonProperty("deny")] - public ulong Deny { internal get; set; } + public ulong Deny { get; } + + public ModifyChannelPermissionsParams(string type, ulong allow, ulong deny) + { + Type = type; + Allow = allow; + Deny = deny; + } } } diff --git a/src/Discord.Net/API/Rest/ModifyCurrentUserNickParams.cs b/src/Discord.Net.Core/API/Rest/ModifyCurrentUserNickParams.cs similarity index 61% rename from src/Discord.Net/API/Rest/ModifyCurrentUserNickParams.cs rename to src/Discord.Net.Core/API/Rest/ModifyCurrentUserNickParams.cs index dd6fba46e..ca7ad2bd3 100644 --- a/src/Discord.Net/API/Rest/ModifyCurrentUserNickParams.cs +++ b/src/Discord.Net.Core/API/Rest/ModifyCurrentUserNickParams.cs @@ -7,6 +7,11 @@ namespace Discord.API.Rest public class ModifyCurrentUserNickParams { [JsonProperty("nick")] - public string Nickname { internal get; set; } + public string Nickname { get; } + + public ModifyCurrentUserNickParams(string nickname) + { + Nickname = nickname; + } } } diff --git a/src/Discord.Net/API/Rest/ModifyCurrentUserParams.cs b/src/Discord.Net.Core/API/Rest/ModifyCurrentUserParams.cs similarity index 51% rename from src/Discord.Net/API/Rest/ModifyCurrentUserParams.cs rename to src/Discord.Net.Core/API/Rest/ModifyCurrentUserParams.cs index 732e46377..d11ef2b77 100644 --- a/src/Discord.Net/API/Rest/ModifyCurrentUserParams.cs +++ b/src/Discord.Net.Core/API/Rest/ModifyCurrentUserParams.cs @@ -1,6 +1,5 @@ #pragma warning disable CS1591 using Newtonsoft.Json; -using System.IO; namespace Discord.API.Rest { @@ -8,11 +7,8 @@ namespace Discord.API.Rest public class ModifyCurrentUserParams { [JsonProperty("username")] - internal Optional _username { get; set; } - public string Username { set { _username = value; } } - + public Optional Username { get; set; } [JsonProperty("avatar")] - internal Optional _avatar { get; set; } - public Stream Avatar { set { _avatar = new Image(value); } } + public Optional Avatar { get; set; } } } diff --git a/src/Discord.Net/API/Rest/ModifyGuildChannelParams.cs b/src/Discord.Net.Core/API/Rest/ModifyGuildChannelParams.cs similarity index 55% rename from src/Discord.Net/API/Rest/ModifyGuildChannelParams.cs rename to src/Discord.Net.Core/API/Rest/ModifyGuildChannelParams.cs index 868150f87..6d6ee4c24 100644 --- a/src/Discord.Net/API/Rest/ModifyGuildChannelParams.cs +++ b/src/Discord.Net.Core/API/Rest/ModifyGuildChannelParams.cs @@ -7,11 +7,8 @@ namespace Discord.API.Rest public class ModifyGuildChannelParams { [JsonProperty("name")] - internal Optional _name { get; set; } - public string Name { set { _name = value; } } - + public Optional Name { get; set; } [JsonProperty("position")] - internal Optional _position { get; set; } - public int Position { set { _position = value; } } + public Optional Position { get; set; } } } diff --git a/src/Discord.Net/API/Rest/ModifyGuildChannelsParams.cs b/src/Discord.Net.Core/API/Rest/ModifyGuildChannelsParams.cs similarity index 55% rename from src/Discord.Net/API/Rest/ModifyGuildChannelsParams.cs rename to src/Discord.Net.Core/API/Rest/ModifyGuildChannelsParams.cs index 99679b924..8ac3299fa 100644 --- a/src/Discord.Net/API/Rest/ModifyGuildChannelsParams.cs +++ b/src/Discord.Net.Core/API/Rest/ModifyGuildChannelsParams.cs @@ -7,9 +7,14 @@ namespace Discord.API.Rest public class ModifyGuildChannelsParams { [JsonProperty("id")] - public ulong Id { internal get; set; } - + public ulong Id { get; set; } [JsonProperty("position")] - public int Position { internal get; set; } + public int Position { get; set; } + + public ModifyGuildChannelsParams(ulong id, int position) + { + Id = id; + Position = position; + } } } diff --git a/src/Discord.Net.Core/API/Rest/ModifyGuildEmbedParams.cs b/src/Discord.Net.Core/API/Rest/ModifyGuildEmbedParams.cs new file mode 100644 index 000000000..f362f8cd7 --- /dev/null +++ b/src/Discord.Net.Core/API/Rest/ModifyGuildEmbedParams.cs @@ -0,0 +1,14 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + public class ModifyGuildEmbedParams + { + [JsonProperty("enabled")] + public Optional Enabled { get; set; } + [JsonProperty("channel")] + public Optional ChannelId { get; set; } + } +} diff --git a/src/Discord.Net.Core/API/Rest/ModifyGuildIntegrationParams.cs b/src/Discord.Net.Core/API/Rest/ModifyGuildIntegrationParams.cs new file mode 100644 index 000000000..3a5526c96 --- /dev/null +++ b/src/Discord.Net.Core/API/Rest/ModifyGuildIntegrationParams.cs @@ -0,0 +1,16 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + public class ModifyGuildIntegrationParams + { + [JsonProperty("expire_behavior")] + public Optional ExpireBehavior { get; set; } + [JsonProperty("expire_grace_period")] + public Optional ExpireGracePeriod { get; set; } + [JsonProperty("enable_emoticons")] + public Optional EnableEmoticons { get; set; } + } +} diff --git a/src/Discord.Net.Core/API/Rest/ModifyGuildMemberParams.cs b/src/Discord.Net.Core/API/Rest/ModifyGuildMemberParams.cs new file mode 100644 index 000000000..17a8e2da1 --- /dev/null +++ b/src/Discord.Net.Core/API/Rest/ModifyGuildMemberParams.cs @@ -0,0 +1,20 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + public class ModifyGuildMemberParams + { + [JsonProperty("mute")] + public Optional Mute { get; set; } + [JsonProperty("deaf")] + public Optional Deaf { get; set; } + [JsonProperty("nick")] + public Optional Nickname { get; set; } + [JsonProperty("roles")] + public Optional RoleIds { get; set; } + [JsonProperty("channel_id")] + public Optional ChannelId { get; set; } + } +} diff --git a/src/Discord.Net.Core/API/Rest/ModifyGuildParams.cs b/src/Discord.Net.Core/API/Rest/ModifyGuildParams.cs new file mode 100644 index 000000000..f72ff2c96 --- /dev/null +++ b/src/Discord.Net.Core/API/Rest/ModifyGuildParams.cs @@ -0,0 +1,30 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + public class ModifyGuildParams + { + [JsonProperty("username")] + public Optional Username { get; set; } + [JsonProperty("name")] + public Optional Name { get; set; } + [JsonProperty("region")] + public Optional RegionId { get; set; } + [JsonProperty("verification_level")] + public Optional VerificationLevel { get; set; } + [JsonProperty("default_message_notifications")] + public Optional DefaultMessageNotifications { get; set; } + [JsonProperty("afk_timeout")] + public Optional AfkTimeout { get; set; } + [JsonProperty("icon")] + public Optional Icon { get; set; } + [JsonProperty("splash")] + public Optional Splash { get; set; } + [JsonProperty("afk_channel_id")] + public Optional AfkChannelId { get; set; } + [JsonProperty("owner_id")] + public Optional OwnerId { get; set; } + } +} diff --git a/src/Discord.Net.Core/API/Rest/ModifyGuildRoleParams.cs b/src/Discord.Net.Core/API/Rest/ModifyGuildRoleParams.cs new file mode 100644 index 000000000..d1226b534 --- /dev/null +++ b/src/Discord.Net.Core/API/Rest/ModifyGuildRoleParams.cs @@ -0,0 +1,20 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + public class ModifyGuildRoleParams + { + [JsonProperty("name")] + public Optional Name { get; set; } + [JsonProperty("permissions")] + public Optional Permissions { get; set; } + [JsonProperty("position")] + public Optional Position { get; set; } + [JsonProperty("color")] + public Optional Color { get; set; } + [JsonProperty("hoist")] + public Optional Hoist { get; set; } + } +} diff --git a/src/Discord.Net/API/Rest/ModifyGuildRolesParams.cs b/src/Discord.Net.Core/API/Rest/ModifyGuildRolesParams.cs similarity index 67% rename from src/Discord.Net/API/Rest/ModifyGuildRolesParams.cs rename to src/Discord.Net.Core/API/Rest/ModifyGuildRolesParams.cs index 69cd413e4..2350a8c47 100644 --- a/src/Discord.Net/API/Rest/ModifyGuildRolesParams.cs +++ b/src/Discord.Net.Core/API/Rest/ModifyGuildRolesParams.cs @@ -7,6 +7,11 @@ namespace Discord.API.Rest public class ModifyGuildRolesParams : ModifyGuildRoleParams { [JsonProperty("id")] - public ulong Id { internal get; set; } + public ulong Id { get; } + + public ModifyGuildRolesParams(ulong id) + { + Id = id; + } } } diff --git a/src/Discord.Net/API/Rest/ModifyMessageParams.cs b/src/Discord.Net.Core/API/Rest/ModifyMessageParams.cs similarity index 67% rename from src/Discord.Net/API/Rest/ModifyMessageParams.cs rename to src/Discord.Net.Core/API/Rest/ModifyMessageParams.cs index aca058ff4..4901ddc9d 100644 --- a/src/Discord.Net/API/Rest/ModifyMessageParams.cs +++ b/src/Discord.Net.Core/API/Rest/ModifyMessageParams.cs @@ -7,7 +7,6 @@ namespace Discord.API.Rest public class ModifyMessageParams { [JsonProperty("content")] - internal Optional _content { get; set; } - public string Content { set { _content = value; } } + public Optional Content { get; set; } } } diff --git a/src/Discord.Net.Core/API/Rest/ModifyPresenceParams.cs b/src/Discord.Net.Core/API/Rest/ModifyPresenceParams.cs new file mode 100644 index 000000000..ac55e2491 --- /dev/null +++ b/src/Discord.Net.Core/API/Rest/ModifyPresenceParams.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 +using System; + +namespace Discord.API.Rest +{ + public class ModifyPresenceParams + { + public Optional Status { get; set; } + public Optional Game { get; set; } + } +} diff --git a/src/Discord.Net/API/Rest/ModifyTextChannelParams.cs b/src/Discord.Net.Core/API/Rest/ModifyTextChannelParams.cs similarity index 70% rename from src/Discord.Net/API/Rest/ModifyTextChannelParams.cs rename to src/Discord.Net.Core/API/Rest/ModifyTextChannelParams.cs index 783771212..3546cee95 100644 --- a/src/Discord.Net/API/Rest/ModifyTextChannelParams.cs +++ b/src/Discord.Net.Core/API/Rest/ModifyTextChannelParams.cs @@ -7,7 +7,6 @@ namespace Discord.API.Rest public class ModifyTextChannelParams : ModifyGuildChannelParams { [JsonProperty("topic")] - internal Optional _topic { get; set; } - public string Topic { set { _topic = value; } } + public Optional Topic { get; set; } } } diff --git a/src/Discord.Net/API/Rest/ModifyVoiceChannelParams.cs b/src/Discord.Net.Core/API/Rest/ModifyVoiceChannelParams.cs similarity index 57% rename from src/Discord.Net/API/Rest/ModifyVoiceChannelParams.cs rename to src/Discord.Net.Core/API/Rest/ModifyVoiceChannelParams.cs index 5732feceb..8b5af9d8e 100644 --- a/src/Discord.Net/API/Rest/ModifyVoiceChannelParams.cs +++ b/src/Discord.Net.Core/API/Rest/ModifyVoiceChannelParams.cs @@ -7,11 +7,8 @@ namespace Discord.API.Rest public class ModifyVoiceChannelParams : ModifyGuildChannelParams { [JsonProperty("bitrate")] - internal Optional _bitrate { get; set; } - public int Bitrate { set { _bitrate = value; } } - + public Optional Bitrate { get; set; } [JsonProperty("user_limit")] - internal Optional _userLimit { get; set; } - public int UserLimit { set { _userLimit = value; } } + public Optional UserLimit { get; set; } } } diff --git a/src/Discord.Net.Core/API/Rest/UploadFileParams.cs b/src/Discord.Net.Core/API/Rest/UploadFileParams.cs new file mode 100644 index 000000000..bbd798900 --- /dev/null +++ b/src/Discord.Net.Core/API/Rest/UploadFileParams.cs @@ -0,0 +1,35 @@ +#pragma warning disable CS1591 +using Discord.Net.Rest; +using System.Collections.Generic; +using System.IO; + +namespace Discord.API.Rest +{ + public class UploadFileParams + { + public Stream File { get; } + + public Optional Filename { get; set; } + public Optional Content { get; set; } + public Optional Nonce { get; set; } + public Optional IsTTS { get; set; } + + public UploadFileParams(Stream file) + { + File = file; + } + + public IReadOnlyDictionary ToDictionary() + { + var d = new Dictionary(); + d["file"] = new MultipartFile(File, Filename.GetValueOrDefault("unknown.dat")); + if (Content.IsSpecified) + d["content"] = Content.Value; + if (IsTTS.IsSpecified) + d["tts"] = IsTTS.Value.ToString(); + if (Nonce.IsSpecified) + d["nonce"] = Nonce.Value; + return d; + } + } +} diff --git a/src/Discord.Net.Core/AssemblyInfo.cs b/src/Discord.Net.Core/AssemblyInfo.cs new file mode 100644 index 000000000..8563c4035 --- /dev/null +++ b/src/Discord.Net.Core/AssemblyInfo.cs @@ -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.Tests")] \ No newline at end of file diff --git a/src/Discord.Net/Audio/IAudioClient.cs b/src/Discord.Net.Core/Audio/IAudioClient.cs similarity index 65% rename from src/Discord.Net/Audio/IAudioClient.cs rename to src/Discord.Net.Core/Audio/IAudioClient.cs index 312152142..3cfdfa856 100644 --- a/src/Discord.Net/Audio/IAudioClient.cs +++ b/src/Discord.Net.Core/Audio/IAudioClient.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Threading.Tasks; namespace Discord.Audio @@ -8,8 +9,7 @@ namespace Discord.Audio event Func Connected; event Func Disconnected; event Func LatencyUpdated; - - DiscordVoiceAPIClient ApiClient { get; } + /// Gets the current connection state of this client. ConnectionState ConnectionState { get; } /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. @@ -17,7 +17,7 @@ namespace Discord.Audio Task DisconnectAsync(); - RTPWriteStream CreateOpusStream(int samplesPerFrame, int bufferSize = 4000); - OpusEncodeStream CreatePCMStream(int samplesPerFrame, int? bitrate = null, OpusApplication application = OpusApplication.MusicOrMixed, int bufferSize = 4000); + Stream CreateOpusStream(int samplesPerFrame, int bufferSize = 4000); + Stream CreatePCMStream(int samplesPerFrame, int? bitrate = null, OpusApplication application = OpusApplication.MusicOrMixed, int bufferSize = 4000); } } diff --git a/src/Discord.Net/Audio/Opus/OpusApplication.cs b/src/Discord.Net.Core/Audio/Opus/OpusApplication.cs similarity index 100% rename from src/Discord.Net/Audio/Opus/OpusApplication.cs rename to src/Discord.Net.Core/Audio/Opus/OpusApplication.cs diff --git a/src/Discord.Net/ConnectionState.cs b/src/Discord.Net.Core/ConnectionState.cs similarity index 100% rename from src/Discord.Net/ConnectionState.cs rename to src/Discord.Net.Core/ConnectionState.cs diff --git a/src/Discord.Net.Core/Discord.Net.Core.xproj b/src/Discord.Net.Core/Discord.Net.Core.xproj new file mode 100644 index 000000000..6759e09b4 --- /dev/null +++ b/src/Discord.Net.Core/Discord.Net.Core.xproj @@ -0,0 +1,19 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 91e9e7bd-75c9-4e98-84aa-2c271922e5c2 + Discord + .\obj + .\bin\ + v4.5.2 + + + 2.0 + + + \ No newline at end of file diff --git a/src/Discord.Net/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs similarity index 100% rename from src/Discord.Net/DiscordConfig.cs rename to src/Discord.Net.Core/DiscordConfig.cs diff --git a/src/Discord.Net.Core/Entities/CacheMode.cs b/src/Discord.Net.Core/Entities/CacheMode.cs new file mode 100644 index 000000000..a047bd616 --- /dev/null +++ b/src/Discord.Net.Core/Entities/CacheMode.cs @@ -0,0 +1,8 @@ +namespace Discord +{ + public enum CacheMode + { + AllowDownload, + CacheOnly + } +} diff --git a/src/Discord.Net/Entities/Channels/ChannelType.cs b/src/Discord.Net.Core/Entities/Channels/ChannelType.cs similarity index 100% rename from src/Discord.Net/Entities/Channels/ChannelType.cs rename to src/Discord.Net.Core/Entities/Channels/ChannelType.cs diff --git a/src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs b/src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs new file mode 100644 index 000000000..9b8074efb --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs @@ -0,0 +1,6 @@ +namespace Discord +{ + public interface IAudioChannel + { + } +} diff --git a/src/Discord.Net.Core/Entities/Channels/IChannel.cs b/src/Discord.Net.Core/Entities/Channels/IChannel.cs new file mode 100644 index 000000000..72608ec6a --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/IChannel.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IChannel : ISnowflakeEntity + { + /// Gets the name of this channel. + string Name { get; } + + /// Gets a collection of all users in this channel. + IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + + /// Gets a user in this channel with the provided id. + Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + } +} diff --git a/src/Discord.Net/Entities/Channels/IDMChannel.cs b/src/Discord.Net.Core/Entities/Channels/IDMChannel.cs similarity index 85% rename from src/Discord.Net/Entities/Channels/IDMChannel.cs rename to src/Discord.Net.Core/Entities/Channels/IDMChannel.cs index a5a3a4168..1608d1543 100644 --- a/src/Discord.Net/Entities/Channels/IDMChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IDMChannel.cs @@ -8,6 +8,6 @@ namespace Discord IUser Recipient { get; } /// Closes this private channel, removing it from your channel list. - Task CloseAsync(); + Task CloseAsync(RequestOptions options = null); } } \ No newline at end of file diff --git a/src/Discord.Net/Entities/Channels/IGroupChannel.cs b/src/Discord.Net.Core/Entities/Channels/IGroupChannel.cs similarity index 57% rename from src/Discord.Net/Entities/Channels/IGroupChannel.cs rename to src/Discord.Net.Core/Entities/Channels/IGroupChannel.cs index 6b71f76b6..d6cb2c182 100644 --- a/src/Discord.Net/Entities/Channels/IGroupChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IGroupChannel.cs @@ -2,12 +2,9 @@ namespace Discord { - public interface IGroupChannel : IMessageChannel, IPrivateChannel + public interface IGroupChannel : IMessageChannel, IPrivateChannel, IAudioChannel { - /// Adds a user to this group. - Task AddUserAsync(IUser user); - /// Leaves this group. - Task LeaveAsync(); + Task LeaveAsync(RequestOptions options = null); } } \ No newline at end of file diff --git a/src/Discord.Net/Entities/Channels/IGuildChannel.cs b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs similarity index 71% rename from src/Discord.Net/Entities/Channels/IGuildChannel.cs rename to src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs index e42ace8d6..81bf42d8e 100644 --- a/src/Discord.Net/Entities/Channels/IGuildChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs @@ -7,44 +7,41 @@ namespace Discord { public interface IGuildChannel : IChannel, IDeletable { - /// Gets the name of this channel. - string Name { get; } /// Gets the position of this channel in the guild's channel list, relative to others of the same type. int Position { get; } - /// Gets the guild this channel is a member of. - IGuild Guild { get; } + /// Gets the id of the guild this channel is a member of. + ulong GuildId { get; } + /// Gets a collection of permission overwrites for this channel. + IReadOnlyCollection PermissionOverwrites { get; } /// Creates a new invite to this channel. /// The time (in seconds) until the invite expires. Set to null to never expire. /// The max amount of times this invite may be used. Set to null to have unlimited uses. /// If true, a user accepting this invite will be kicked from the guild after closing their client. - Task CreateInviteAsync(int? maxAge = 1800, int? maxUses = default(int?), bool isTemporary = false); + Task CreateInviteAsync(int? maxAge = 1800, int? maxUses = default(int?), bool isTemporary = false, RequestOptions options = null); /// Returns a collection of all invites to this channel. - Task> GetInvitesAsync(); - - /// Gets a collection of permission overwrites for this channel. - IReadOnlyCollection PermissionOverwrites { get; } + Task> GetInvitesAsync(RequestOptions options = null); /// Modifies this guild channel. - Task ModifyAsync(Action func); + Task ModifyAsync(Action func, RequestOptions options = null); /// Gets the permission overwrite for a specific role, or null if one does not exist. OverwritePermissions? GetPermissionOverwrite(IRole role); /// Gets the permission overwrite for a specific user, or null if one does not exist. OverwritePermissions? GetPermissionOverwrite(IUser user); /// Removes the permission overwrite for the given role, if one exists. - Task RemovePermissionOverwriteAsync(IRole role); + Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null); /// Removes the permission overwrite for the given user, if one exists. - Task RemovePermissionOverwriteAsync(IUser user); + Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null); /// Adds or updates the permission overwrite for the given role. - Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions); + Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null); /// Adds or updates the permission overwrite for the given user. - Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions); + Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null); /// Gets a collection of all users in this channel. - new Task> GetUsersAsync(); + new IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// Gets a user in this channel with the provided id. - new Task GetUserAsync(ulong id); + new Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); } } \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs new file mode 100644 index 000000000..7c13e4a6f --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IMessageChannel : IChannel + { + /// Sends a message to this message channel. + Task SendMessageAsync(string text, bool isTTS = false, RequestOptions options = null); + /// Sends a file to this text channel, with an optional caption. + Task SendFileAsync(string filePath, string text = null, bool isTTS = false, RequestOptions options = null); + /// Sends a file to this text channel, with an optional caption. + Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, RequestOptions options = null); + + /// Gets a message from this message channel with the given id, or null if not found. + Task GetMessageAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// Gets the last N messages from this message channel. + IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, + CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// Gets a collection of messages in this channel. + IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, + CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// Gets a collection of messages in this channel. + IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, + CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// Gets a collection of pinned messages in this channel. + Task> GetPinnedMessagesAsync(RequestOptions options = null); + /// Bulk deletes multiple messages. + Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null); + + /// Broadcasts the "user is typing" message to all users in this channel, lasting 10 seconds. + Task TriggerTypingAsync(RequestOptions options = null); + /// Continuously broadcasts the "user is typing" message to all users in this channel until the returned object is disposed. + IDisposable EnterTypingState(RequestOptions options = null); + } +} diff --git a/src/Discord.Net/Entities/Channels/IPrivateChannel.cs b/src/Discord.Net.Core/Entities/Channels/IPrivateChannel.cs similarity index 100% rename from src/Discord.Net/Entities/Channels/IPrivateChannel.cs rename to src/Discord.Net.Core/Entities/Channels/IPrivateChannel.cs diff --git a/src/Discord.Net/Entities/Channels/ITextChannel.cs b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs similarity index 78% rename from src/Discord.Net/Entities/Channels/ITextChannel.cs rename to src/Discord.Net.Core/Entities/Channels/ITextChannel.cs index 3b4248b6e..7ecaf6d7b 100644 --- a/src/Discord.Net/Entities/Channels/ITextChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs @@ -10,6 +10,6 @@ namespace Discord string Topic { get; } /// Modifies this text channel. - Task ModifyAsync(Action func); + Task ModifyAsync(Action func, RequestOptions options = null); } } \ No newline at end of file diff --git a/src/Discord.Net/Entities/Channels/IVoiceChannel.cs b/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs similarity index 79% rename from src/Discord.Net/Entities/Channels/IVoiceChannel.cs rename to src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs index 5f6e8c817..d1be73072 100644 --- a/src/Discord.Net/Entities/Channels/IVoiceChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; namespace Discord { - public interface IVoiceChannel : IGuildChannel + public interface IVoiceChannel : IGuildChannel, IAudioChannel { /// Gets the bitrate, in bits per second, clients in this voice channel are requested to use. int Bitrate { get; } @@ -13,7 +13,7 @@ namespace Discord int UserLimit { get; } /// Modifies this voice channel. - Task ModifyAsync(Action func); + Task ModifyAsync(Action func, RequestOptions options = null); /// Connects to this voice channel. Task ConnectAsync(); } diff --git a/src/Discord.Net/Entities/Guilds/DefaultMessageNotifications.cs b/src/Discord.Net.Core/Entities/Guilds/DefaultMessageNotifications.cs similarity index 83% rename from src/Discord.Net/Entities/Guilds/DefaultMessageNotifications.cs rename to src/Discord.Net.Core/Entities/Guilds/DefaultMessageNotifications.cs index efc107537..a5cabc117 100644 --- a/src/Discord.Net/Entities/Guilds/DefaultMessageNotifications.cs +++ b/src/Discord.Net.Core/Entities/Guilds/DefaultMessageNotifications.cs @@ -2,9 +2,9 @@ { public enum DefaultMessageNotifications { - /// By default, only mentions will trigger notifications. - MentionsOnly = 0, /// By default, all messages will trigger notifications. - AllMessages = 1 + AllMessages = 0, + /// By default, only mentions will trigger notifications. + MentionsOnly = 1 } } diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildEmoji.cs b/src/Discord.Net.Core/Entities/Guilds/GuildEmoji.cs new file mode 100644 index 000000000..8b2bbd9c2 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/GuildEmoji.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using Model = Discord.API.Emoji; + +namespace Discord +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public struct GuildEmoji + { + public ulong Id { get; } + public string Name { get; } + public bool IsManaged { get; } + public bool RequireColons { get; } + public IReadOnlyList RoleIds { get; } + + private GuildEmoji(ulong id, string name, bool isManaged, bool requireColons, IReadOnlyList roleIds) + { + Id = id; + Name = name; + IsManaged = isManaged; + RequireColons = requireColons; + RoleIds = roleIds; + } + internal static GuildEmoji Create(Model model) + { + return new GuildEmoji(model.Id, model.Name, model.Managed, model.RequireColons, ImmutableArray.Create(model.Roles)); + } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id})"; + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/IBan.cs b/src/Discord.Net.Core/Entities/Guilds/IBan.cs new file mode 100644 index 000000000..05ab0c00f --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/IBan.cs @@ -0,0 +1,8 @@ +namespace Discord +{ + public interface IBan + { + IUser User { get; } + string Reason { get; } + } +} diff --git a/src/Discord.Net/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs similarity index 70% rename from src/Discord.Net/Entities/Guilds/IGuild.cs rename to src/Discord.Net.Core/Entities/Guilds/IGuild.cs index b1c010439..413b5da62 100644 --- a/src/Discord.Net/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -1,12 +1,12 @@ -using System; +using Discord.API.Rest; +using Discord.Audio; +using System; using System.Collections.Generic; using System.Threading.Tasks; -using Discord.API.Rest; -using Discord.Audio; namespace Discord { - public interface IGuild : IDeletable, ISnowflakeEntity, IUpdateable + public interface IGuild : IDeletable, ISnowflakeEntity { /// Gets the name of this guild. string Name { get; } @@ -20,8 +20,12 @@ namespace Discord MfaLevel MfaLevel { get; } /// Gets the level of requirements a user must fulfill before being allowed to post messages in this guild. VerificationLevel VerificationLevel { get; } + /// Returns the id of this guild's icon, or null if one is not set. + string IconId { get; } /// Returns the url to this guild's icon, or null if one is not set. string IconUrl { get; } + /// Returns the id of this guild's splash image, or null if one is not set. + string SplashId { get; } /// Returns the url to this guild's splash image, or null if one is not set. string SplashUrl { get; } /// Returns true if this guild is currently connected and ready to be used. Only applies to the WebSocket client. @@ -37,66 +41,68 @@ namespace Discord ulong OwnerId { get; } /// Gets the id of the region hosting this guild's voice channels. string VoiceRegionId { get; } - /// Gets the IAudioClient currently associated with this guild. IAudioClient AudioClient { get; } /// Gets the built-in role containing all users in this guild. IRole EveryoneRole { get; } /// Gets a collection of all custom emojis for this guild. - IReadOnlyCollection Emojis { get; } + IReadOnlyCollection Emojis { get; } /// Gets a collection of all extra features added to this guild. IReadOnlyCollection Features { get; } /// Gets a collection of all roles in this guild. IReadOnlyCollection Roles { get; } /// Modifies this guild. - Task ModifyAsync(Action func); + Task ModifyAsync(Action func, RequestOptions options = null); /// Modifies this guild's embed. - Task ModifyEmbedAsync(Action func); + Task ModifyEmbedAsync(Action func, RequestOptions options = null); /// Bulk modifies the channels of this guild. - Task ModifyChannelsAsync(IEnumerable args); + Task ModifyChannelsAsync(IEnumerable args, RequestOptions options = null); /// Bulk modifies the roles of this guild. - Task ModifyRolesAsync(IEnumerable args); + Task ModifyRolesAsync(IEnumerable args, RequestOptions options = null); /// Leaves this guild. If you are the owner, use Delete instead. - Task LeaveAsync(); + Task LeaveAsync(RequestOptions options = null); /// Gets a collection of all users banned on this guild. - Task> GetBansAsync(); + Task> GetBansAsync(RequestOptions options = null); /// Bans the provided user from this guild and optionally prunes their recent messages. - Task AddBanAsync(IUser user, int pruneDays = 0); + Task AddBanAsync(IUser user, int pruneDays = 0, RequestOptions options = null); /// Bans the provided user id from this guild and optionally prunes their recent messages. - Task AddBanAsync(ulong userId, int pruneDays = 0); + Task AddBanAsync(ulong userId, int pruneDays = 0, RequestOptions options = null); /// Unbans the provided user if it is currently banned. - Task RemoveBanAsync(IUser user); + Task RemoveBanAsync(IUser user, RequestOptions options = null); /// Unbans the provided user id if it is currently banned. - Task RemoveBanAsync(ulong userId); + Task RemoveBanAsync(ulong userId, RequestOptions options = null); /// Gets a collection of all channels in this guild. - Task> GetChannelsAsync(); + Task> GetChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// Gets the channel in this guild with the provided id, or null if not found. - Task GetChannelAsync(ulong id); + Task GetChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// Creates a new text channel. - Task CreateTextChannelAsync(string name); + Task CreateTextChannelAsync(string name, RequestOptions options = null); /// Creates a new voice channel. - Task CreateVoiceChannelAsync(string name); + Task CreateVoiceChannelAsync(string name, RequestOptions options = null); + + Task> GetIntegrationsAsync(RequestOptions options = null); + Task CreateIntegrationAsync(ulong id, string type, RequestOptions options = null); /// Gets a collection of all invites to this guild. - Task> GetInvitesAsync(); + Task> GetInvitesAsync(RequestOptions options = null); /// Gets the role in this guild with the provided id, or null if not found. IRole GetRole(ulong id); /// Creates a new role. - Task CreateRoleAsync(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false); + Task CreateRoleAsync(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false, RequestOptions options = null); /// Gets a collection of all users in this guild. - Task> GetUsersAsync(); + Task> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); //TODO: shouldnt this be paged? /// Gets the user in this guild with the provided id, or null if not found. - Task GetUserAsync(ulong id); + Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// Gets the current user for this guild. - Task GetCurrentUserAsync(); - /// Downloads all users for this guild if the current list is incomplete. Only applies to the WebSocket client. + Task GetCurrentUserAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// Downloads all users for this guild if the current list is incomplete. Task DownloadUsersAsync(); /// Removes all users from this guild if they have not logged on in a provided number of days or, if simulate is true, returns the number of users that would be removed. - Task PruneUsersAsync(int days = 30, bool simulate = false); + Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null); } } \ No newline at end of file diff --git a/src/Discord.Net/Entities/Guilds/IGuildIntegration.cs b/src/Discord.Net.Core/Entities/Guilds/IGuildIntegration.cs similarity index 83% rename from src/Discord.Net/Entities/Guilds/IGuildIntegration.cs rename to src/Discord.Net.Core/Entities/Guilds/IGuildIntegration.cs index 7f6ed6408..1a0c6d2d0 100644 --- a/src/Discord.Net/Entities/Guilds/IGuildIntegration.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuildIntegration.cs @@ -2,7 +2,6 @@ namespace Discord { - //TODO: Add docstrings public interface IGuildIntegration { ulong Id { get; } @@ -15,8 +14,8 @@ namespace Discord DateTimeOffset SyncedAt { get; } IntegrationAccount Account { get; } - IGuild Guild { get; } + ulong GuildId { get; } + ulong RoleId { get; } IUser User { get; } - IRole Role { get; } } } diff --git a/src/Discord.Net/Entities/Guilds/IUserGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IUserGuild.cs similarity index 100% rename from src/Discord.Net/Entities/Guilds/IUserGuild.cs rename to src/Discord.Net.Core/Entities/Guilds/IUserGuild.cs diff --git a/src/Discord.Net/Entities/Guilds/IVoiceRegion.cs b/src/Discord.Net.Core/Entities/Guilds/IVoiceRegion.cs similarity index 100% rename from src/Discord.Net/Entities/Guilds/IVoiceRegion.cs rename to src/Discord.Net.Core/Entities/Guilds/IVoiceRegion.cs diff --git a/src/Discord.Net/Entities/Guilds/IntegrationAccount.cs b/src/Discord.Net.Core/Entities/Guilds/IntegrationAccount.cs similarity index 100% rename from src/Discord.Net/Entities/Guilds/IntegrationAccount.cs rename to src/Discord.Net.Core/Entities/Guilds/IntegrationAccount.cs diff --git a/src/Discord.Net/Entities/Guilds/MfaLevel.cs b/src/Discord.Net.Core/Entities/Guilds/MfaLevel.cs similarity index 100% rename from src/Discord.Net/Entities/Guilds/MfaLevel.cs rename to src/Discord.Net.Core/Entities/Guilds/MfaLevel.cs diff --git a/src/Discord.Net/Entities/Guilds/VerificationLevel.cs b/src/Discord.Net.Core/Entities/Guilds/VerificationLevel.cs similarity index 100% rename from src/Discord.Net/Entities/Guilds/VerificationLevel.cs rename to src/Discord.Net.Core/Entities/Guilds/VerificationLevel.cs diff --git a/src/Discord.Net/Entities/IApplication.cs b/src/Discord.Net.Core/Entities/IApplication.cs similarity index 77% rename from src/Discord.Net/Entities/IApplication.cs rename to src/Discord.Net.Core/Entities/IApplication.cs index 157205226..4fb1e4b91 100644 --- a/src/Discord.Net/Entities/IApplication.cs +++ b/src/Discord.Net.Core/Entities/IApplication.cs @@ -1,6 +1,6 @@ namespace Discord { - public interface IApplication : ISnowflakeEntity, IUpdateable + public interface IApplication : ISnowflakeEntity { string Name { get; } string Description { get; } diff --git a/src/Discord.Net/Entities/IDeletable.cs b/src/Discord.Net.Core/Entities/IDeletable.cs similarity index 75% rename from src/Discord.Net/Entities/IDeletable.cs rename to src/Discord.Net.Core/Entities/IDeletable.cs index f35f8ad88..ba22a537a 100644 --- a/src/Discord.Net/Entities/IDeletable.cs +++ b/src/Discord.Net.Core/Entities/IDeletable.cs @@ -5,6 +5,6 @@ namespace Discord public interface IDeletable { /// Deletes this object and all its children. - Task DeleteAsync(); + Task DeleteAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/IEntity.cs b/src/Discord.Net.Core/Entities/IEntity.cs new file mode 100644 index 000000000..b1bb922c9 --- /dev/null +++ b/src/Discord.Net.Core/Entities/IEntity.cs @@ -0,0 +1,15 @@ +using System; + +namespace Discord +{ + public interface IEntity + where TId : IEquatable + { + /// Gets the IDiscordClient that created this object. + IDiscordClient Discord { get; } + + /// Gets the unique identifier for this object. + TId Id { get; } + + } +} diff --git a/src/Discord.Net/Entities/IMentionable.cs b/src/Discord.Net.Core/Entities/IMentionable.cs similarity index 100% rename from src/Discord.Net/Entities/IMentionable.cs rename to src/Discord.Net.Core/Entities/IMentionable.cs diff --git a/src/Discord.Net/Entities/ISnowflakeEntity.cs b/src/Discord.Net.Core/Entities/ISnowflakeEntity.cs similarity index 68% rename from src/Discord.Net/Entities/ISnowflakeEntity.cs rename to src/Discord.Net.Core/Entities/ISnowflakeEntity.cs index 60623425c..5b099b5ac 100644 --- a/src/Discord.Net/Entities/ISnowflakeEntity.cs +++ b/src/Discord.Net.Core/Entities/ISnowflakeEntity.cs @@ -4,7 +4,6 @@ namespace Discord { public interface ISnowflakeEntity : IEntity { - /// Gets when this object was created. DateTimeOffset CreatedAt { get; } } } diff --git a/src/Discord.Net/Entities/IUpdateable.cs b/src/Discord.Net.Core/Entities/IUpdateable.cs similarity index 77% rename from src/Discord.Net/Entities/IUpdateable.cs rename to src/Discord.Net.Core/Entities/IUpdateable.cs index 50b23bb95..b0f51aee7 100644 --- a/src/Discord.Net/Entities/IUpdateable.cs +++ b/src/Discord.Net.Core/Entities/IUpdateable.cs @@ -5,6 +5,6 @@ namespace Discord public interface IUpdateable { /// Updates this object's properties with its current state. - Task UpdateAsync(); + Task UpdateAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net/Entities/Invites/IInvite.cs b/src/Discord.Net.Core/Entities/Invites/IInvite.cs similarity index 92% rename from src/Discord.Net/Entities/Invites/IInvite.cs rename to src/Discord.Net.Core/Entities/Invites/IInvite.cs index 5e5ca40ae..081b57d76 100644 --- a/src/Discord.Net/Entities/Invites/IInvite.cs +++ b/src/Discord.Net.Core/Entities/Invites/IInvite.cs @@ -15,6 +15,6 @@ namespace Discord ulong GuildId { get; } /// Accepts this invite and joins the target guild. This will fail on bot accounts. - Task AcceptAsync(); + Task AcceptAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net/Entities/Invites/IInviteMetadata.cs b/src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs similarity index 100% rename from src/Discord.Net/Entities/Invites/IInviteMetadata.cs rename to src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs diff --git a/src/Discord.Net/Entities/Messages/Direction.cs b/src/Discord.Net.Core/Entities/Messages/Direction.cs similarity index 100% rename from src/Discord.Net/Entities/Messages/Direction.cs rename to src/Discord.Net.Core/Entities/Messages/Direction.cs diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs b/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs new file mode 100644 index 000000000..64b13e8e3 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs @@ -0,0 +1,25 @@ +using System.Diagnostics; +using Model = Discord.API.EmbedProvider; + +namespace Discord +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public struct EmbedProvider + { + public string Name { get; } + public string Url { get; } + + private EmbedProvider(string name, string url) + { + Name = name; + Url = url; + } + internal static EmbedProvider Create(Model model) + { + return new EmbedProvider(model.Name, model.Url); + } + + private string DebuggerDisplay => $"{Name} ({Url})"; + public override string ToString() => Name; + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs b/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs new file mode 100644 index 000000000..6a5fc4163 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs @@ -0,0 +1,31 @@ +using System.Diagnostics; +using Model = Discord.API.EmbedThumbnail; + +namespace Discord +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public struct EmbedThumbnail + { + public string Url { get; } + public string ProxyUrl { get; } + public int? Height { get; } + public int? Width { get; } + + private EmbedThumbnail(string url, string proxyUrl, int? height, int? width) + { + Url = url; + ProxyUrl = proxyUrl; + Height = height; + Width = width; + } + internal static EmbedThumbnail Create(Model model) + { + return new EmbedThumbnail(model.Url, model.ProxyUrl, + model.Height.IsSpecified ? model.Height.Value : (int?)null, + model.Width.IsSpecified ? model.Width.Value : (int?)null); + } + + private string DebuggerDisplay => $"{ToString()} ({Url})"; + public override string ToString() => Width != null && Height != null ? $"{Width}x{Height}" : "0x0"; + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/Emoji.cs b/src/Discord.Net.Core/Entities/Messages/Emoji.cs new file mode 100644 index 000000000..612e99f29 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/Emoji.cs @@ -0,0 +1,54 @@ +using Discord.API; +using System; +using System.Diagnostics; +using System.Globalization; + +namespace Discord +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public struct Emoji + { + public ulong Id { get; } + public string Name { get; } + + public string Url => CDN.GetEmojiUrl(Id); + + internal Emoji(ulong id, string name) + { + Id = id; + Name = name; + } + + public static Emoji Parse(string text) + { + Emoji result; + if (TryParse(text, out result)) + return result; + throw new ArgumentException("Invalid emoji format", nameof(text)); + } + + public static bool TryParse(string text, out Emoji result) + { + result = default(Emoji); + if (text.Length >= 4 && text[0] == '<' && text[1] == ':' && text[text.Length - 1] == '>') + { + int splitIndex = text.IndexOf(':', 2); + if (splitIndex == -1) + return false; + + ulong id; + if (!ulong.TryParse(text.Substring(splitIndex + 1, text.Length - splitIndex - 2), NumberStyles.None, CultureInfo.InvariantCulture, out id)) + return false; + + string name = text.Substring(2, splitIndex - 2); + result = new Emoji(id, name); + return true; + } + return false; + + } + + private string DebuggerDisplay => $"{Name} ({Id})"; + public override string ToString() => Name; + } +} diff --git a/src/Discord.Net/Entities/Messages/IAttachment.cs b/src/Discord.Net.Core/Entities/Messages/IAttachment.cs similarity index 100% rename from src/Discord.Net/Entities/Messages/IAttachment.cs rename to src/Discord.Net.Core/Entities/Messages/IAttachment.cs diff --git a/src/Discord.Net/Entities/Messages/IEmbed.cs b/src/Discord.Net.Core/Entities/Messages/IEmbed.cs similarity index 69% rename from src/Discord.Net/Entities/Messages/IEmbed.cs rename to src/Discord.Net.Core/Entities/Messages/IEmbed.cs index e0080f320..3bca85fd0 100644 --- a/src/Discord.Net/Entities/Messages/IEmbed.cs +++ b/src/Discord.Net.Core/Entities/Messages/IEmbed.cs @@ -6,7 +6,7 @@ string Type { get; } string Title { get; } string Description { get; } - EmbedProvider Provider { get; } - EmbedThumbnail Thumbnail { get; } + EmbedProvider? Provider { get; } + EmbedThumbnail? Thumbnail { get; } } } diff --git a/src/Discord.Net.Core/Entities/Messages/IMessage.cs b/src/Discord.Net.Core/Entities/Messages/IMessage.cs new file mode 100644 index 000000000..9b15d1b07 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/IMessage.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; + +namespace Discord +{ + public interface IMessage : ISnowflakeEntity + { + /// Gets the type of this system message. + MessageType Type { get; } + /// Returns true if this message was sent as a text-to-speech message. + bool IsTTS { get; } + /// Returns true if this message was added to its channel's pinned messages. + bool IsPinned { get; } + /// Returns true if this message was created using a webhook. + bool IsWebhook { get; } + /// Returns the content for this message. + string Content { get; } + /// Gets the time this message was sent. + DateTimeOffset Timestamp { get; } + /// Gets the time of this message's last edit, if any. + DateTimeOffset? EditedTimestamp { get; } + + /// Gets the id of the channel this message was sent to. + IMessageChannel Channel { get; } + /// Gets the author of this message. + IUser Author { get; } + /// Gets the id of the webhook used to created this message, if any. + ulong? WebhookId { get; } + + /// Returns all attachments included in this message. + IReadOnlyCollection Attachments { get; } + /// Returns all embeds included in this message. + IReadOnlyCollection Embeds { get; } + /// Returns all tags included in this message's content. + IReadOnlyCollection Tags { get; } + /// Returns the ids of channels mentioned in this message. + IReadOnlyCollection MentionedChannelIds { get; } + /// Returns the ids of roles mentioned in this message. + IReadOnlyCollection MentionedRoleIds { get; } + /// Returns the ids of users mentioned in this message. + IReadOnlyCollection MentionedUserIds { get; } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Messages/ISystemMessage.cs b/src/Discord.Net.Core/Entities/Messages/ISystemMessage.cs new file mode 100644 index 000000000..2dfaf8f2d --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/ISystemMessage.cs @@ -0,0 +1,6 @@ +namespace Discord +{ + public interface ISystemMessage : IMessage + { + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/ITag.cs b/src/Discord.Net.Core/Entities/Messages/ITag.cs new file mode 100644 index 000000000..27824e6d3 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/ITag.cs @@ -0,0 +1,11 @@ +namespace Discord +{ + public interface ITag + { + int Index { get; } + int Length { get; } + TagType Type { get; } + ulong Key { get; } + object Value { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs new file mode 100644 index 000000000..661a6d59a --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs @@ -0,0 +1,24 @@ +using Discord.API.Rest; +using System; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IUserMessage : IMessage, IDeletable + { + /// Modifies this message. + Task ModifyAsync(Action func, RequestOptions options = null); + /// Adds this message to its channel's pinned messages. + Task PinAsync(RequestOptions options = null); + /// Removes this message from its channel's pinned messages. + Task UnpinAsync(RequestOptions options = null); + + /// Transforms this message's text into a human readable form by resolving its tags. + string Resolve( + TagHandling userHandling = TagHandling.Name, + TagHandling channelHandling = TagHandling.Name, + TagHandling roleHandling = TagHandling.Name, + TagHandling everyoneHandling = TagHandling.Ignore, + TagHandling emojiHandling = TagHandling.Name); + } +} diff --git a/src/Discord.Net/Entities/Messages/MessageType.cs b/src/Discord.Net.Core/Entities/Messages/MessageType.cs similarity index 100% rename from src/Discord.Net/Entities/Messages/MessageType.cs rename to src/Discord.Net.Core/Entities/Messages/MessageType.cs diff --git a/src/Discord.Net.Core/Entities/Messages/Tag.cs b/src/Discord.Net.Core/Entities/Messages/Tag.cs new file mode 100644 index 000000000..06d995e73 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/Tag.cs @@ -0,0 +1,28 @@ +using System.Diagnostics; + +namespace Discord +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class Tag : ITag + { + public TagType Type { get; } + public int Index { get; } + public int Length { get; } + public ulong Key { get; } + public T Value { get; } + + internal Tag(TagType type, int index, int length, ulong key, T value) + { + Type = type; + Index = index; + Length = length; + Key = key; + Value = value; + } + + private string DebuggerDisplay => $"{Value?.ToString() ?? "null"} ({Type})"; + public override string ToString() => $"{Value?.ToString() ?? "null"} ({Type})"; + + object ITag.Value => Value; + } +} diff --git a/src/Discord.Net/Entities/Messages/EveryoneMentionHandling.cs b/src/Discord.Net.Core/Entities/Messages/TagHandling.cs similarity index 60% rename from src/Discord.Net/Entities/Messages/EveryoneMentionHandling.cs rename to src/Discord.Net.Core/Entities/Messages/TagHandling.cs index 5e05606e5..3572a37a5 100644 --- a/src/Discord.Net/Entities/Messages/EveryoneMentionHandling.cs +++ b/src/Discord.Net.Core/Entities/Messages/TagHandling.cs @@ -1,9 +1,11 @@ namespace Discord { - public enum EveryoneMentionHandling + public enum TagHandling { Ignore = 0, Remove, + Name, + FullName, Sanitize } } diff --git a/src/Discord.Net.Core/Entities/Messages/TagType.cs b/src/Discord.Net.Core/Entities/Messages/TagType.cs new file mode 100644 index 000000000..2d93bb3e3 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/TagType.cs @@ -0,0 +1,12 @@ +namespace Discord +{ + public enum TagType + { + UserMention, + ChannelMention, + RoleMention, + EveryoneMention, + HereMention, + Emoji + } +} diff --git a/src/Discord.Net/Entities/Permissions/ChannelPermission.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs similarity index 91% rename from src/Discord.Net/Entities/Permissions/ChannelPermission.cs rename to src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs index 44f3aa20b..5bedfbfae 100644 --- a/src/Discord.Net/Entities/Permissions/ChannelPermission.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs @@ -29,9 +29,11 @@ MoveMembers = 24, UseVAD = 25, - //Nicknames + //General2 //ChangeNickname = 26, //ManageNicknames = 27, ManagePermissions = 28, + //ManageWebhooks = 29, + //ManageEmojis = 30 } } diff --git a/src/Discord.Net/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs similarity index 100% rename from src/Discord.Net/Entities/Permissions/ChannelPermissions.cs rename to src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs diff --git a/src/Discord.Net/Entities/Permissions/GuildPermission.cs b/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs similarity index 91% rename from src/Discord.Net/Entities/Permissions/GuildPermission.cs rename to src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs index 73a449851..e74a4da49 100644 --- a/src/Discord.Net/Entities/Permissions/GuildPermission.cs +++ b/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs @@ -29,9 +29,11 @@ MoveMembers = 24, UseVAD = 25, - //Nicknames + //General2 ChangeNickname = 26, ManageNicknames = 27, ManageRoles = 28, + ManageWebhooks = 29, + ManageEmojis = 30 } } diff --git a/src/Discord.Net/Entities/Permissions/GuildPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs similarity index 91% rename from src/Discord.Net/Entities/Permissions/GuildPermissions.cs rename to src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs index a921724cf..5941fde97 100644 --- a/src/Discord.Net/Entities/Permissions/GuildPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs @@ -11,7 +11,7 @@ namespace Discord public static readonly GuildPermissions None = new GuildPermissions(); /// Gets a GuildPermissions that grants all permissions. //TODO: C#7 Candidate for binary literals - public static readonly GuildPermissions All = new GuildPermissions(Convert.ToUInt64("00011111111100111111110000111111", 2)); + public static readonly GuildPermissions All = new GuildPermissions(Convert.ToUInt64("01111111111100111111110000111111", 2)); /// Gets a packed value representing all the permissions in this GuildPermissions. public ulong RawValue { get; } @@ -67,6 +67,10 @@ namespace Discord public bool ManageNicknames => Permissions.GetValue(RawValue, GuildPermission.ManageNicknames); /// If True, a user may adjust roles. public bool ManageRoles => Permissions.GetValue(RawValue, GuildPermission.ManageRoles); + /// If True, a user may edit the emojis for this guild. + public bool ManageWebhooks => Permissions.GetValue(RawValue, GuildPermission.ManageWebhooks); + /// If True, a user may edit the emojis for this guild. + public bool ManageEmojis => Permissions.GetValue(RawValue, GuildPermission.ManageEmojis); /// Creates a new GuildPermissions with the provided packed value. public GuildPermissions(ulong rawValue) { RawValue = rawValue; } @@ -77,7 +81,7 @@ namespace Discord bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, bool? userExternalEmojis = null, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, bool? moveMembers = null, bool? useVoiceActivation = null, bool? changeNickname = null, bool? manageNicknames = null, - bool? manageRoles = null) + bool? manageRoles = null, bool? manageWebhooks = null, bool? manageEmojis = null) { ulong value = initialValue; @@ -105,6 +109,8 @@ namespace Discord Permissions.SetValue(ref value, changeNickname, GuildPermission.ChangeNickname); Permissions.SetValue(ref value, manageNicknames, GuildPermission.ManageNicknames); Permissions.SetValue(ref value, manageRoles, GuildPermission.ManageRoles); + Permissions.SetValue(ref value, manageWebhooks, GuildPermission.ManageWebhooks); + Permissions.SetValue(ref value, manageEmojis, GuildPermission.ManageEmojis); RawValue = value; } @@ -116,10 +122,11 @@ namespace Discord bool embedLinks = false, bool attachFiles = false, bool readMessageHistory = false, bool mentionEveryone = false, bool useExternalEmojis = false, bool connect = false, bool speak = false, bool muteMembers = false, bool deafenMembers = false, bool moveMembers = false, bool useVoiceActivation = false, bool? changeNickname = false, bool? manageNicknames = false, - bool manageRoles = false) + bool manageRoles = false, bool manageWebhooks = false, bool manageEmojis = false) : this(0, createInstantInvite, manageRoles, kickMembers, banMembers, manageChannels, manageGuild, readMessages, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, mentionEveryone, useExternalEmojis, connect, - speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, changeNickname, manageNicknames, manageRoles) { } + speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, changeNickname, manageNicknames, manageRoles, + manageWebhooks, manageEmojis) { } /// Creates a new GuildPermissions from this one, changing the provided non-null permissions. public GuildPermissions Modify(bool? createInstantInvite = null, bool? kickMembers = null, @@ -128,10 +135,11 @@ namespace Discord bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, bool? useExternalEmojis = null, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, bool? moveMembers = null, bool? useVoiceActivation = null, bool? changeNickname = null, bool? manageNicknames = null, - bool? manageRoles = null) + bool? manageRoles = null, bool? manageWebhooks = null, bool? manageEmojis = null) => new GuildPermissions(RawValue, createInstantInvite, manageRoles, kickMembers, banMembers, manageChannels, manageGuild, readMessages, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, mentionEveryone, useExternalEmojis, connect, - speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, changeNickname, manageNicknames, manageRoles); + speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, changeNickname, manageNicknames, manageRoles, + manageWebhooks, manageEmojis); public bool Has(GuildPermission permission) => Permissions.GetValue(RawValue, permission); diff --git a/src/Discord.Net/Entities/Permissions/Overwrite.cs b/src/Discord.Net.Core/Entities/Permissions/Overwrite.cs similarity index 96% rename from src/Discord.Net/Entities/Permissions/Overwrite.cs rename to src/Discord.Net.Core/Entities/Permissions/Overwrite.cs index 7333d93e1..ff5b00623 100644 --- a/src/Discord.Net/Entities/Permissions/Overwrite.cs +++ b/src/Discord.Net.Core/Entities/Permissions/Overwrite.cs @@ -19,7 +19,7 @@ namespace Discord Permissions = permissions; } - internal Overwrite(Model model) + public Overwrite(Model model) : this(model.TargetId, model.TargetType, new OverwritePermissions(model.Allow, model.Deny)) { } } } diff --git a/src/Discord.Net/Entities/Permissions/OverwritePermissions.cs b/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs similarity index 100% rename from src/Discord.Net/Entities/Permissions/OverwritePermissions.cs rename to src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs diff --git a/src/Discord.Net/Entities/Permissions/PermValue.cs b/src/Discord.Net.Core/Entities/Permissions/PermValue.cs similarity index 100% rename from src/Discord.Net/Entities/Permissions/PermValue.cs rename to src/Discord.Net.Core/Entities/Permissions/PermValue.cs diff --git a/src/Discord.Net/Entities/Permissions/PermissionTarget.cs b/src/Discord.Net.Core/Entities/Permissions/PermissionTarget.cs similarity index 100% rename from src/Discord.Net/Entities/Permissions/PermissionTarget.cs rename to src/Discord.Net.Core/Entities/Permissions/PermissionTarget.cs diff --git a/src/Discord.Net/Entities/Roles/Color.cs b/src/Discord.Net.Core/Entities/Roles/Color.cs similarity index 98% rename from src/Discord.Net/Entities/Roles/Color.cs rename to src/Discord.Net.Core/Entities/Roles/Color.cs index 0eb562e80..563917959 100644 --- a/src/Discord.Net/Entities/Roles/Color.cs +++ b/src/Discord.Net.Core/Entities/Roles/Color.cs @@ -1,7 +1,7 @@ using System; using System.Diagnostics; -namespace Discord +namespace Discord { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public struct Color diff --git a/src/Discord.Net/Entities/Roles/IRole.cs b/src/Discord.Net.Core/Entities/Roles/IRole.cs similarity index 61% rename from src/Discord.Net/Entities/Roles/IRole.cs rename to src/Discord.Net.Core/Entities/Roles/IRole.cs index 29975be46..aa34fb019 100644 --- a/src/Discord.Net/Entities/Roles/IRole.cs +++ b/src/Discord.Net.Core/Entities/Roles/IRole.cs @@ -1,18 +1,22 @@ -using System; -using System.Collections.Generic; +using Discord.API.Rest; +using System; using System.Threading.Tasks; -using Discord.API.Rest; namespace Discord { - public interface IRole : IDeletable, ISnowflakeEntity + public interface IRole : ISnowflakeEntity, IDeletable, IMentionable { + /// Gets the guild owning this role. + IGuild Guild { get; } + /// Gets the color given to users of this role. Color Color { get; } /// Returns true if users of this role are separated in the user list. bool IsHoisted { get; } /// Returns true if this role is automatically managed by Discord. bool IsManaged { get; } + /// Returns true if this role may be mentioned in messages. + bool IsMentionable { get; } /// Gets the name of this role. string Name { get; } /// Gets the permissions granted to members of this role. @@ -20,10 +24,7 @@ namespace Discord /// Gets this role's position relative to other roles in the same guild. int Position { get; } - /// Gets the id of the guild owning this role. - ulong GuildId { get; } - - /// Modifies this role. - Task ModifyAsync(Action func); + ///// Modifies this role. + Task ModifyAsync(Action func, RequestOptions options = null); } } \ No newline at end of file diff --git a/src/Discord.Net/Entities/Users/Game.cs b/src/Discord.Net.Core/Entities/Users/Game.cs similarity index 67% rename from src/Discord.Net/Entities/Users/Game.cs rename to src/Discord.Net.Core/Entities/Users/Game.cs index 9b5d891ef..5bed84ddb 100644 --- a/src/Discord.Net/Entities/Users/Game.cs +++ b/src/Discord.Net.Core/Entities/Users/Game.cs @@ -4,7 +4,7 @@ using Model = Discord.API.Game; namespace Discord { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class Game + public struct Game { public string Name { get; } public string StreamUrl { get; } @@ -16,10 +16,14 @@ namespace Discord StreamUrl = streamUrl; StreamType = type; } - public Game(string name) + private Game(string name) : this(name, null, StreamType.NotStreaming) { } - internal Game(Model model) - : this(model.Name, model.StreamUrl.GetValueOrDefault(null), model.StreamType.GetValueOrDefault(null) ?? StreamType.NotStreaming) { } + internal static Game Create(Model model) + { + return new Game(model.Name, + model.StreamUrl.GetValueOrDefault(null), + model.StreamType.GetValueOrDefault(null) ?? StreamType.NotStreaming); + } public override string ToString() => Name; private string DebuggerDisplay => StreamUrl != null ? $"{Name} ({StreamUrl})" : Name; diff --git a/src/Discord.Net/Entities/Users/IConnection.cs b/src/Discord.Net.Core/Entities/Users/IConnection.cs similarity index 100% rename from src/Discord.Net/Entities/Users/IConnection.cs rename to src/Discord.Net.Core/Entities/Users/IConnection.cs diff --git a/src/Discord.Net.Core/Entities/Users/IGroupUser.cs b/src/Discord.Net.Core/Entities/Users/IGroupUser.cs new file mode 100644 index 000000000..dd046a5a8 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Users/IGroupUser.cs @@ -0,0 +1,8 @@ +namespace Discord +{ + public interface IGroupUser : IUser, IVoiceState + { + ///// Kicks this user from this group. + //Task KickAsync(RequestOptions options = null); + } +} diff --git a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs new file mode 100644 index 000000000..7763a14ae --- /dev/null +++ b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs @@ -0,0 +1,30 @@ +using Discord.API.Rest; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Discord +{ + /// A Guild-User pairing. + public interface IGuildUser : IUser, IVoiceState + { + /// Gets when this user joined this guild. + DateTimeOffset? JoinedAt { get; } + /// Gets the nickname for this user. + string Nickname { get; } + GuildPermissions GuildPermissions { get; } + + /// Gets the id of the guild for this user. + ulong GuildId { get; } + /// Returns a collection of the ids of the roles this user is a member of in this guild, including the guild's @everyone role. + IReadOnlyCollection RoleIds { get; } + + /// Gets the level permissions granted to this user to a given channel. + ChannelPermissions GetPermissions(IGuildChannel channel); + + /// Kicks this user from this guild. + Task KickAsync(RequestOptions options = null); + /// Modifies this user's properties in this guild. + Task ModifyAsync(Action func, RequestOptions options = null); + } +} diff --git a/src/Discord.Net/Entities/Users/IPresence.cs b/src/Discord.Net.Core/Entities/Users/IPresence.cs similarity index 89% rename from src/Discord.Net/Entities/Users/IPresence.cs rename to src/Discord.Net.Core/Entities/Users/IPresence.cs index af7be998a..7f182241b 100644 --- a/src/Discord.Net/Entities/Users/IPresence.cs +++ b/src/Discord.Net.Core/Entities/Users/IPresence.cs @@ -3,7 +3,7 @@ public interface IPresence { /// Gets the game this user is currently playing, if any. - Game Game { get; } + Game? Game { get; } /// Gets the current status of this user. UserStatus Status { get; } } diff --git a/src/Discord.Net/Entities/Users/ISelfUser.cs b/src/Discord.Net.Core/Entities/Users/ISelfUser.cs similarity index 75% rename from src/Discord.Net/Entities/Users/ISelfUser.cs rename to src/Discord.Net.Core/Entities/Users/ISelfUser.cs index b6803ccf6..a6e8f80e8 100644 --- a/src/Discord.Net/Entities/Users/ISelfUser.cs +++ b/src/Discord.Net.Core/Entities/Users/ISelfUser.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; namespace Discord { - public interface ISelfUser : IUser, IUpdateable + public interface ISelfUser : IUser { /// Gets the email associated with this user. string Email { get; } @@ -13,7 +13,7 @@ namespace Discord /// Returns true if this user has enabled MFA on their account. bool IsMfaEnabled { get; } - Task ModifyAsync(Action func); - Task ModifyStatusAsync(Action func); + Task ModifyAsync(Action func, RequestOptions options = null); + Task ModifyStatusAsync(Action func, RequestOptions options = null); } } \ No newline at end of file diff --git a/src/Discord.Net/Entities/Users/IUser.cs b/src/Discord.Net.Core/Entities/Users/IUser.cs similarity index 53% rename from src/Discord.Net/Entities/Users/IUser.cs rename to src/Discord.Net.Core/Entities/Users/IUser.cs index 5eef8231c..c02f8aeca 100644 --- a/src/Discord.Net/Entities/Users/IUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IUser.cs @@ -1,7 +1,11 @@ +using System.Threading.Tasks; + namespace Discord { public interface IUser : ISnowflakeEntity, IMentionable, IPresence { + /// Gets the id of this user's avatar. + string AvatarId { get; } /// Gets the url to this user's avatar. string AvatarUrl { get; } /// Gets the per-username unique id for this user. @@ -12,5 +16,10 @@ namespace Discord bool IsBot { get; } /// Gets the username for this user. string Username { get; } + + /// Returns a private message channel to this user, creating one if it does not already exist. + Task GetDMChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// Returns a private message channel to this user, creating one if it does not already exist. + Task CreateDMChannelAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net/Entities/Users/IVoiceState.cs b/src/Discord.Net.Core/Entities/Users/IVoiceState.cs similarity index 100% rename from src/Discord.Net/Entities/Users/IVoiceState.cs rename to src/Discord.Net.Core/Entities/Users/IVoiceState.cs diff --git a/src/Discord.Net/Entities/Users/StreamType.cs b/src/Discord.Net.Core/Entities/Users/StreamType.cs similarity index 100% rename from src/Discord.Net/Entities/Users/StreamType.cs rename to src/Discord.Net.Core/Entities/Users/StreamType.cs diff --git a/src/Discord.Net/Entities/Users/UserStatus.cs b/src/Discord.Net.Core/Entities/Users/UserStatus.cs similarity index 61% rename from src/Discord.Net/Entities/Users/UserStatus.cs rename to src/Discord.Net.Core/Entities/Users/UserStatus.cs index a653c8b9d..d183c139d 100644 --- a/src/Discord.Net/Entities/Users/UserStatus.cs +++ b/src/Discord.Net.Core/Entities/Users/UserStatus.cs @@ -5,7 +5,9 @@ Unknown, Online, Idle, - Offline, - DoNotDisturb + AFK, + DoNotDisturb, + Invisible, + Offline } } diff --git a/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs b/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs new file mode 100644 index 000000000..f52edd719 --- /dev/null +++ b/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord +{ + public static class AsyncEnumerableExtensions + { + public static async Task> Flatten(this IAsyncEnumerable> source) + { + return (await source.ToArray().ConfigureAwait(false)).SelectMany(x => x); + } + } +} diff --git a/src/Discord.Net/Extensions/CollectionExtensions.cs b/src/Discord.Net.Core/Extensions/CollectionExtensions.cs similarity index 100% rename from src/Discord.Net/Extensions/CollectionExtensions.cs rename to src/Discord.Net.Core/Extensions/CollectionExtensions.cs diff --git a/src/Discord.Net/Extensions/DiscordClientExtensions.cs b/src/Discord.Net.Core/Extensions/DiscordClientExtensions.cs similarity index 100% rename from src/Discord.Net/Extensions/DiscordClientExtensions.cs rename to src/Discord.Net.Core/Extensions/DiscordClientExtensions.cs diff --git a/src/Discord.Net/Extensions/GuildExtensions.cs b/src/Discord.Net.Core/Extensions/GuildExtensions.cs similarity index 100% rename from src/Discord.Net/Extensions/GuildExtensions.cs rename to src/Discord.Net.Core/Extensions/GuildExtensions.cs diff --git a/src/Discord.Net/Extensions/GuildUserExtensions.cs b/src/Discord.Net.Core/Extensions/GuildUserExtensions.cs similarity index 75% rename from src/Discord.Net/Extensions/GuildUserExtensions.cs rename to src/Discord.Net.Core/Extensions/GuildUserExtensions.cs index 492b5b76b..57d6a13dc 100644 --- a/src/Discord.Net/Extensions/GuildUserExtensions.cs +++ b/src/Discord.Net.Core/Extensions/GuildUserExtensions.cs @@ -9,11 +9,11 @@ namespace Discord public static Task AddRolesAsync(this IGuildUser user, params IRole[] roles) => AddRolesAsync(user, (IEnumerable)roles); public static Task AddRolesAsync(this IGuildUser user, IEnumerable roles) - => user.ModifyAsync(x => x.Roles = user.Roles.Concat(roles)); + => user.ModifyAsync(x => x.RoleIds = user.RoleIds.Concat(roles.Select(y => y.Id)).ToArray()); public static Task RemoveRolesAsync(this IGuildUser user, params IRole[] roles) => RemoveRolesAsync(user, (IEnumerable)roles); public static Task RemoveRolesAsync(this IGuildUser user, IEnumerable roles) - => user.ModifyAsync(x => x.Roles = user.Roles.Except(roles)); + => user.ModifyAsync(x => x.RoleIds = user.RoleIds.Except(roles.Select(y => y.Id)).ToArray()); } } diff --git a/src/Discord.Net/Extensions/TaskCompletionSourceExtensions.cs b/src/Discord.Net.Core/Extensions/TaskCompletionSourceExtensions.cs similarity index 100% rename from src/Discord.Net/Extensions/TaskCompletionSourceExtensions.cs rename to src/Discord.Net.Core/Extensions/TaskCompletionSourceExtensions.cs diff --git a/src/Discord.Net/Format.cs b/src/Discord.Net.Core/Format.cs similarity index 92% rename from src/Discord.Net/Format.cs rename to src/Discord.Net.Core/Format.cs index d31f0ad0a..0039836d8 100644 --- a/src/Discord.Net/Format.cs +++ b/src/Discord.Net.Core/Format.cs @@ -14,7 +14,7 @@ /// Returns a markdown-formatted string with strikethrough formatting. public static string Strikethrough(string text) => $"~~{text}~~"; - /// Returns a markdown-formatted string with strikeout formatting. + /// Returns a markdown-formatted string with codeblock formatting. public static string Code(string text, string language = null) { if (language != null || text.Contains("\n")) @@ -27,9 +27,7 @@ public static string Sanitize(string text) { foreach (string unsafeChar in SensitiveCharacters) - { text = text.Replace(unsafeChar, $"\\{unsafeChar}"); - } return text; } } diff --git a/src/Discord.Net/IDiscordClient.cs b/src/Discord.Net.Core/IDiscordClient.cs similarity index 61% rename from src/Discord.Net/IDiscordClient.cs rename to src/Discord.Net.Core/IDiscordClient.cs index 0b3474ed9..4131b1796 100644 --- a/src/Discord.Net/IDiscordClient.cs +++ b/src/Discord.Net.Core/IDiscordClient.cs @@ -1,5 +1,4 @@ using Discord.API; -using Discord.Logging; using System; using System.Collections.Generic; using System.IO; @@ -7,36 +6,30 @@ using System.Threading.Tasks; namespace Discord { - //TODO: Add docstrings - //TODO: Docstrings should explain when REST requests are sent and how many public interface IDiscordClient : IDisposable { ConnectionState ConnectionState { get; } - DiscordRestApiClient ApiClient { get; } - ILogManager LogManager { get; } + ISelfUser CurrentUser { get; } Task ConnectAsync(); Task DisconnectAsync(); Task GetApplicationInfoAsync(); - Task GetChannelAsync(ulong id); - Task> GetPrivateChannelsAsync(); + Task GetChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload); + Task> GetPrivateChannelsAsync(CacheMode mode = CacheMode.AllowDownload); Task> GetConnectionsAsync(); - Task GetGuildAsync(ulong id); - Task> GetGuildsAsync(); - Task> GetGuildSummariesAsync(); + Task GetGuildAsync(ulong id, CacheMode mode = CacheMode.AllowDownload); + Task> GetGuildsAsync(CacheMode mode = CacheMode.AllowDownload); Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null); Task GetInviteAsync(string inviteId); - Task GetUserAsync(ulong id); + Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload); Task GetUserAsync(string username, string discriminator); - Task GetCurrentUserAsync(); - Task> QueryUsersAsync(string query, int limit); Task> GetVoiceRegionsAsync(); Task GetVoiceRegionAsync(string id); diff --git a/src/Discord.Net/Logging/LogManager.cs b/src/Discord.Net.Core/Logging/LogManager.cs similarity index 55% rename from src/Discord.Net/Logging/LogManager.cs rename to src/Discord.Net.Core/Logging/LogManager.cs index e37b2bce6..104e02835 100644 --- a/src/Discord.Net/Logging/LogManager.cs +++ b/src/Discord.Net.Core/Logging/LogManager.cs @@ -3,9 +3,10 @@ using System.Threading.Tasks; namespace Discord.Logging { - internal class LogManager : ILogManager, ILogger + internal class LogManager { public LogSeverity Level { get; } + public Logger ClientLogger { get; } public event Func Message { add { _messageEvent.Add(value); } remove { _messageEvent.Remove(value); } } private readonly AsyncEvent> _messageEvent = new AsyncEvent>(); @@ -13,6 +14,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) @@ -30,21 +32,6 @@ namespace Discord.Logging if (severity <= Level) await _messageEvent.InvokeAsync(new LogMessage(severity, source, null, ex)).ConfigureAwait(false); } - async Task ILogger.LogAsync(LogSeverity severity, string message, Exception ex) - { - if (severity <= Level) - await _messageEvent.InvokeAsync(new LogMessage(severity, "Discord", message, ex)).ConfigureAwait(false); - } - async Task ILogger.LogAsync(LogSeverity severity, FormattableString message, Exception ex) - { - if (severity <= Level) - await _messageEvent.InvokeAsync(new LogMessage(severity, "Discord", message.ToString(), ex)).ConfigureAwait(false); - } - async Task ILogger.LogAsync(LogSeverity severity, Exception ex) - { - if (severity <= Level) - await _messageEvent.InvokeAsync(new LogMessage(severity, "Discord", null, ex)).ConfigureAwait(false); - } public Task ErrorAsync(string source, string message, Exception ex = null) => LogAsync(LogSeverity.Error, source, message, ex); @@ -52,12 +39,6 @@ namespace Discord.Logging => LogAsync(LogSeverity.Error, source, message, ex); public Task ErrorAsync(string source, Exception ex) => LogAsync(LogSeverity.Error, source, ex); - Task ILogger.ErrorAsync(string message, Exception ex) - => LogAsync(LogSeverity.Error, "Discord", message, ex); - Task ILogger.ErrorAsync(FormattableString message, Exception ex) - => LogAsync(LogSeverity.Error, "Discord", message, ex); - Task ILogger.ErrorAsync(Exception ex) - => LogAsync(LogSeverity.Error, "Discord", ex); public Task WarningAsync(string source, string message, Exception ex = null) => LogAsync(LogSeverity.Warning, source, message, ex); @@ -65,12 +46,6 @@ namespace Discord.Logging => LogAsync(LogSeverity.Warning, source, message, ex); public Task WarningAsync(string source, Exception ex) => LogAsync(LogSeverity.Warning, source, ex); - Task ILogger.WarningAsync(string message, Exception ex) - => LogAsync(LogSeverity.Warning, "Discord", message, ex); - Task ILogger.WarningAsync(FormattableString message, Exception ex) - => LogAsync(LogSeverity.Warning, "Discord", message, ex); - Task ILogger.WarningAsync(Exception ex) - => LogAsync(LogSeverity.Warning, "Discord", ex); public Task InfoAsync(string source, string message, Exception ex = null) => LogAsync(LogSeverity.Info, source, message, ex); @@ -78,12 +53,6 @@ namespace Discord.Logging => LogAsync(LogSeverity.Info, source, message, ex); public Task InfoAsync(string source, Exception ex) => LogAsync(LogSeverity.Info, source, ex); - Task ILogger.InfoAsync(string message, Exception ex) - => LogAsync(LogSeverity.Info, "Discord", message, ex); - Task ILogger.InfoAsync(FormattableString message, Exception ex) - => LogAsync(LogSeverity.Info, "Discord", message, ex); - Task ILogger.InfoAsync(Exception ex) - => LogAsync(LogSeverity.Info, "Discord", ex); public Task VerboseAsync(string source, string message, Exception ex = null) => LogAsync(LogSeverity.Verbose, source, message, ex); @@ -91,12 +60,6 @@ namespace Discord.Logging => LogAsync(LogSeverity.Verbose, source, message, ex); public Task VerboseAsync(string source, Exception ex) => LogAsync(LogSeverity.Verbose, source, ex); - Task ILogger.VerboseAsync(string message, Exception ex) - => LogAsync(LogSeverity.Verbose, "Discord", message, ex); - Task ILogger.VerboseAsync(FormattableString message, Exception ex) - => LogAsync(LogSeverity.Verbose, "Discord", message, ex); - Task ILogger.VerboseAsync(Exception ex) - => LogAsync(LogSeverity.Verbose, "Discord", ex); public Task DebugAsync(string source, string message, Exception ex = null) => LogAsync(LogSeverity.Debug, source, message, ex); @@ -104,13 +67,12 @@ namespace Discord.Logging => LogAsync(LogSeverity.Debug, source, message, ex); public Task DebugAsync(string source, Exception ex) => LogAsync(LogSeverity.Debug, source, ex); - Task ILogger.DebugAsync(string message, Exception ex) - => LogAsync(LogSeverity.Debug, "Discord", message, ex); - Task ILogger.DebugAsync(FormattableString message, Exception ex) - => LogAsync(LogSeverity.Debug, "Discord", message, ex); - Task ILogger.DebugAsync(Exception ex) - => LogAsync(LogSeverity.Debug, "Discord", ex); - public ILogger CreateLogger(string name) => new Logger(this, name); + 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); + } } } diff --git a/src/Discord.Net/Logging/LogMessage.cs b/src/Discord.Net.Core/Logging/LogMessage.cs similarity index 100% rename from src/Discord.Net/Logging/LogMessage.cs rename to src/Discord.Net.Core/Logging/LogMessage.cs diff --git a/src/Discord.Net/LogSeverity.cs b/src/Discord.Net.Core/Logging/LogSeverity.cs similarity index 100% rename from src/Discord.Net/LogSeverity.cs rename to src/Discord.Net.Core/Logging/LogSeverity.cs diff --git a/src/Discord.Net/Logging/Logger.cs b/src/Discord.Net.Core/Logging/Logger.cs similarity index 98% rename from src/Discord.Net/Logging/Logger.cs rename to src/Discord.Net.Core/Logging/Logger.cs index 2255f4451..c871c0b26 100644 --- a/src/Discord.Net/Logging/Logger.cs +++ b/src/Discord.Net.Core/Logging/Logger.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; namespace Discord.Logging { - internal class Logger : ILogger + internal class Logger { private readonly LogManager _manager; diff --git a/src/Discord.Net/LoginState.cs b/src/Discord.Net.Core/LoginState.cs similarity index 100% rename from src/Discord.Net/LoginState.cs rename to src/Discord.Net.Core/LoginState.cs diff --git a/src/Discord.Net/Net/Converters/DiscordContractResolver.cs b/src/Discord.Net.Core/Net/Converters/DiscordContractResolver.cs similarity index 80% rename from src/Discord.Net/Net/Converters/DiscordContractResolver.cs rename to src/Discord.Net.Core/Net/Converters/DiscordContractResolver.cs index c387db0bf..0a61806b6 100644 --- a/src/Discord.Net/Net/Converters/DiscordContractResolver.cs +++ b/src/Discord.Net.Core/Net/Converters/DiscordContractResolver.cs @@ -3,13 +3,12 @@ using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Reflection; namespace Discord.Net.Converters { - public class DiscordContractResolver : DefaultContractResolver + internal class DiscordContractResolver : DefaultContractResolver { private static readonly TypeInfo _ienumerable = typeof(IEnumerable).GetTypeInfo(); private static readonly MethodInfo _shouldSerialize = typeof(DiscordContractResolver).GetTypeInfo().GetDeclaredMethod("ShouldSerialize"); @@ -25,8 +24,9 @@ namespace Discord.Net.Converters { JsonConverter converter; var type = propInfo.PropertyType; + Type genericType = type.IsConstructedGenericType ? type.GetGenericTypeDefinition() : null; - if (type.IsConstructedGenericType && type.GetGenericTypeDefinition() == typeof(Optional<>)) + if (genericType == typeof(Optional<>)) { var typeInput = propInfo.DeclaringType; var innerTypeOutput = type.GenericTypeArguments[0]; @@ -47,6 +47,20 @@ namespace Discord.Net.Converters instanceField.SetValue(null, converter); } } + else if (genericType == typeof(ObjectOrId<>)) + { + var innerTypeOutput = type.GenericTypeArguments[0]; + + var converterType = typeof(ObjectOrIdConverter<>).MakeGenericType(innerTypeOutput).GetTypeInfo(); + var instanceField = converterType.GetDeclaredField("Instance"); + converter = instanceField.GetValue(null) as JsonConverter; + if (converter == null) + { + var innerConverter = GetConverter(propInfo, innerTypeOutput); + converter = converterType.DeclaredConstructors.First().Invoke(new object[] { innerConverter }) as JsonConverter; + instanceField.SetValue(null, converter); + } + } else converter = GetConverter(propInfo, type); diff --git a/src/Discord.Net/Net/Converters/ImageConverter.cs b/src/Discord.Net.Core/Net/Converters/ImageConverter.cs similarity index 95% rename from src/Discord.Net/Net/Converters/ImageConverter.cs rename to src/Discord.Net.Core/Net/Converters/ImageConverter.cs index 88e205688..79e8c984d 100644 --- a/src/Discord.Net/Net/Converters/ImageConverter.cs +++ b/src/Discord.Net.Core/Net/Converters/ImageConverter.cs @@ -4,7 +4,7 @@ using System; namespace Discord.Net.Converters { - public class ImageConverter : JsonConverter + internal class ImageConverter : JsonConverter { public static readonly ImageConverter Instance = new ImageConverter(); diff --git a/src/Discord.Net/Net/Converters/NullableUInt64Converter.cs b/src/Discord.Net.Core/Net/Converters/NullableUInt64Converter.cs similarity index 94% rename from src/Discord.Net/Net/Converters/NullableUInt64Converter.cs rename to src/Discord.Net.Core/Net/Converters/NullableUInt64Converter.cs index 050ac7c32..a2e409292 100644 --- a/src/Discord.Net/Net/Converters/NullableUInt64Converter.cs +++ b/src/Discord.Net.Core/Net/Converters/NullableUInt64Converter.cs @@ -4,7 +4,7 @@ using System.Globalization; namespace Discord.Net.Converters { - public class NullableUInt64Converter : JsonConverter + internal class NullableUInt64Converter : JsonConverter { public static readonly NullableUInt64Converter Instance = new NullableUInt64Converter(); diff --git a/src/Discord.Net.Core/Net/Converters/ObjectOrIdConverter.cs b/src/Discord.Net.Core/Net/Converters/ObjectOrIdConverter.cs new file mode 100644 index 000000000..229c8cd87 --- /dev/null +++ b/src/Discord.Net.Core/Net/Converters/ObjectOrIdConverter.cs @@ -0,0 +1,39 @@ +using Discord.API; +using Newtonsoft.Json; +using System; + +namespace Discord.Net.Converters +{ + internal class ObjectOrIdConverter : JsonConverter + { + internal static ObjectOrIdConverter Instance; + + private readonly JsonConverter _innerConverter; + + public override bool CanConvert(Type objectType) => true; + public override bool CanRead => true; + public override bool CanWrite => false; + + public ObjectOrIdConverter(JsonConverter innerConverter) + { + _innerConverter = innerConverter; + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + switch (reader.TokenType) + { + case JsonToken.String: + case JsonToken.Integer: + return new ObjectOrId(ulong.Parse(reader.ReadAsString())); + default: + return new ObjectOrId(serializer.Deserialize(reader)); + } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new InvalidOperationException(); + } + } +} diff --git a/src/Discord.Net/Net/Converters/OptionalConverter.cs b/src/Discord.Net.Core/Net/Converters/OptionalConverter.cs similarity index 91% rename from src/Discord.Net/Net/Converters/OptionalConverter.cs rename to src/Discord.Net.Core/Net/Converters/OptionalConverter.cs index 260d642d4..c6965ec61 100644 --- a/src/Discord.Net/Net/Converters/OptionalConverter.cs +++ b/src/Discord.Net.Core/Net/Converters/OptionalConverter.cs @@ -3,9 +3,9 @@ using System; namespace Discord.Net.Converters { - public class OptionalConverter : JsonConverter + internal class OptionalConverter : JsonConverter { - public static OptionalConverter Instance; + internal static OptionalConverter Instance; private readonly JsonConverter _innerConverter; diff --git a/src/Discord.Net/Net/Converters/PermissionTargetConverter.cs b/src/Discord.Net.Core/Net/Converters/PermissionTargetConverter.cs similarity index 95% rename from src/Discord.Net/Net/Converters/PermissionTargetConverter.cs rename to src/Discord.Net.Core/Net/Converters/PermissionTargetConverter.cs index 6dc74932f..0ed566a84 100644 --- a/src/Discord.Net/Net/Converters/PermissionTargetConverter.cs +++ b/src/Discord.Net.Core/Net/Converters/PermissionTargetConverter.cs @@ -3,7 +3,7 @@ using System; namespace Discord.Net.Converters { - public class PermissionTargetConverter : JsonConverter + internal class PermissionTargetConverter : JsonConverter { public static readonly PermissionTargetConverter Instance = new PermissionTargetConverter(); diff --git a/src/Discord.Net/Net/Converters/StringEntityConverter.cs b/src/Discord.Net.Core/Net/Converters/StringEntityConverter.cs similarity index 93% rename from src/Discord.Net/Net/Converters/StringEntityConverter.cs rename to src/Discord.Net.Core/Net/Converters/StringEntityConverter.cs index 902fb1a75..d7dd58d71 100644 --- a/src/Discord.Net/Net/Converters/StringEntityConverter.cs +++ b/src/Discord.Net.Core/Net/Converters/StringEntityConverter.cs @@ -3,7 +3,7 @@ using System; namespace Discord.Net.Converters { - public class StringEntityConverter : JsonConverter + internal class StringEntityConverter : JsonConverter { public static readonly StringEntityConverter Instance = new StringEntityConverter(); diff --git a/src/Discord.Net/Net/Converters/UInt64ArrayConverter.cs b/src/Discord.Net.Core/Net/Converters/UInt64ArrayConverter.cs similarity index 96% rename from src/Discord.Net/Net/Converters/UInt64ArrayConverter.cs rename to src/Discord.Net.Core/Net/Converters/UInt64ArrayConverter.cs index d0a8d170b..a5c2d2096 100644 --- a/src/Discord.Net/Net/Converters/UInt64ArrayConverter.cs +++ b/src/Discord.Net.Core/Net/Converters/UInt64ArrayConverter.cs @@ -5,7 +5,7 @@ using System.Globalization; namespace Discord.Net.Converters { - public class UInt64ArrayConverter : JsonConverter + internal class UInt64ArrayConverter : JsonConverter { public static readonly UInt64ArrayConverter Instance = new UInt64ArrayConverter(); diff --git a/src/Discord.Net/Net/Converters/UInt64Converter.cs b/src/Discord.Net.Core/Net/Converters/UInt64Converter.cs similarity index 94% rename from src/Discord.Net/Net/Converters/UInt64Converter.cs rename to src/Discord.Net.Core/Net/Converters/UInt64Converter.cs index 6cbcd81f6..27cbe9290 100644 --- a/src/Discord.Net/Net/Converters/UInt64Converter.cs +++ b/src/Discord.Net.Core/Net/Converters/UInt64Converter.cs @@ -4,7 +4,7 @@ using System.Globalization; namespace Discord.Net.Converters { - public class UInt64Converter : JsonConverter + internal class UInt64Converter : JsonConverter { public static readonly UInt64Converter Instance = new UInt64Converter(); diff --git a/src/Discord.Net/Net/Converters/UInt64EntityConverter.cs b/src/Discord.Net.Core/Net/Converters/UInt64EntityConverter.cs similarity index 93% rename from src/Discord.Net/Net/Converters/UInt64EntityConverter.cs rename to src/Discord.Net.Core/Net/Converters/UInt64EntityConverter.cs index 8a102ab22..b8d8f1057 100644 --- a/src/Discord.Net/Net/Converters/UInt64EntityConverter.cs +++ b/src/Discord.Net.Core/Net/Converters/UInt64EntityConverter.cs @@ -4,7 +4,7 @@ using System.Globalization; namespace Discord.Net.Converters { - public class UInt64EntityConverter : JsonConverter + internal class UInt64EntityConverter : JsonConverter { public static readonly UInt64EntityConverter Instance = new UInt64EntityConverter(); diff --git a/src/Discord.Net/Net/Converters/UserStatusConverter.cs b/src/Discord.Net.Core/Net/Converters/UserStatusConverter.cs similarity index 84% rename from src/Discord.Net/Net/Converters/UserStatusConverter.cs rename to src/Discord.Net.Core/Net/Converters/UserStatusConverter.cs index 5c01b969b..c0a287c16 100644 --- a/src/Discord.Net/Net/Converters/UserStatusConverter.cs +++ b/src/Discord.Net.Core/Net/Converters/UserStatusConverter.cs @@ -3,7 +3,7 @@ using System; namespace Discord.Net.Converters { - public class UserStatusConverter : JsonConverter + internal class UserStatusConverter : JsonConverter { public static readonly UserStatusConverter Instance = new UserStatusConverter(); @@ -19,10 +19,12 @@ namespace Discord.Net.Converters return UserStatus.Online; case "idle": return UserStatus.Idle; - case "offline": - return UserStatus.Offline; case "dnd": return UserStatus.DoNotDisturb; + case "invisible": + return UserStatus.Invisible; //Should never happen + case "offline": + return UserStatus.Offline; default: throw new JsonSerializationException("Unknown user status"); } @@ -36,14 +38,18 @@ namespace Discord.Net.Converters writer.WriteValue("online"); break; case UserStatus.Idle: + case UserStatus.AFK: writer.WriteValue("idle"); break; - case UserStatus.Offline: - writer.WriteValue("offline"); - break; case UserStatus.DoNotDisturb: writer.WriteValue("dnd"); break; + case UserStatus.Invisible: + writer.WriteValue("invisible"); + break; + case UserStatus.Offline: + writer.WriteValue("offline"); + break; default: throw new JsonSerializationException("Invalid user status"); } diff --git a/src/Discord.Net/Net/HttpException.cs b/src/Discord.Net.Core/Net/HttpException.cs similarity index 100% rename from src/Discord.Net/Net/HttpException.cs rename to src/Discord.Net.Core/Net/HttpException.cs diff --git a/src/Discord.Net.Core/Net/Queue/ClientBucket.cs b/src/Discord.Net.Core/Net/Queue/ClientBucket.cs new file mode 100644 index 000000000..93e5cfd23 --- /dev/null +++ b/src/Discord.Net.Core/Net/Queue/ClientBucket.cs @@ -0,0 +1,26 @@ +using System.Collections.Immutable; + +namespace Discord.Net.Queue +{ + public struct ClientBucket + { + private static readonly ImmutableDictionary _defs; + static ClientBucket() + { + var builder = ImmutableDictionary.CreateBuilder(); + builder.Add("", new ClientBucket(5, 5)); + _defs = builder.ToImmutable(); + } + + public static ClientBucket Get(string id) => _defs[id]; + + public int WindowCount { get; } + public int WindowSeconds { get; } + + public ClientBucket(int count, int seconds) + { + WindowCount = count; + WindowSeconds = seconds; + } + } +} diff --git a/src/Discord.Net.Core/Net/Queue/RequestQueue.cs b/src/Discord.Net.Core/Net/Queue/RequestQueue.cs new file mode 100644 index 000000000..28caca1c2 --- /dev/null +++ b/src/Discord.Net.Core/Net/Queue/RequestQueue.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.Queue +{ + public class RequestQueue : IDisposable + { + public event Func RateLimitTriggered; + + internal TokenType TokenType { get; set; } + + private readonly ConcurrentDictionary _buckets; + private readonly SemaphoreSlim _tokenLock; + private CancellationTokenSource _clearToken; + private CancellationToken _parentToken; + private CancellationToken _requestCancelToken; //Parent token + Clear token + private CancellationTokenSource _cancelToken; //Dispose token + private DateTimeOffset _waitUntil; + + private Task _cleanupTask; + + public RequestQueue() + { + _tokenLock = new SemaphoreSlim(1, 1); + + _clearToken = new CancellationTokenSource(); + _cancelToken = new CancellationTokenSource(); + _requestCancelToken = CancellationToken.None; + _parentToken = CancellationToken.None; + + _buckets = new ConcurrentDictionary(); + + _cleanupTask = RunCleanup(); + } + + public async Task SetCancelTokenAsync(CancellationToken cancelToken) + { + await _tokenLock.WaitAsync().ConfigureAwait(false); + try + { + _parentToken = cancelToken; + _requestCancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _clearToken.Token).Token; + } + finally { _tokenLock.Release(); } + } + public async Task ClearAsync() + { + await _tokenLock.WaitAsync().ConfigureAwait(false); + try + { + _clearToken?.Cancel(); + _clearToken = new CancellationTokenSource(); + if (_parentToken != null) + _requestCancelToken = CancellationTokenSource.CreateLinkedTokenSource(_clearToken.Token, _parentToken).Token; + else + _requestCancelToken = _clearToken.Token; + } + finally { _tokenLock.Release(); } + } + + public async Task SendAsync(RestRequest request) + { + request.CancelToken = _requestCancelToken; + var bucket = GetOrCreateBucket(request.BucketId); + return await bucket.SendAsync(request).ConfigureAwait(false); + } + public async Task SendAsync(WebSocketRequest request) + { + //TODO: Re-impl websocket buckets + request.CancelToken = _requestCancelToken; + await request.SendAsync().ConfigureAwait(false); + } + + internal async Task EnterGlobalAsync(int id, RestRequest request) + { + int millis = (int)Math.Ceiling((_waitUntil - DateTimeOffset.UtcNow).TotalMilliseconds); + if (millis > 0) + { + Debug.WriteLine($"[{id}] Sleeping {millis} ms (Pre-emptive) [Global]"); + await Task.Delay(millis).ConfigureAwait(false); + } + } + internal void PauseGlobal(RateLimitInfo info, TimeSpan lag) + { + _waitUntil = DateTimeOffset.UtcNow.AddMilliseconds(info.RetryAfter.Value + lag.TotalMilliseconds); + } + + private RequestBucket GetOrCreateBucket(string id) + { + return _buckets.GetOrAdd(id, x => new RequestBucket(this, x)); + } + internal async Task RaiseRateLimitTriggered(string bucketId, RateLimitInfo? info) + { + await RateLimitTriggered(bucketId, info).ConfigureAwait(false); + } + + private async Task RunCleanup() + { + try + { + while (!_cancelToken.IsCancellationRequested) + { + var now = DateTimeOffset.UtcNow; + foreach (var bucket in _buckets.Select(x => x.Value)) + { + RequestBucket ignored; + if ((now - bucket.LastAttemptAt).TotalMinutes > 1.0) + _buckets.TryRemove(bucket.Id, out ignored); + } + await Task.Delay(60000, _cancelToken.Token); //Runs each minute + } + } + catch (OperationCanceledException) { } + catch (ObjectDisposedException) { } + } + + public void Dispose() + { + _cancelToken.Dispose(); + } + } +} diff --git a/src/Discord.Net.Core/Net/Queue/RequestQueueBucket.cs b/src/Discord.Net.Core/Net/Queue/RequestQueueBucket.cs new file mode 100644 index 000000000..211a68eab --- /dev/null +++ b/src/Discord.Net.Core/Net/Queue/RequestQueueBucket.cs @@ -0,0 +1,238 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Diagnostics; +using System.IO; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.Queue +{ + internal class RequestBucket + { + private readonly object _lock; + private readonly RequestQueue _queue; + private int _semaphore; + private DateTimeOffset? _resetTick; + + public string Id { get; private set; } + public int WindowCount { get; private set; } + public DateTimeOffset LastAttemptAt { get; private set; } + + public RequestBucket(RequestQueue queue, string id) + { + _queue = queue; + Id = id; + + _lock = new object(); + + if (queue.TokenType == TokenType.User) + WindowCount = ClientBucket.Get(Id).WindowCount; + else + WindowCount = 1; //Only allow one request until we get a header back + _semaphore = WindowCount; + _resetTick = null; + LastAttemptAt = DateTimeOffset.UtcNow; + } + + static int nextId = 0; + public async Task SendAsync(RestRequest request) + { + int id = Interlocked.Increment(ref nextId); + Debug.WriteLine($"[{id}] Start"); + LastAttemptAt = DateTimeOffset.UtcNow; + while (true) + { + await _queue.EnterGlobalAsync(id, request).ConfigureAwait(false); + await EnterAsync(id, request).ConfigureAwait(false); + + Debug.WriteLine($"[{id}] Sending..."); + var response = await request.SendAsync().ConfigureAwait(false); + TimeSpan lag = DateTimeOffset.UtcNow - DateTimeOffset.Parse(response.Headers["Date"]); + var info = new RateLimitInfo(response.Headers); + + if (response.StatusCode < (HttpStatusCode)200 || response.StatusCode >= (HttpStatusCode)300) + { + switch (response.StatusCode) + { + case (HttpStatusCode)429: + if (info.IsGlobal) + { + Debug.WriteLine($"[{id}] (!) 429 [Global]"); + _queue.PauseGlobal(info, lag); + } + else + { + Debug.WriteLine($"[{id}] (!) 429"); + Update(id, info, lag); + } + await _queue.RaiseRateLimitTriggered(Id, info).ConfigureAwait(false); + continue; //Retry + case HttpStatusCode.BadGateway: //502 + Debug.WriteLine($"[{id}] (!) 502"); + continue; //Continue + default: + string reason = null; + if (response.Stream != null) + { + try + { + using (var reader = new StreamReader(response.Stream)) + using (var jsonReader = new JsonTextReader(reader)) + { + var json = JToken.Load(jsonReader); + reason = json.Value("message"); + } + } + catch { } + } + throw new HttpException(response.StatusCode, reason); + } + } + else + { + Debug.WriteLine($"[{id}] Success"); + Update(id, info, lag); + Debug.WriteLine($"[{id}] Stop"); + return response.Stream; + } + } + } + + private async Task EnterAsync(int id, RestRequest request) + { + int windowCount; + DateTimeOffset? resetAt; + bool isRateLimited = false; + + while (true) + { + if (DateTimeOffset.UtcNow > request.TimeoutAt || request.CancelToken.IsCancellationRequested) + { + if (!isRateLimited) + throw new TimeoutException(); + else + throw new RateLimitedException(); + } + + lock (_lock) + { + windowCount = WindowCount; + resetAt = _resetTick; + } + + DateTimeOffset? timeoutAt = request.TimeoutAt; + if (windowCount > 0 && Interlocked.Decrement(ref _semaphore) < 0) + { + isRateLimited = true; + await _queue.RaiseRateLimitTriggered(Id, null).ConfigureAwait(false); + if (resetAt.HasValue) + { + if (resetAt > timeoutAt) + throw new RateLimitedException(); + int millis = (int)Math.Ceiling((resetAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds); + Debug.WriteLine($"[{id}] Sleeping {millis} ms (Pre-emptive)"); + if (millis > 0) + await Task.Delay(millis, request.CancelToken).ConfigureAwait(false); + } + else + { + if ((timeoutAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds < 500.0) + throw new RateLimitedException(); + Debug.WriteLine($"[{id}] Sleeping 500* ms (Pre-emptive)"); + await Task.Delay(500, request.CancelToken).ConfigureAwait(false); + } + continue; + } + else + Debug.WriteLine($"[{id}] Entered Semaphore ({_semaphore}/{WindowCount} remaining)"); + break; + } + } + + private void Update(int id, RateLimitInfo info, TimeSpan lag) + { + lock (_lock) + { + if (!info.Limit.HasValue && _queue.TokenType != TokenType.User) + { + WindowCount = 0; + return; + } + + bool hasQueuedReset = _resetTick != null; + if (info.Limit.HasValue && WindowCount != info.Limit.Value) + { + WindowCount = info.Limit.Value; + _semaphore = info.Remaining.Value; + Debug.WriteLine($"[{id}] Upgraded Semaphore to {info.Remaining.Value}/{WindowCount} "); + } + + var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + DateTimeOffset resetTick; + + //Using X-RateLimit-Remaining causes a race condition + /*if (info.Remaining.HasValue) + { + Debug.WriteLine($"[{id}] X-RateLimit-Remaining: " + info.Remaining.Value); + _semaphore = info.Remaining.Value; + }*/ + if (info.RetryAfter.HasValue) + { + //RetryAfter is more accurate than Reset, where available + resetTick = DateTimeOffset.UtcNow.AddMilliseconds(info.RetryAfter.Value); + Debug.WriteLine($"[{id}] Retry-After: {info.RetryAfter.Value} ({info.RetryAfter.Value} ms)"); + } + else if (info.Reset.HasValue) + { + resetTick = info.Reset.Value.AddSeconds(/*1.0 +*/ lag.TotalSeconds); + int diff = (int)(resetTick - DateTimeOffset.UtcNow).TotalMilliseconds; + Debug.WriteLine($"[{id}] X-RateLimit-Reset: {info.Reset.Value.ToUnixTimeSeconds()} ({diff} ms, {lag.TotalMilliseconds} ms lag)"); + } + else if (_queue.TokenType == TokenType.User) + { + resetTick = DateTimeOffset.UtcNow.AddSeconds(ClientBucket.Get(Id).WindowSeconds); + Debug.WriteLine($"[{id}] Client Bucket: " + ClientBucket.Get(Id).WindowSeconds); + } + + if (resetTick == null) + { + resetTick = DateTimeOffset.UtcNow.AddSeconds(1.0); //Forcibly reset in a second + Debug.WriteLine($"[{id}] Unknown Retry Time!"); + } + + if (!hasQueuedReset || resetTick > _resetTick) + { + _resetTick = resetTick; + LastAttemptAt = resetTick; //Make sure we dont destroy this until after its been reset + Debug.WriteLine($"[{id}] Reset in {(int)Math.Ceiling((resetTick - DateTimeOffset.UtcNow).TotalMilliseconds)} ms"); + + if (!hasQueuedReset) + { + var _ = QueueReset(id, (int)Math.Ceiling((_resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds)); + } + } + } + } + private async Task QueueReset(int id, int millis) + { + while (true) + { + if (millis > 0) + await Task.Delay(millis).ConfigureAwait(false); + lock (_lock) + { + millis = (int)Math.Ceiling((_resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds); + if (millis <= 0) //Make sure we havent gotten a more accurate reset time + { + Debug.WriteLine($"[{id}] * Reset *"); + _semaphore = WindowCount; + _resetTick = null; + return; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Core/Net/Queue/Requests/IRequest.cs b/src/Discord.Net.Core/Net/Queue/Requests/IRequest.cs new file mode 100644 index 000000000..c8d861a11 --- /dev/null +++ b/src/Discord.Net.Core/Net/Queue/Requests/IRequest.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading; + +namespace Discord.Net.Queue +{ + public interface IRequest + { + CancellationToken CancelToken { get; } + DateTimeOffset? TimeoutAt { get; } + string BucketId { get; } + } +} diff --git a/src/Discord.Net.Core/Net/Queue/Requests/JsonRestRequest.cs b/src/Discord.Net.Core/Net/Queue/Requests/JsonRestRequest.cs new file mode 100644 index 000000000..d328a3e26 --- /dev/null +++ b/src/Discord.Net.Core/Net/Queue/Requests/JsonRestRequest.cs @@ -0,0 +1,21 @@ +using Discord.Net.Rest; +using System.Threading.Tasks; + +namespace Discord.Net.Queue +{ + public class JsonRestRequest : RestRequest + { + public string Json { get; } + + public JsonRestRequest(IRestClient client, string method, string endpoint, string bucket, string json, RequestOptions options) + : base(client, method, endpoint, bucket, options) + { + Json = json; + } + + public override async Task SendAsync() + { + return await Client.SendAsync(Method, Endpoint, Json, Options).ConfigureAwait(false); + } + } +} diff --git a/src/Discord.Net.Core/Net/Queue/Requests/MultipartRestRequest.cs b/src/Discord.Net.Core/Net/Queue/Requests/MultipartRestRequest.cs new file mode 100644 index 000000000..e27bb92a0 --- /dev/null +++ b/src/Discord.Net.Core/Net/Queue/Requests/MultipartRestRequest.cs @@ -0,0 +1,22 @@ +using Discord.Net.Rest; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Discord.Net.Queue +{ + public class MultipartRestRequest : RestRequest + { + public IReadOnlyDictionary MultipartParams { get; } + + public MultipartRestRequest(IRestClient client, string method, string endpoint, string bucket, IReadOnlyDictionary multipartParams, RequestOptions options) + : base(client, method, endpoint, bucket, options) + { + MultipartParams = multipartParams; + } + + public override async Task SendAsync() + { + return await Client.SendAsync(Method, Endpoint, MultipartParams, Options).ConfigureAwait(false); + } + } +} diff --git a/src/Discord.Net.Core/Net/Queue/Requests/RestRequest.cs b/src/Discord.Net.Core/Net/Queue/Requests/RestRequest.cs new file mode 100644 index 000000000..8382003c8 --- /dev/null +++ b/src/Discord.Net.Core/Net/Queue/Requests/RestRequest.cs @@ -0,0 +1,38 @@ +using Discord.Net.Rest; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.Queue +{ + public class RestRequest : IRequest + { + public IRestClient Client { get; } + public string Method { get; } + public string Endpoint { get; } + public string BucketId { get; } + public DateTimeOffset? TimeoutAt { get; } + public TaskCompletionSource Promise { get; } + public RequestOptions Options { get; } + public CancellationToken CancelToken { get; internal set; } + + public RestRequest(IRestClient client, string method, string endpoint, string bucketId, RequestOptions options) + { + Preconditions.NotNull(options, nameof(options)); + + Client = client; + Method = method; + Endpoint = endpoint; + BucketId = bucketId; + Options = options; + TimeoutAt = options.Timeout.HasValue ? DateTimeOffset.UtcNow.AddMilliseconds(options.Timeout.Value) : (DateTimeOffset?)null; + Promise = new TaskCompletionSource(); + } + + public virtual async Task SendAsync() + { + return await Client.SendAsync(Method, Endpoint, Options).ConfigureAwait(false); + } + } +} diff --git a/src/Discord.Net.Core/Net/Queue/Requests/WebSocketRequest.cs b/src/Discord.Net.Core/Net/Queue/Requests/WebSocketRequest.cs new file mode 100644 index 000000000..08cdb192c --- /dev/null +++ b/src/Discord.Net.Core/Net/Queue/Requests/WebSocketRequest.cs @@ -0,0 +1,38 @@ +using Discord.Net.WebSockets; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.Queue +{ + public class WebSocketRequest : IRequest + { + public IWebSocketClient Client { get; } + public string BucketId { get; } + public byte[] Data { get; } + public bool IsText { get; } + public DateTimeOffset? TimeoutAt { get; } + public TaskCompletionSource Promise { get; } + public RequestOptions Options { get; } + public CancellationToken CancelToken { get; internal set; } + + public WebSocketRequest(IWebSocketClient client, string bucketId, byte[] data, bool isText, RequestOptions options) + { + Preconditions.NotNull(options, nameof(options)); + + Client = client; + BucketId = bucketId; + Data = data; + IsText = isText; + Options = options; + TimeoutAt = options.Timeout.HasValue ? DateTimeOffset.UtcNow.AddMilliseconds(options.Timeout.Value) : (DateTimeOffset?)null; + Promise = new TaskCompletionSource(); + } + + public async Task SendAsync() + { + await Client.SendAsync(Data, 0, Data.Length, IsText).ConfigureAwait(false); + } + } +} diff --git a/src/Discord.Net.Core/Net/RateLimitInfo.cs b/src/Discord.Net.Core/Net/RateLimitInfo.cs new file mode 100644 index 000000000..2c2faccf8 --- /dev/null +++ b/src/Discord.Net.Core/Net/RateLimitInfo.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; + +namespace Discord.Net +{ + public struct RateLimitInfo + { + public bool IsGlobal { get; } + public int? Limit { get; } + public int? Remaining { get; } + public int? RetryAfter { get; } + public DateTimeOffset? Reset { get; } + + internal RateLimitInfo(Dictionary headers) + { + string temp; + IsGlobal = headers.TryGetValue("X-RateLimit-Global", out temp) ? bool.Parse(temp) : false; + Limit = headers.TryGetValue("X-RateLimit-Limit", out temp) ? int.Parse(temp) : (int?)null; + Remaining = headers.TryGetValue("X-RateLimit-Remaining", out temp) ? int.Parse(temp) : (int?)null; + Reset = headers.TryGetValue("X-RateLimit-Reset", out temp) ? DateTimeOffset.FromUnixTimeSeconds(int.Parse(temp)) : (DateTimeOffset?)null; + RetryAfter = headers.TryGetValue("Retry-After", out temp) ? int.Parse(temp) : (int?)null; + } + } +} diff --git a/src/Discord.Net.Core/Net/RateLimitedException.cs b/src/Discord.Net.Core/Net/RateLimitedException.cs new file mode 100644 index 000000000..e8572f911 --- /dev/null +++ b/src/Discord.Net.Core/Net/RateLimitedException.cs @@ -0,0 +1,12 @@ +using System; + +namespace Discord.Net +{ + public class RateLimitedException : TimeoutException + { + public RateLimitedException() + : base("You are being rate limited.") + { + } + } +} diff --git a/src/Discord.Net/Net/Rest/DefaultRestClient.cs b/src/Discord.Net.Core/Net/Rest/DefaultRestClient.cs similarity index 64% rename from src/Discord.Net/Net/Rest/DefaultRestClient.cs rename to src/Discord.Net.Core/Net/Rest/DefaultRestClient.cs index bcad2ece4..02c356efd 100644 --- a/src/Discord.Net/Net/Rest/DefaultRestClient.cs +++ b/src/Discord.Net.Core/Net/Rest/DefaultRestClient.cs @@ -1,5 +1,4 @@ using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Globalization; @@ -67,22 +66,22 @@ namespace Discord.Net.Rest _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; } - public async Task SendAsync(string method, string endpoint, bool headerOnly = false) + public async Task SendAsync(string method, string endpoint, RequestOptions options) { string uri = Path.Combine(_baseUrl, endpoint); using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) - return await SendInternalAsync(restRequest, headerOnly).ConfigureAwait(false); + return await SendInternalAsync(restRequest, options).ConfigureAwait(false); } - public async Task SendAsync(string method, string endpoint, string json, bool headerOnly = false) + public async Task SendAsync(string method, string endpoint, string json, RequestOptions options) { string uri = Path.Combine(_baseUrl, endpoint); using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) { restRequest.Content = new StringContent(json, Encoding.UTF8, "application/json"); - return await SendInternalAsync(restRequest, headerOnly).ConfigureAwait(false); + return await SendInternalAsync(restRequest, options).ConfigureAwait(false); } } - public async Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, bool headerOnly = false) + public async Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, RequestOptions options) { string uri = Path.Combine(_baseUrl, endpoint); using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) @@ -110,54 +109,21 @@ namespace Discord.Net.Rest } } restRequest.Content = content; - return await SendInternalAsync(restRequest, headerOnly).ConfigureAwait(false); + return await SendInternalAsync(restRequest, options).ConfigureAwait(false); } } - private async Task SendInternalAsync(HttpRequestMessage request, bool headerOnly) + private async Task SendInternalAsync(HttpRequestMessage request, RequestOptions options) { while (true) { var cancelToken = _cancelToken; //It's okay if another thread changes this, causes a retry to abort HttpResponseMessage response = await _client.SendAsync(request, cancelToken).ConfigureAwait(false); + + var headers = response.Headers.ToDictionary(x => x.Key, x => x.Value.FirstOrDefault()); + var stream = !options.HeaderOnly ? await response.Content.ReadAsStreamAsync().ConfigureAwait(false) : null; - int statusCode = (int)response.StatusCode; - if (statusCode < 200 || statusCode >= 300) //2xx = Success - { - string reason = null; - JToken content = null; - if (response.Content.Headers.GetValues("content-type").FirstOrDefault() == "application/json") - { - try - { - using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) - using (var reader = new StreamReader(stream)) - using (var json = new JsonTextReader(reader)) - { - content = _errorDeserializer.Deserialize(json); - reason = content.Value("message"); - if (reason == null) //Occasionally an error message is given under a different key because reasons - reason = content.ToString(Formatting.None); - } - } - catch { } //Might have been HTML Should we check for content-type? - } - - if (statusCode == 429 && content != null) - { - //TODO: Include bucket info - string bucketId = content.Value("bucket"); - int retryAfterMillis = content.Value("retry_after"); - throw new HttpRateLimitException(bucketId, retryAfterMillis, reason); - } - else - throw new HttpException(response.StatusCode, reason); - } - - if (headerOnly) - return null; - else - return await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + return new RestResponse(response.StatusCode, headers, stream); } } diff --git a/src/Discord.Net.Core/Net/Rest/IRestClient.cs b/src/Discord.Net.Core/Net/Rest/IRestClient.cs new file mode 100644 index 000000000..16cfbe62d --- /dev/null +++ b/src/Discord.Net.Core/Net/Rest/IRestClient.cs @@ -0,0 +1,17 @@ +using Discord.Net.Queue; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.Rest +{ + public interface IRestClient + { + void SetHeader(string key, string value); + void SetCancelToken(CancellationToken cancelToken); + + Task SendAsync(string method, string endpoint, RequestOptions options); + Task SendAsync(string method, string endpoint, string json, RequestOptions options); + Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, RequestOptions options); + } +} diff --git a/src/Discord.Net/Net/Rest/MultipartFile.cs b/src/Discord.Net.Core/Net/Rest/MultipartFile.cs similarity index 100% rename from src/Discord.Net/Net/Rest/MultipartFile.cs rename to src/Discord.Net.Core/Net/Rest/MultipartFile.cs diff --git a/src/Discord.Net/Net/Rest/RestClientProvider.cs b/src/Discord.Net.Core/Net/Rest/RestClientProvider.cs similarity index 100% rename from src/Discord.Net/Net/Rest/RestClientProvider.cs rename to src/Discord.Net.Core/Net/Rest/RestClientProvider.cs diff --git a/src/Discord.Net.Core/Net/Rest/RestResponse.cs b/src/Discord.Net.Core/Net/Rest/RestResponse.cs new file mode 100644 index 000000000..412ff4dce --- /dev/null +++ b/src/Discord.Net.Core/Net/Rest/RestResponse.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.IO; +using System.Net; + +namespace Discord.Net.Rest +{ + public struct RestResponse + { + public HttpStatusCode StatusCode { get; } + public Dictionary Headers { get; } + public Stream Stream { get; } + + public RestResponse(HttpStatusCode statusCode, Dictionary headers, Stream stream) + { + StatusCode = statusCode; + Headers = headers; + Stream = stream; + } + } +} diff --git a/src/Discord.Net/Net/RpcException.cs b/src/Discord.Net.Core/Net/RpcException.cs similarity index 100% rename from src/Discord.Net/Net/RpcException.cs rename to src/Discord.Net.Core/Net/RpcException.cs diff --git a/src/Discord.Net/Net/WebSocketException.cs b/src/Discord.Net.Core/Net/WebSocketException.cs similarity index 100% rename from src/Discord.Net/Net/WebSocketException.cs rename to src/Discord.Net.Core/Net/WebSocketException.cs diff --git a/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs b/src/Discord.Net.Core/Net/WebSockets/DefaultWebSocketClient.cs similarity index 97% rename from src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs rename to src/Discord.Net.Core/Net/WebSockets/DefaultWebSocketClient.cs index 51464efd3..707f7663b 100644 --- a/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs +++ b/src/Discord.Net.Core/Net/WebSockets/DefaultWebSocketClient.cs @@ -54,7 +54,7 @@ namespace Discord.Net.WebSockets await _sendLock.WaitAsync().ConfigureAwait(false); try { - await ConnectInternalAsync(host); + await ConnectInternalAsync(host).ConfigureAwait(false); } finally { @@ -86,7 +86,7 @@ namespace Discord.Net.WebSockets await _sendLock.WaitAsync().ConfigureAwait(false); try { - await DisconnectInternalAsync(); + await DisconnectInternalAsync().ConfigureAwait(false); } finally { @@ -192,7 +192,7 @@ namespace Discord.Net.WebSockets resultCount = socketResult.Count; result = buffer.Array; } - + if (socketResult.MessageType == WebSocketMessageType.Text) { string text = Encoding.UTF8.GetString(result, 0, resultCount); diff --git a/src/Discord.Net/Net/WebSockets/IWebSocketClient.cs b/src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs similarity index 100% rename from src/Discord.Net/Net/WebSockets/IWebSocketClient.cs rename to src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs diff --git a/src/Discord.Net/Net/WebSockets/WebSocketProvider.cs b/src/Discord.Net.Core/Net/WebSockets/WebSocketProvider.cs similarity index 100% rename from src/Discord.Net/Net/WebSockets/WebSocketProvider.cs rename to src/Discord.Net.Core/Net/WebSockets/WebSocketProvider.cs diff --git a/src/Discord.Net/RequestOptions.cs b/src/Discord.Net.Core/RequestOptions.cs similarity index 55% rename from src/Discord.Net/RequestOptions.cs rename to src/Discord.Net.Core/RequestOptions.cs index 242642d56..1d362fad1 100644 --- a/src/Discord.Net/RequestOptions.cs +++ b/src/Discord.Net.Core/RequestOptions.cs @@ -6,10 +6,23 @@ /// The max time, in milliseconds, to wait for this request to complete. If null, a request will not time out. If a rate limit has been triggered for this request's bucket and will not be unpaused in time, this request will fail immediately. public int? Timeout { get; set; } + public bool HeaderOnly { get; internal set; } + + internal bool IgnoreState { get; set; } + + internal static RequestOptions CreateOrClone(RequestOptions options) + { + if (options == null) + return new RequestOptions(); + else + return options.Clone(); + } public RequestOptions() { Timeout = 30000; } + + public RequestOptions Clone() => MemberwiseClone() as RequestOptions; } } diff --git a/src/Discord.Net/TokenType.cs b/src/Discord.Net.Core/TokenType.cs similarity index 100% rename from src/Discord.Net/TokenType.cs rename to src/Discord.Net.Core/TokenType.cs diff --git a/src/Discord.Net/Utilities/AsyncEvent.cs b/src/Discord.Net.Core/Utils/AsyncEvent.cs similarity index 94% rename from src/Discord.Net/Utilities/AsyncEvent.cs rename to src/Discord.Net.Core/Utils/AsyncEvent.cs index 0a4d55ed7..a7fdeddf2 100644 --- a/src/Discord.Net/Utilities/AsyncEvent.cs +++ b/src/Discord.Net.Core/Utils/AsyncEvent.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; namespace Discord { internal class AsyncEvent + where T : class { private readonly object _subLock = new object(); internal ImmutableArray _subscriptions; @@ -19,11 +20,13 @@ namespace Discord public void Add(T subscriber) { + Preconditions.NotNull(subscriber, nameof(subscriber)); lock (_subLock) _subscriptions = _subscriptions.Add(subscriber); } public void Remove(T subscriber) { + Preconditions.NotNull(subscriber, nameof(subscriber)); lock (_subLock) _subscriptions = _subscriptions.Remove(subscriber); } diff --git a/src/Discord.Net/Utilities/ConcurrentHashSet.cs b/src/Discord.Net.Core/Utils/ConcurrentHashSet.cs similarity index 97% rename from src/Discord.Net/Utilities/ConcurrentHashSet.cs rename to src/Discord.Net.Core/Utils/ConcurrentHashSet.cs index 1805649a9..1ef105527 100644 --- a/src/Discord.Net/Utilities/ConcurrentHashSet.cs +++ b/src/Discord.Net.Core/Utils/ConcurrentHashSet.cs @@ -7,6 +7,30 @@ using System.Threading; namespace Discord { + //Based on https://github.com/dotnet/corefx/blob/d0dc5fc099946adc1035b34a8b1f6042eddb0c75/src/System.Threading.Tasks.Parallel/src/System/Threading/PlatformHelper.cs + //Copyright (c) .NET Foundation and Contributors + internal static class ConcurrentHashSet + { + private const int PROCESSOR_COUNT_REFRESH_INTERVAL_MS = 30000; + private static volatile int s_processorCount; + private static volatile int s_lastProcessorCountRefreshTicks; + + public static int DefaultConcurrencyLevel + { + get + { + int now = Environment.TickCount; + if (s_processorCount == 0 || (now - s_lastProcessorCountRefreshTicks) >= PROCESSOR_COUNT_REFRESH_INTERVAL_MS) + { + s_processorCount = Environment.ProcessorCount; + s_lastProcessorCountRefreshTicks = now; + } + + return s_processorCount; + } + } + } + //Based on https://github.com/dotnet/corefx/blob/master/src/System.Collections.Concurrent/src/System/Collections/Concurrent/ConcurrentDictionary.cs //Copyright (c) .NET Foundation and Contributors [DebuggerDisplay("Count = {Count}")] @@ -52,7 +76,7 @@ namespace Discord bucketNo = (hashcode & 0x7fffffff) % bucketCount; lockNo = bucketNo % lockCount; } - private static int DefaultConcurrencyLevel => PlatformHelper.ProcessorCount; + private static int DefaultConcurrencyLevel => ConcurrentHashSet.DefaultConcurrencyLevel; private volatile Tables _tables; private readonly IEqualityComparer _comparer; @@ -448,28 +472,4 @@ namespace Discord Monitor.Exit(_tables._locks[i]); } } - - //https://github.com/dotnet/corefx/blob/d0dc5fc099946adc1035b34a8b1f6042eddb0c75/src/System.Threading.Tasks.Parallel/src/System/Threading/PlatformHelper.cs - //Copyright (c) .NET Foundation and Contributors - internal static class PlatformHelper - { - private const int PROCESSOR_COUNT_REFRESH_INTERVAL_MS = 30000; - private static volatile int s_processorCount; - private static volatile int s_lastProcessorCountRefreshTicks; - - internal static int ProcessorCount - { - get - { - int now = Environment.TickCount; - if (s_processorCount == 0 || (now - s_lastProcessorCountRefreshTicks) >= PROCESSOR_COUNT_REFRESH_INTERVAL_MS) - { - s_processorCount = Environment.ProcessorCount; - s_lastProcessorCountRefreshTicks = now; - } - - return s_processorCount; - } - } - } } \ No newline at end of file diff --git a/src/Discord.Net.Core/Utils/DateTimeUtils.cs b/src/Discord.Net.Core/Utils/DateTimeUtils.cs new file mode 100644 index 000000000..aa127fe29 --- /dev/null +++ b/src/Discord.Net.Core/Utils/DateTimeUtils.cs @@ -0,0 +1,15 @@ +using System; + +namespace Discord +{ +internal static class DateTimeUtils +{ + public static DateTimeOffset FromSnowflake(ulong value) + => DateTimeOffset.FromUnixTimeMilliseconds((long)((value >> 22) + 1420070400000UL)); + + public static DateTimeOffset FromTicks(long ticks) + => new DateTimeOffset(ticks, TimeSpan.Zero); + public static DateTimeOffset? FromTicks(long? ticks) + => ticks != null ? new DateTimeOffset(ticks.Value, TimeSpan.Zero) : (DateTimeOffset?)null; +} +} diff --git a/src/Discord.Net.Core/Utils/MentionUtils.cs b/src/Discord.Net.Core/Utils/MentionUtils.cs new file mode 100644 index 000000000..1b1408852 --- /dev/null +++ b/src/Discord.Net.Core/Utils/MentionUtils.cs @@ -0,0 +1,242 @@ +using System; +using System.Globalization; +using System.Text; + +namespace Discord +{ + public static class MentionUtils + { + private const char SanitizeChar = '\x200b'; + + //If the system can't be positive a user doesn't have a nickname, assume useNickname = true (source: Jake) + internal static string MentionUser(string id, bool useNickname = true) => useNickname ? $"<@!{id}>" : $"<@{id}>"; + public static string MentionUser(ulong id) => MentionUser(id.ToString(), true); + internal static string MentionChannel(string id) => $"<#{id}>"; + public static string MentionChannel(ulong id) => MentionChannel(id.ToString()); + internal static string MentionRole(string id) => $"<@&{id}>"; + public static string MentionRole(ulong id) => MentionRole(id.ToString()); + + /// Parses a provided user mention string. + public static ulong ParseUser(string text) + { + ulong id; + if (TryParseUser(text, out id)) + return id; + throw new ArgumentException("Invalid mention format", nameof(text)); + } + /// Tries to parse a provided user mention string. + public static bool TryParseUser(string text, out ulong userId) + { + if (text.Length >= 3 && text[0] == '<' && text[1] == '@' && text[text.Length - 1] == '>') + { + if (text.Length >= 4 && text[2] == '!') + text = text.Substring(3, text.Length - 4); //<@!123> + else + text = text.Substring(2, text.Length - 3); //<@123> + + if (ulong.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out userId)) + return true; + } + userId = 0; + return false; + } + + /// Parses a provided channel mention string. + public static ulong ParseChannel(string text) + { + ulong id; + if (TryParseChannel(text, out id)) + return id; + throw new ArgumentException("Invalid mention format", nameof(text)); + } + /// Tries to parse a provided channel mention string. + public static bool TryParseChannel(string text, out ulong channelId) + { + if (text.Length >= 3 && text[0] == '<' && text[1] == '#' && text[text.Length - 1] == '>') + { + text = text.Substring(2, text.Length - 3); //<#123> + + if (ulong.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out channelId)) + return true; + } + channelId = 0; + return false; + } + + /// Parses a provided role mention string. + public static ulong ParseRole(string text) + { + ulong id; + if (TryParseRole(text, out id)) + return id; + throw new ArgumentException("Invalid mention format", nameof(text)); + } + /// Tries to parse a provided role mention string. + public static bool TryParseRole(string text, out ulong roleId) + { + if (text.Length >= 4 && text[0] == '<' && text[1] == '@' && text[2] == '&' && text[text.Length - 1] == '>') + { + text = text.Substring(3, text.Length - 4); //<@&123> + + if (ulong.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out roleId)) + return true; + } + roleId = 0; + return false; + } + + internal static string Resolve(IMessage msg, TagHandling userHandling, TagHandling channelHandling, TagHandling roleHandling, TagHandling everyoneHandling, TagHandling emojiHandling) + { + var text = new StringBuilder(msg.Content); + var tags = msg.Tags; + int indexOffset = 0; + + foreach (var tag in tags) + { + string newText = ""; + switch (tag.Type) + { + case TagType.UserMention: + if (userHandling == TagHandling.Ignore) continue; + newText = ResolveUserMention(tag, userHandling); + break; + case TagType.ChannelMention: + if (channelHandling == TagHandling.Ignore) continue; + newText = ResolveChannelMention(tag, channelHandling); + break; + case TagType.RoleMention: + if (roleHandling == TagHandling.Ignore) continue; + newText = ResolveRoleMention(tag, roleHandling); + break; + case TagType.EveryoneMention: + if (everyoneHandling == TagHandling.Ignore) continue; + newText = ResolveEveryoneMention(tag, everyoneHandling); + break; + case TagType.HereMention: + if (everyoneHandling == TagHandling.Ignore) continue; + newText = ResolveHereMention(tag, everyoneHandling); + break; + case TagType.Emoji: + if (emojiHandling == TagHandling.Ignore) continue; + newText = ResolveEmoji(tag, emojiHandling); + break; + } + text.Remove(tag.Index + indexOffset, tag.Length); + text.Insert(tag.Index + indexOffset, newText); + indexOffset += newText.Length - tag.Length; + } + return text.ToString(); + } + internal static string ResolveUserMention(ITag tag, TagHandling mode) + { + if (mode != TagHandling.Remove) + { + var user = tag.Value as IUser; + var guildUser = user as IGuildUser; + switch (mode) + { + case TagHandling.Name: + if (user != null) + return $"@{guildUser?.Nickname ?? user?.Username}"; + else + return $"@unknown-user"; + case TagHandling.FullName: + if (user != null) + return $"@{user.Username}#{user.Discriminator}"; + else + return $"@unknown-user"; + case TagHandling.Sanitize: + if (guildUser != null && guildUser.Nickname == null) + return MentionUser($"{SanitizeChar}{tag.Key}", false); + else + return MentionUser($"{SanitizeChar}{tag.Key}", true); + } + } + return ""; + } + internal static string ResolveChannelMention(ITag tag, TagHandling mode) + { + if (mode != TagHandling.Remove) + { + var channel = tag.Value as IChannel; + switch (mode) + { + case TagHandling.Name: + case TagHandling.FullName: + if (channel != null) + return $"#{channel.Name}"; + else + return $"#deleted-channel"; + case TagHandling.Sanitize: + return MentionChannel($"{SanitizeChar}{tag.Key}"); + } + } + return ""; + } + internal static string ResolveRoleMention(ITag tag, TagHandling mode) + { + if (mode != TagHandling.Remove) + { + var role = tag.Value as IRole; + switch (mode) + { + case TagHandling.Name: + case TagHandling.FullName: + if (role != null) + return $"@{role.Name}"; + else + return $"@deleted-role"; + case TagHandling.Sanitize: + return MentionRole($"{SanitizeChar}{tag.Key}"); + } + } + return ""; + } + internal static string ResolveEveryoneMention(ITag tag, TagHandling mode) + { + if (mode != TagHandling.Remove) + { + switch (mode) + { + case TagHandling.Name: + case TagHandling.FullName: + return "@everyone"; + case TagHandling.Sanitize: + return $"@{SanitizeChar}everyone"; + } + } + return ""; + } + internal static string ResolveHereMention(ITag tag, TagHandling mode) + { + if (mode != TagHandling.Remove) + { + switch (mode) + { + case TagHandling.Name: + case TagHandling.FullName: + return "@everyone"; + case TagHandling.Sanitize: + return $"@{SanitizeChar}everyone"; + } + } + return ""; + } + internal static string ResolveEmoji(ITag tag, TagHandling mode) + { + if (mode != TagHandling.Remove) + { + Emoji emoji = (Emoji)tag.Value; + switch (mode) + { + case TagHandling.Name: + case TagHandling.FullName: + return $":{emoji.Name}:"; + case TagHandling.Sanitize: + return $"<@{SanitizeChar}everyone"; + } + } + return ""; + } + } +} diff --git a/src/Discord.Net/Utilities/Optional.cs b/src/Discord.Net.Core/Utils/Optional.cs similarity index 100% rename from src/Discord.Net/Utilities/Optional.cs rename to src/Discord.Net.Core/Utils/Optional.cs diff --git a/src/Discord.Net.Core/Utils/Paging/Page.cs b/src/Discord.Net.Core/Utils/Paging/Page.cs new file mode 100644 index 000000000..996d0ac6a --- /dev/null +++ b/src/Discord.Net.Core/Utils/Paging/Page.cs @@ -0,0 +1,22 @@ +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Discord +{ + internal class Page : IReadOnlyCollection + { + private readonly IReadOnlyCollection _items; + public int Index { get; } + + public Page(PageInfo info, IEnumerable source) + { + Index = info.Page; + _items = source.ToImmutableArray(); + } + + int IReadOnlyCollection.Count => _items.Count; + IEnumerator IEnumerable.GetEnumerator() => _items.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => _items.GetEnumerator(); + } +} diff --git a/src/Discord.Net.Core/Utils/Paging/PageInfo.cs b/src/Discord.Net.Core/Utils/Paging/PageInfo.cs new file mode 100644 index 000000000..3b49225f2 --- /dev/null +++ b/src/Discord.Net.Core/Utils/Paging/PageInfo.cs @@ -0,0 +1,23 @@ +namespace Discord +{ + internal class PageInfo + { + public int Page { get; set; } + public ulong? Position { get; set; } + public int? Count { get; set; } + public int PageSize { get; set; } + public int? Remaining { get; set; } + + internal PageInfo(ulong? pos, int? count, int pageSize) + { + Page = 1; + Position = pos; + Count = count; + Remaining = count; + PageSize = pageSize; + + if (Count != null && Count.Value < PageSize) + PageSize = Count.Value; + } + } +} diff --git a/src/Discord.Net.Core/Utils/Paging/PagedEnumerator.cs b/src/Discord.Net.Core/Utils/Paging/PagedEnumerator.cs new file mode 100644 index 000000000..730f9517a --- /dev/null +++ b/src/Discord.Net.Core/Utils/Paging/PagedEnumerator.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord +{ + internal class PagedAsyncEnumerable : IAsyncEnumerable> + { + public int PageSize { get; } + + private readonly ulong? _start; + private readonly int? _count; + private readonly Func>> _getPage; + private readonly Action> _nextPage; + + public PagedAsyncEnumerable(int pageSize, Func>> getPage, Action> nextPage = null, + ulong? start = null, int? count = null) + { + PageSize = pageSize; + _start = start; + _count = count; + + _getPage = getPage; + _nextPage = nextPage; + } + + public IAsyncEnumerator> GetEnumerator() => new Enumerator(this); + internal class Enumerator : IAsyncEnumerator> + { + private readonly PagedAsyncEnumerable _source; + private readonly PageInfo _info; + + public IReadOnlyCollection Current { get; private set; } + + public Enumerator(PagedAsyncEnumerable source) + { + _source = source; + _info = new PageInfo(source._start, source._count, source.PageSize); + } + + public async Task MoveNext(CancellationToken cancelToken) + { + if (_info.Remaining == 0) + return false; + + var data = await _source._getPage(_info, cancelToken).ConfigureAwait(false); + Current = new Page(_info, data); + + _info.Page++; + if (_info.Remaining != null) + { + if (Current.Count >= _info.Remaining) + _info.Remaining = 0; + else + _info.Remaining -= Current.Count; + } + else + { + if (Current.Count == 0) + _info.Remaining = 0; + } + _info.PageSize = _info.Remaining != null ? (int)Math.Min(_info.Remaining.Value, _source.PageSize) : _source.PageSize; + + if (_info.Remaining != 0) + _source?._nextPage(_info, data); + + return true; + } + + public void Dispose() { Current = null; } + } + } +} \ No newline at end of file diff --git a/src/Discord.Net/Entities/Permissions/Permissions.cs b/src/Discord.Net.Core/Utils/Permissions.cs similarity index 91% rename from src/Discord.Net/Entities/Permissions/Permissions.cs rename to src/Discord.Net.Core/Utils/Permissions.cs index d7cfe3191..390c142de 100644 --- a/src/Discord.Net/Entities/Permissions/Permissions.cs +++ b/src/Discord.Net.Core/Utils/Permissions.cs @@ -5,7 +5,7 @@ namespace Discord internal static class Permissions { public const int MaxBits = 53; - + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static PermValue GetValue(ulong allow, ulong deny, ChannelPermission bit) => GetValue(allow, deny, (byte)bit); @@ -86,16 +86,16 @@ namespace Discord [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void UnsetBit(ref ulong value, byte bit) => value &= ~(1U << bit); - public static ulong ResolveGuild(IGuildUser user) + public static ulong ResolveGuild(IGuild guild, IGuildUser user) { ulong resolvedPermissions = 0; - - if (user.Id == user.Guild.OwnerId) + + if (user.Id == guild.OwnerId) resolvedPermissions = GuildPermissions.All.RawValue; //Owners always have all permissions else { - foreach (var role in user.Roles) - resolvedPermissions |= role.Permissions.RawValue; + foreach (var roleId in user.RoleIds) + resolvedPermissions |= guild.GetRole(roleId)?.Permissions.RawValue ?? 0; if (GetValue(resolvedPermissions, GuildPermission.Administrator)) resolvedPermissions = GuildPermissions.All.RawValue; //Administrators always have all permissions } @@ -106,7 +106,7 @@ namespace Discord { return ResolveChannel(user, channel, ResolveGuild(user)); }*/ - public static ulong ResolveChannel(IGuildUser user, IGuildChannel channel, ulong guildPermissions) + public static ulong ResolveChannel(IGuild guild, IGuildUser user, IGuildChannel channel, ulong guildPermissions) { ulong resolvedPermissions = 0; @@ -119,13 +119,13 @@ namespace Discord resolvedPermissions = guildPermissions; OverwritePermissions? perms; - var roles = user.Roles; - if (roles.Count > 0) + var roleIds = user.RoleIds; + if (roleIds.Count > 0) { ulong deniedPermissions = 0UL, allowedPermissions = 0UL; - foreach (var role in roles) + foreach (var roleId in roleIds) { - perms = channel.GetPermissionOverwrite(role); + perms = channel.GetPermissionOverwrite(guild.GetRole(roleId)); if (perms != null) { deniedPermissions |= perms.Value.DenyValue; @@ -137,7 +137,7 @@ namespace Discord perms = channel.GetPermissionOverwrite(user); if (perms != null) resolvedPermissions = (resolvedPermissions & ~perms.Value.DenyValue) | perms.Value.AllowValue; - + //TODO: C#7 Typeswitch candidate var textChannel = channel as ITextChannel; var voiceChannel = channel as IVoiceChannel; @@ -151,4 +151,4 @@ namespace Discord return resolvedPermissions; } } -} +} \ No newline at end of file diff --git a/src/Discord.Net/Utilities/Preconditions.cs b/src/Discord.Net.Core/Utils/Preconditions.cs similarity index 99% rename from src/Discord.Net/Utilities/Preconditions.cs rename to src/Discord.Net.Core/Utils/Preconditions.cs index 1bd8da7ac..14c9db24d 100644 --- a/src/Discord.Net/Utilities/Preconditions.cs +++ b/src/Discord.Net.Core/Utils/Preconditions.cs @@ -1,5 +1,4 @@ -using Discord.API; -using System; +using System; namespace Discord { diff --git a/src/Discord.Net.Core/project.json b/src/Discord.Net.Core/project.json new file mode 100644 index 000000000..ea17308ae --- /dev/null +++ b/src/Discord.Net.Core/project.json @@ -0,0 +1,50 @@ +{ + "version": "1.0.0-beta2-*", + "description": "An unofficial .Net API wrapper for the Discord service.", + "authors": [ "RogueException" ], + + "packOptions": { + "tags": [ "discord", "discordapp" ], + "licenseUrl": "http://opensource.org/licenses/MIT", + "projectUrl": "https://github.com/RogueException/Discord.Net", + "repository": { + "type": "git", + "url": "git://github.com/RogueException/Discord.Net" + } + }, + + "configurations": { + "Release": { + "buildOptions": { + "define": [ "RELEASE" ], + "nowarn": [ "CS1573", "CS1591" ], + "optimize": true, + "warningsAsErrors": true, + "xmlDoc": true + } + } + }, + + "dependencies": { + "Microsoft.Win32.Primitives": "4.0.1", + "Newtonsoft.Json": "9.0.1", + "System.Collections.Concurrent": "4.0.12", + "System.Collections.Immutable": "1.2.0", + "System.Interactive.Async": "3.0.0", + "System.Net.Http": "4.1.0", + "System.Net.WebSockets.Client": { + "version": "4.0.0", + "type": "build" + } + }, + + "frameworks": { + "netstandard1.3": { + "imports": [ + "dotnet5.4", + "dnxcore50", + "portable-net45+win8" + ] + } + } +} diff --git a/src/Discord.Net.Rest/AssemblyInfo.cs b/src/Discord.Net.Rest/AssemblyInfo.cs new file mode 100644 index 000000000..aff0626bf --- /dev/null +++ b/src/Discord.Net.Rest/AssemblyInfo.cs @@ -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.Tests")] \ No newline at end of file diff --git a/src/Discord.Net.Rest/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs new file mode 100644 index 000000000..4ed019dfb --- /dev/null +++ b/src/Discord.Net.Rest/BaseDiscordClient.cs @@ -0,0 +1,164 @@ +using Discord.Logging; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Rest +{ + public abstract class BaseDiscordClient : IDiscordClient + { + public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } + private readonly AsyncEvent> _logEvent = new AsyncEvent>(); + + public event Func LoggedIn { add { _loggedInEvent.Add(value); } remove { _loggedInEvent.Remove(value); } } + private readonly AsyncEvent> _loggedInEvent = new AsyncEvent>(); + public event Func LoggedOut { add { _loggedOutEvent.Add(value); } remove { _loggedOutEvent.Remove(value); } } + private readonly AsyncEvent> _loggedOutEvent = new AsyncEvent>(); + + 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; } + + /// Creates a new REST-only discord client. + internal BaseDiscordClient(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, info) => + { + if (info == null) + await _queueLogger.WarningAsync($"Preemptive Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); + else + await _queueLogger.WarningAsync($"Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); + }; + ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false); + } + + /// + 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 ApiClient.LoginAsync(tokenType, token).ConfigureAwait(false); + 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; } + + /// + 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; + } + } + /// + public void Dispose() => Dispose(true); + + //IDiscordClient + ConnectionState IDiscordClient.ConnectionState => ConnectionState.Disconnected; + ISelfUser IDiscordClient.CurrentUser => CurrentUser; + + Task IDiscordClient.GetApplicationInfoAsync() { throw new NotSupportedException(); } + + Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode) + => Task.FromResult(null); + Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode) + => Task.FromResult>(ImmutableArray.Create()); + + Task> IDiscordClient.GetConnectionsAsync() + => Task.FromResult>(ImmutableArray.Create()); + + Task IDiscordClient.GetInviteAsync(string inviteId) + => Task.FromResult(null); + + Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode) + => Task.FromResult(null); + Task> IDiscordClient.GetGuildsAsync(CacheMode mode) + => Task.FromResult>(ImmutableArray.Create()); + Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon) { throw new NotSupportedException(); } + + Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode) + => Task.FromResult(null); + Task IDiscordClient.GetUserAsync(string username, string discriminator) + => Task.FromResult(null); + + Task> IDiscordClient.GetVoiceRegionsAsync() + => Task.FromResult>(ImmutableArray.Create()); + Task IDiscordClient.GetVoiceRegionAsync(string id) + => Task.FromResult(null); + + Task IDiscordClient.ConnectAsync() { throw new NotSupportedException(); } + Task IDiscordClient.DisconnectAsync() { throw new NotSupportedException(); } + + } +} diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs new file mode 100644 index 000000000..f1c619e02 --- /dev/null +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -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 GetApplicationInfoAsync(BaseDiscordClient client) + { + var model = await client.ApiClient.GetMyApplicationAsync().ConfigureAwait(false); + return RestApplication.Create(client, model); + } + + public static async Task GetChannelAsync(BaseDiscordClient 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> GetPrivateChannelsAsync(BaseDiscordClient client) + { + var models = await client.ApiClient.GetMyPrivateChannelsAsync().ConfigureAwait(false); + return models.Select(x => RestDMChannel.Create(client, x)).ToImmutableArray(); + } + + public static async Task> GetConnectionsAsync(BaseDiscordClient client) + { + var models = await client.ApiClient.GetMyConnectionsAsync().ConfigureAwait(false); + return models.Select(x => RestConnection.Create(x)).ToImmutableArray(); + } + + public static async Task GetInviteAsync(BaseDiscordClient 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 GetGuildAsync(BaseDiscordClient 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 GetGuildEmbedAsync(BaseDiscordClient 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> GetGuildSummariesAsync(BaseDiscordClient client) + { + var models = await client.ApiClient.GetMyGuildsAsync().ConfigureAwait(false); + return models.Select(x => RestUserGuild.Create(client, x)).ToImmutableArray(); + } + public static async Task> GetGuildsAsync(BaseDiscordClient client) + { + var summaryModels = await client.ApiClient.GetMyGuildsAsync().ConfigureAwait(false); + var guilds = ImmutableArray.CreateBuilder(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 CreateGuildAsync(BaseDiscordClient 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 GetUserAsync(BaseDiscordClient 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 GetGuildUserAsync(BaseDiscordClient client, + ulong guildId, ulong id) + { + var model = await client.ApiClient.GetGuildMemberAsync(guildId, id).ConfigureAwait(false); + if (model != null) + return RestGuildUser.Create(client, new RestGuild(client, guildId), model); + return null; + } + + public static async Task> GetVoiceRegionsAsync(BaseDiscordClient client) + { + var models = await client.ApiClient.GetVoiceRegionsAsync().ConfigureAwait(false); + return models.Select(x => RestVoiceRegion.Create(client, x)).ToImmutableArray(); + } + public static async Task GetVoiceRegionAsync(BaseDiscordClient 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(); + } + } +} diff --git a/src/Discord.Net.Rest/Discord.Net.Rest.xproj b/src/Discord.Net.Rest/Discord.Net.Rest.xproj new file mode 100644 index 000000000..6a5d3e2b8 --- /dev/null +++ b/src/Discord.Net.Rest/Discord.Net.Rest.xproj @@ -0,0 +1,19 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + bfc6dc28-0351-4573-926a-d4124244c04f + Discord.Rest + .\obj + .\bin\ + v4.6.1 + + + 2.0 + + + \ No newline at end of file diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs new file mode 100644 index 000000000..f36c0fb06 --- /dev/null +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -0,0 +1,129 @@ +using Discord.Net.Queue; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Threading.Tasks; + +namespace Discord.Rest +{ + public class DiscordRestClient : BaseDiscordClient, IDiscordClient + { + public new RestSelfUser CurrentUser => base.CurrentUser as RestSelfUser; + + public DiscordRestClient() : this(new DiscordRestConfig()) { } + public DiscordRestClient(DiscordRestConfig config) : base(config, CreateApiClient(config)) { } + + private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config) + => new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent, requestQueue: new RequestQueue()); + + protected override Task OnLoginAsync(TokenType tokenType, string token) + { + base.CurrentUser = RestSelfUser.Create(this, ApiClient.CurrentUser); + return Task.CompletedTask; + } + + /// + public Task GetApplicationInfoAsync() + => ClientHelper.GetApplicationInfoAsync(this); + + /// + public Task GetChannelAsync(ulong id) + => ClientHelper.GetChannelAsync(this, id); + /// + public Task> GetPrivateChannelsAsync() + => ClientHelper.GetPrivateChannelsAsync(this); + + /// + public Task> GetConnectionsAsync() + => ClientHelper.GetConnectionsAsync(this); + + /// + public Task GetInviteAsync(string inviteId) + => ClientHelper.GetInviteAsync(this, inviteId); + + /// + public Task GetGuildAsync(ulong id) + => ClientHelper.GetGuildAsync(this, id); + /// + public Task GetGuildEmbedAsync(ulong id) + => ClientHelper.GetGuildEmbedAsync(this, id); + /// + public Task> GetGuildSummariesAsync() + => ClientHelper.GetGuildSummariesAsync(this); + /// + public Task> GetGuildsAsync() + => ClientHelper.GetGuildsAsync(this); + /// + public Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null) + => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon); + + /// + public Task GetUserAsync(ulong id) + => ClientHelper.GetUserAsync(this, id); + /// + public Task GetGuildUserAsync(ulong guildId, ulong id) + => ClientHelper.GetGuildUserAsync(this, guildId, id); + + /// + public Task> GetVoiceRegionsAsync() + => ClientHelper.GetVoiceRegionsAsync(this); + /// + public Task GetVoiceRegionAsync(string id) + => ClientHelper.GetVoiceRegionAsync(this, id); + + //IDiscordClient + async Task IDiscordClient.GetApplicationInfoAsync() + => await GetApplicationInfoAsync().ConfigureAwait(false); + + async Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return await GetChannelAsync(id).ConfigureAwait(false); + else + return null; + } + async Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return await GetPrivateChannelsAsync().ConfigureAwait(false); + else + return ImmutableArray.Create(); + } + + async Task> IDiscordClient.GetConnectionsAsync() + => await GetConnectionsAsync().ConfigureAwait(false); + + async Task IDiscordClient.GetInviteAsync(string inviteId) + => await GetInviteAsync(inviteId).ConfigureAwait(false); + + async Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return await GetGuildAsync(id).ConfigureAwait(false); + else + return null; + } + async Task> IDiscordClient.GetGuildsAsync(CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return await GetGuildsAsync().ConfigureAwait(false); + else + return ImmutableArray.Create(); + } + async Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon) + => await CreateGuildAsync(name, region, jpegIcon).ConfigureAwait(false); + + async Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode) + { + if (mode == CacheMode.AllowDownload) + return await GetUserAsync(id).ConfigureAwait(false); + else + return null; + } + + async Task> IDiscordClient.GetVoiceRegionsAsync() + => await GetVoiceRegionsAsync().ConfigureAwait(false); + async Task IDiscordClient.GetVoiceRegionAsync(string id) + => await GetVoiceRegionAsync(id).ConfigureAwait(false); + } +} diff --git a/src/Discord.Net/Rest/DiscordRestConfig.cs b/src/Discord.Net.Rest/DiscordRestConfig.cs similarity index 100% rename from src/Discord.Net/Rest/DiscordRestConfig.cs rename to src/Discord.Net.Rest/DiscordRestConfig.cs diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs new file mode 100644 index 000000000..ccd24e5dd --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -0,0 +1,218 @@ +using Discord.API.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord.Rest +{ + internal static class ChannelHelper + { + //General + public static async Task DeleteAsync(IChannel channel, BaseDiscordClient client, + RequestOptions options) + { + await client.ApiClient.DeleteChannelAsync(channel.Id, options).ConfigureAwait(false); + } + public static async Task ModifyAsync(IGuildChannel channel, BaseDiscordClient client, + Action func, + RequestOptions options) + { + var args = new ModifyGuildChannelParams(); + func(args); + return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, args, options).ConfigureAwait(false); + } + public static async Task ModifyAsync(ITextChannel channel, BaseDiscordClient client, + Action func, + RequestOptions options) + { + var args = new ModifyTextChannelParams(); + func(args); + return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, args, options).ConfigureAwait(false); + } + public static async Task ModifyAsync(IVoiceChannel channel, BaseDiscordClient client, + Action func, + RequestOptions options) + { + var args = new ModifyVoiceChannelParams(); + func(args); + return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, args, options).ConfigureAwait(false); + } + + //Invites + public static async Task> GetInvitesAsync(IChannel channel, BaseDiscordClient client, + RequestOptions options) + { + var models = await client.ApiClient.GetChannelInvitesAsync(channel.Id, options).ConfigureAwait(false); + return models.Select(x => RestInviteMetadata.Create(client, x)).ToImmutableArray(); + } + public static async Task CreateInviteAsync(IChannel channel, BaseDiscordClient client, + int? maxAge, int? maxUses, bool isTemporary, RequestOptions options) + { + var args = new CreateChannelInviteParams { IsTemporary = isTemporary }; + if (maxAge.HasValue) + args.MaxAge = maxAge.Value; + if (maxUses.HasValue) + args.MaxUses = maxUses.Value; + var model = await client.ApiClient.CreateChannelInviteAsync(channel.Id, args, options).ConfigureAwait(false); + return RestInviteMetadata.Create(client, model); + } + + //Messages + public static async Task GetMessageAsync(IChannel channel, BaseDiscordClient client, + ulong id, IGuild guild, RequestOptions options) + { + var model = await client.ApiClient.GetChannelMessageAsync(channel.Id, id, options).ConfigureAwait(false); + return RestMessage.Create(client, guild, model); + } + public static IAsyncEnumerable> GetMessagesAsync(IChannel channel, BaseDiscordClient client, + ulong? fromMessageId, Direction dir, int limit, IGuild guild, RequestOptions options) + { + if (dir == Direction.Around) + throw new NotImplementedException(); //TODO: Impl + + return new PagedAsyncEnumerable( + DiscordConfig.MaxMessagesPerBatch, + async (info, ct) => + { + var args = new GetChannelMessagesParams + { + RelativeDirection = dir, + Limit = info.PageSize + }; + if (info.Position != null) + args.RelativeMessageId = info.Position.Value; + var models = await client.ApiClient.GetChannelMessagesAsync(channel.Id, args, options).ConfigureAwait(false); + return models.Select(x => RestMessage.Create(client, guild, x)).ToImmutableArray(); ; + }, + nextPage: (info, lastPage) => + { + if (dir == Direction.Before) + info.Position = lastPage.Min(x => x.Id); + else + info.Position = lastPage.Max(x => x.Id); + if (lastPage.Count != DiscordConfig.MaxMessagesPerBatch) + info.Remaining = 0; + }, + start: fromMessageId, + count: limit + ); + } + public static async Task> GetPinnedMessagesAsync(IChannel channel, BaseDiscordClient client, + IGuild guild, RequestOptions options) + { + var models = await client.ApiClient.GetPinsAsync(channel.Id, options).ConfigureAwait(false); + return models.Select(x => RestMessage.Create(client, guild, x)).ToImmutableArray(); + } + + public static async Task SendMessageAsync(IChannel channel, BaseDiscordClient client, + string text, bool isTTS, IGuild guild, RequestOptions options) + { + var args = new CreateMessageParams(text) { IsTTS = isTTS }; + var model = await client.ApiClient.CreateMessageAsync(channel.Id, args, options).ConfigureAwait(false); + return RestUserMessage.Create(client, guild, model); + } + + public static Task SendFileAsync(IChannel channel, BaseDiscordClient client, + string filePath, string text, bool isTTS, IGuild guild, RequestOptions options) + { + string filename = Path.GetFileName(filePath); + using (var file = File.OpenRead(filePath)) + return SendFileAsync(channel, client, file, filename, text, isTTS, guild, options); + } + public static async Task SendFileAsync(IChannel channel, BaseDiscordClient client, + Stream stream, string filename, string text, bool isTTS, IGuild guild, RequestOptions options) + { + var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS }; + var model = await client.ApiClient.UploadFileAsync(channel.Id, args, options).ConfigureAwait(false); + return RestUserMessage.Create(client, guild, model); + } + + public static async Task DeleteMessagesAsync(IChannel channel, BaseDiscordClient client, + IEnumerable messages, RequestOptions options) + { + var args = new DeleteMessagesParams(messages.Select(x => x.Id).ToArray()); + await client.ApiClient.DeleteMessagesAsync(channel.Id, args, options).ConfigureAwait(false); + } + + //Permission Overwrites + public static async Task AddPermissionOverwriteAsync(IGuildChannel channel, BaseDiscordClient client, + IUser user, OverwritePermissions perms, RequestOptions options) + { + var args = new ModifyChannelPermissionsParams("member", perms.AllowValue, perms.DenyValue); + await client.ApiClient.ModifyChannelPermissionsAsync(channel.Id, user.Id, args, options).ConfigureAwait(false); + } + public static async Task AddPermissionOverwriteAsync(IGuildChannel channel, BaseDiscordClient client, + IRole role, OverwritePermissions perms, RequestOptions options) + { + var args = new ModifyChannelPermissionsParams("role", perms.AllowValue, perms.DenyValue); + await client.ApiClient.ModifyChannelPermissionsAsync(channel.Id, role.Id, args, options).ConfigureAwait(false); + } + public static async Task RemovePermissionOverwriteAsync(IGuildChannel channel, BaseDiscordClient client, + IUser user, RequestOptions options) + { + await client.ApiClient.DeleteChannelPermissionAsync(channel.Id, user.Id, options).ConfigureAwait(false); + } + public static async Task RemovePermissionOverwriteAsync(IGuildChannel channel, BaseDiscordClient client, + IRole role, RequestOptions options) + { + await client.ApiClient.DeleteChannelPermissionAsync(channel.Id, role.Id, options).ConfigureAwait(false); + } + + //Users + public static async Task GetUserAsync(IGuildChannel channel, IGuild guild, BaseDiscordClient client, + ulong id, RequestOptions options) + { + var model = await client.ApiClient.GetGuildMemberAsync(channel.GuildId, id, options).ConfigureAwait(false); + if (model == null) + return null; + var user = RestGuildUser.Create(client, guild, model); + if (!user.GetPermissions(channel).ReadMessages) + return null; + + return user; + } + public static IAsyncEnumerable> GetUsersAsync(IGuildChannel channel, IGuild guild, BaseDiscordClient client, + ulong? fromUserId, int? limit, RequestOptions options) + { + return new PagedAsyncEnumerable( + DiscordConfig.MaxUsersPerBatch, + async (info, ct) => + { + var args = new GetGuildMembersParams + { + Limit = info.PageSize + }; + if (info.Position != null) + args.AfterUserId = info.Position.Value; + var models = await guild.Discord.ApiClient.GetGuildMembersAsync(guild.Id, args, options).ConfigureAwait(false); + return models + .Select(x => RestGuildUser.Create(client, guild, x)) + .Where(x => x.GetPermissions(channel).ReadMessages) + .ToImmutableArray(); + }, + nextPage: (info, lastPage) => + { + info.Position = lastPage.Max(x => x.Id); + if (lastPage.Count != DiscordConfig.MaxMessagesPerBatch) + info.Remaining = 0; + }, + start: fromUserId, + count: limit + ); + } + + //Typing + public static async Task TriggerTypingAsync(IMessageChannel channel, BaseDiscordClient client, + RequestOptions options = null) + { + await client.ApiClient.TriggerTypingIndicatorAsync(channel.Id, options).ConfigureAwait(false); + } + public static IDisposable EnterTypingState(IMessageChannel channel, BaseDiscordClient client, + RequestOptions options) + => new TypingNotifier(client, channel, options); + } +} diff --git a/src/Discord.Net.Rest/Entities/Channels/IRestAudioChannel.cs b/src/Discord.Net.Rest/Entities/Channels/IRestAudioChannel.cs new file mode 100644 index 000000000..2c3341537 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Channels/IRestAudioChannel.cs @@ -0,0 +1,6 @@ +namespace Discord.Rest +{ + public interface IRestAudioChannel : IAudioChannel + { + } +} diff --git a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs new file mode 100644 index 000000000..6d2b729dd --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace Discord.Rest +{ + public interface IRestMessageChannel : IMessageChannel + { + /// Sends a message to this message channel. + new Task SendMessageAsync(string text, bool isTTS = false, RequestOptions options = null); + /// Sends a file to this text channel, with an optional caption. + new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, RequestOptions options = null); + /// Sends a file to this text channel, with an optional caption. + new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, RequestOptions options = null); + + /// Gets a message from this message channel with the given id, or null if not found. + Task GetMessageAsync(ulong id, RequestOptions options = null); + /// Gets the last N messages from this message channel. + IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null); + /// Gets a collection of messages in this channel. + IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null); + /// Gets a collection of messages in this channel. + IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null); + /// Gets a collection of pinned messages in this channel. + new Task> GetPinnedMessagesAsync(RequestOptions options = null); + } +} diff --git a/src/Discord.Net.Rest/Entities/Channels/IRestPrivateChannel.cs b/src/Discord.Net.Rest/Entities/Channels/IRestPrivateChannel.cs new file mode 100644 index 000000000..a6939f81e --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Channels/IRestPrivateChannel.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Discord.Rest +{ + public interface IRestPrivateChannel : IPrivateChannel + { + new IReadOnlyCollection Recipients { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs new file mode 100644 index 000000000..0481d37ed --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord.Rest +{ + public abstract class RestChannel : RestEntity, IChannel, IUpdateable + { + public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + + internal RestChannel(BaseDiscordClient discord, ulong id) + : base(discord, id) + { + } + internal static RestChannel Create(BaseDiscordClient discord, Model model) + { + switch (model.Type) + { + case ChannelType.Text: + case ChannelType.Voice: + return RestGuildChannel.Create(discord, new RestGuild(discord, model.GuildId.Value), model); + case ChannelType.DM: + case ChannelType.Group: + return CreatePrivate(discord, model) as RestChannel; + default: + throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); + } + } + internal static IRestPrivateChannel CreatePrivate(BaseDiscordClient discord, Model model) + { + switch (model.Type) + { + 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(RequestOptions options = null); + + //IChannel + string IChannel.Name => null; + + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(null); //Overriden + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + => AsyncEnumerable.Empty>(); //Overriden + } +} diff --git a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs new file mode 100644 index 000000000..fa3dae82b --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord.Rest +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestDMChannel : RestChannel, IDMChannel, IRestPrivateChannel, IRestMessageChannel, IUpdateable + { + public RestUser CurrentUser { get; private set; } + public RestUser Recipient { get; private set; } + + public IReadOnlyCollection Users => ImmutableArray.Create(CurrentUser, Recipient); + + internal RestDMChannel(BaseDiscordClient discord, ulong id, ulong recipientId) + : base(discord, id) + { + Recipient = new RestUser(Discord, recipientId); + CurrentUser = new RestUser(Discord, discord.CurrentUser.Id); + } + internal new static RestDMChannel Create(BaseDiscordClient discord, Model model) + { + var entity = new RestDMChannel(discord, model.Id, model.Recipients.Value[0].Id); + entity.Update(model); + return entity; + } + internal override void Update(Model model) + { + Recipient.Update(model.Recipients.Value[0]); + } + + public override async Task UpdateAsync(RequestOptions options = null) + { + var model = await Discord.ApiClient.GetChannelAsync(Id, options).ConfigureAwait(false); + Update(model); + } + public Task CloseAsync(RequestOptions options = null) + => ChannelHelper.DeleteAsync(this, Discord, options); + + public RestUser GetUser(ulong id) + { + if (id == Recipient.Id) + return Recipient; + else if (id == Discord.CurrentUser.Id) + return CurrentUser; + else + return null; + } + + public Task GetMessageAsync(ulong id, RequestOptions options = null) + => ChannelHelper.GetMessageAsync(this, Discord, id, null, options); + public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, null, options); + public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, null, options); + public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, null, options); + public Task> GetPinnedMessagesAsync(RequestOptions options = null) + => ChannelHelper.GetPinnedMessagesAsync(this, Discord, null, options); + + public Task SendMessageAsync(string text, bool isTTS, RequestOptions options = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, null, options); + public Task SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, null, options); + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, null, options); + + public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + + public Task TriggerTypingAsync(RequestOptions options = null) + => ChannelHelper.TriggerTypingAsync(this, Discord, options); + public IDisposable EnterTypingState(RequestOptions options = null) + => ChannelHelper.EnterTypingState(this, Discord, options); + + public override string ToString() => $"@{Recipient}"; + private string DebuggerDisplay => $"@{Recipient} ({Id}, DM)"; + + //IDMChannel + IUser IDMChannel.Recipient => Recipient; + + //IRestPrivateChannel + IReadOnlyCollection IRestPrivateChannel.Recipients => ImmutableArray.Create(Recipient); + + //IPrivateChannel + IReadOnlyCollection IPrivateChannel.Recipients => ImmutableArray.Create(Recipient); + + //IMessageChannel + async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetMessageAsync(id, options).ConfigureAwait(false); + else + return null; + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(limit, options); + else + return AsyncEnumerable.Empty>(); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(fromMessageId, dir, limit, options); + else + return AsyncEnumerable.Empty>(); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(fromMessage, dir, limit, options); + else + return AsyncEnumerable.Empty>(); + } + async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) + => await GetPinnedMessagesAsync(options).ConfigureAwait(false); + + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) + => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) + => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, RequestOptions options) + => await SendMessageAsync(text, isTTS, options).ConfigureAwait(false); + IDisposable IMessageChannel.EnterTypingState(RequestOptions options) + => EnterTypingState(options); + + //IChannel + string IChannel.Name => $"@{Recipient}"; + + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetUser(id)); + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + } +} diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs new file mode 100644 index 000000000..0868568cd --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord.Rest +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestGroupChannel : RestChannel, IGroupChannel, IRestPrivateChannel, IRestMessageChannel, IRestAudioChannel, IUpdateable + { + private string _iconId; + private ImmutableDictionary _users; + + public string Name { get; private set; } + + public IReadOnlyCollection Users => _users.ToReadOnlyCollection(); + public IReadOnlyCollection Recipients + => _users.Select(x => x.Value).Where(x => x.Id != Discord.CurrentUser.Id).ToReadOnlyCollection(() => _users.Count - 1); + + internal RestGroupChannel(BaseDiscordClient discord, ulong id) + : base(discord, id) + { + } + internal new static RestGroupChannel Create(BaseDiscordClient discord, Model model) + { + var entity = new RestGroupChannel(discord, model.Id); + entity.Update(model); + return entity; + } + internal override void Update(Model model) + { + if (model.Name.IsSpecified) + Name = model.Name.Value; + if (model.Icon.IsSpecified) + _iconId = model.Icon.Value; + + if (model.Recipients.IsSpecified) + UpdateUsers(model.Recipients.Value); + } + internal void UpdateUsers(API.User[] models) + { + var users = ImmutableDictionary.CreateBuilder(); + for (int i = 0; i < models.Length; i++) + users[models[i].Id] = RestGroupUser.Create(Discord, models[i]); + _users = users.ToImmutable(); + } + + public override async Task UpdateAsync(RequestOptions options = null) + { + var model = await Discord.ApiClient.GetChannelAsync(Id, options).ConfigureAwait(false); + Update(model); + } + public Task LeaveAsync(RequestOptions options = null) + => ChannelHelper.DeleteAsync(this, Discord, options); + + public RestUser GetUser(ulong id) + { + RestGroupUser user; + if (_users.TryGetValue(id, out user)) + return user; + return null; + } + + public Task GetMessageAsync(ulong id, RequestOptions options = null) + => ChannelHelper.GetMessageAsync(this, Discord, id, null, options); + public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, null, options); + public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, null, options); + public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, null, options); + public Task> GetPinnedMessagesAsync(RequestOptions options = null) + => ChannelHelper.GetPinnedMessagesAsync(this, Discord, null, options); + + public Task SendMessageAsync(string text, bool isTTS, RequestOptions options = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, null, options); + public Task SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, null, options); + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, null, options); + + public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + + public Task TriggerTypingAsync(RequestOptions options = null) + => ChannelHelper.TriggerTypingAsync(this, Discord, options); + public IDisposable EnterTypingState(RequestOptions options = null) + => ChannelHelper.EnterTypingState(this, Discord, options); + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id}, Group)"; + + //ISocketPrivateChannel + IReadOnlyCollection IRestPrivateChannel.Recipients => Recipients; + + //IPrivateChannel + IReadOnlyCollection IPrivateChannel.Recipients => Recipients; + + //IMessageChannel + async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetMessageAsync(id, options).ConfigureAwait(false); + else + return null; + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(limit, options); + else + return AsyncEnumerable.Empty>(); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(fromMessageId, dir, limit, options); + else + return AsyncEnumerable.Empty>(); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(fromMessage, dir, limit, options); + else + return AsyncEnumerable.Empty>(); + } + async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) + => await GetPinnedMessagesAsync(options).ConfigureAwait(false); + + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) + => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) + => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, RequestOptions options) + => await SendMessageAsync(text, isTTS, options).ConfigureAwait(false); + IDisposable IMessageChannel.EnterTypingState(RequestOptions options) + => EnterTypingState(options); + + //IChannel + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetUser(id)); + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + } +} diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs new file mode 100644 index 000000000..2607a2d96 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs @@ -0,0 +1,157 @@ +using Discord.API.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord.Rest +{ + public abstract class RestGuildChannel : RestChannel, IGuildChannel, IUpdateable + { + private ImmutableArray _overwrites; + + public IReadOnlyCollection PermissionOverwrites => _overwrites; + + internal IGuild Guild { get; } + public string Name { get; private set; } + public int Position { get; private set; } + + public ulong GuildId => Guild.Id; + + internal RestGuildChannel(BaseDiscordClient discord, IGuild guild, ulong id) + : base(discord, id) + { + Guild = guild; + } + internal static RestGuildChannel Create(BaseDiscordClient discord, IGuild guild, Model model) + { + switch (model.Type) + { + case ChannelType.Text: + return RestTextChannel.Create(discord, guild, model); + case ChannelType.Voice: + return RestVoiceChannel.Create(discord, guild, model); + default: + throw new InvalidOperationException("Unknown guild channel type"); + } + } + internal override void Update(Model model) + { + Name = model.Name.Value; + Position = model.Position.Value; + + var overwrites = model.PermissionOverwrites.Value; + var newOverwrites = ImmutableArray.CreateBuilder(overwrites.Length); + for (int i = 0; i < overwrites.Length; i++) + newOverwrites.Add(new Overwrite(overwrites[i])); + _overwrites = newOverwrites.ToImmutable(); + } + + public override async Task UpdateAsync(RequestOptions options = null) + { + var model = await Discord.ApiClient.GetChannelAsync(GuildId, Id, options).ConfigureAwait(false); + Update(model); + } + public async Task ModifyAsync(Action func, RequestOptions options = null) + { + var model = await ChannelHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); + Update(model); + } + public Task DeleteAsync(RequestOptions options = null) + => ChannelHelper.DeleteAsync(this, Discord, options); + + public OverwritePermissions? GetPermissionOverwrite(IUser user) + { + for (int i = 0; i < _overwrites.Length; i++) + { + if (_overwrites[i].TargetId == user.Id) + return _overwrites[i].Permissions; + } + return null; + } + public OverwritePermissions? GetPermissionOverwrite(IRole role) + { + 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, RequestOptions options = null) + { + await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, user, perms, options).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, RequestOptions options = null) + { + await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, role, perms, options).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, RequestOptions options = null) + { + await ChannelHelper.RemovePermissionOverwriteAsync(this, Discord, user, options).ConfigureAwait(false); + + for (int i = 0; i < _overwrites.Length; i++) + { + if (_overwrites[i].TargetId == user.Id) + { + _overwrites = _overwrites.RemoveAt(i); + return; + } + } + } + public async Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) + { + await ChannelHelper.RemovePermissionOverwriteAsync(this, Discord, role, options).ConfigureAwait(false); + + for (int i = 0; i < _overwrites.Length; i++) + { + if (_overwrites[i].TargetId == role.Id) + { + _overwrites = _overwrites.RemoveAt(i); + return; + } + } + } + + public async Task> GetInvitesAsync(RequestOptions options = null) + => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); + public async Task CreateInviteAsync(int? maxAge = 3600, int? maxUses = null, bool isTemporary = true, RequestOptions options = null) + => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, options).ConfigureAwait(false); + + public override string ToString() => Name; + + //IGuildChannel + async Task> IGuildChannel.GetInvitesAsync(RequestOptions options) + => await GetInvitesAsync(options).ConfigureAwait(false); + async Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, RequestOptions options) + => await CreateInviteAsync(maxAge, maxUses, isTemporary, options).ConfigureAwait(false); + + OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IRole role) + => GetPermissionOverwrite(role); + OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IUser user) + => GetPermissionOverwrite(user); + async Task IGuildChannel.AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options) + => await AddPermissionOverwriteAsync(role, permissions, options).ConfigureAwait(false); + async Task IGuildChannel.AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options) + => await AddPermissionOverwriteAsync(user, permissions, options).ConfigureAwait(false); + async Task IGuildChannel.RemovePermissionOverwriteAsync(IRole role, RequestOptions options) + => await RemovePermissionOverwriteAsync(role, options).ConfigureAwait(false); + async Task IGuildChannel.RemovePermissionOverwriteAsync(IUser user, RequestOptions options) + => await RemovePermissionOverwriteAsync(user, options).ConfigureAwait(false); + + IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + => AsyncEnumerable.Empty>(); //Overriden //Overriden in Text/Voice + Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(null); //Overriden in Text/Voice + + //IChannel + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + => AsyncEnumerable.Empty>(); //Overriden in Text/Voice + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(null); //Overriden in Text/Voice + } +} diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs new file mode 100644 index 000000000..35ba91f02 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -0,0 +1,148 @@ +using Discord.API.Rest; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord.Rest +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestTextChannel : RestGuildChannel, IRestMessageChannel, ITextChannel + { + public string Topic { get; private set; } + + public string Mention => MentionUtils.MentionChannel(Id); + + internal RestTextChannel(BaseDiscordClient discord, IGuild guild, ulong id) + : base(discord, guild, id) + { + } + internal new static RestTextChannel Create(BaseDiscordClient discord, IGuild guild, Model model) + { + var entity = new RestTextChannel(discord, guild, model.Id); + entity.Update(model); + return entity; + } + internal override void Update(Model model) + { + base.Update(model); + + Topic = model.Topic.Value; + } + + public async Task ModifyAsync(Action func, RequestOptions options = null) + { + var model = await ChannelHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); + Update(model); + } + + public Task GetUserAsync(ulong id, RequestOptions options = null) + => ChannelHelper.GetUserAsync(this, Guild, Discord, id, options); + public IAsyncEnumerable> GetUsersAsync(RequestOptions options = null) + => ChannelHelper.GetUsersAsync(this, Guild, Discord, null, null, options); + + public Task GetMessageAsync(ulong id, RequestOptions options = null) + => ChannelHelper.GetMessageAsync(this, Discord, id, null, options); + public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, null, options); + public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, null, options); + public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, null, options); + public Task> GetPinnedMessagesAsync(RequestOptions options = null) + => ChannelHelper.GetPinnedMessagesAsync(this, Discord, null, options); + + public Task SendMessageAsync(string text, bool isTTS, RequestOptions options = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, null, options); + public Task SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, null, options); + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, null, options); + + public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + + public Task TriggerTypingAsync(RequestOptions options = null) + => ChannelHelper.TriggerTypingAsync(this, Discord, options); + public IDisposable EnterTypingState(RequestOptions options = null) + => ChannelHelper.EnterTypingState(this, Discord, options); + + private string DebuggerDisplay => $"{Name} ({Id}, Text)"; + + //IMessageChannel + async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetMessageAsync(id, options).ConfigureAwait(false); + else + return null; + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(limit, options); + else + return AsyncEnumerable.Empty>(); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(fromMessageId, dir, limit, options); + else + return AsyncEnumerable.Empty>(); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(fromMessage, dir, limit, options); + else + return AsyncEnumerable.Empty>(); + } + async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) + => await GetPinnedMessagesAsync(options).ConfigureAwait(false); + + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) + => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) + => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, RequestOptions options) + => await SendMessageAsync(text, isTTS, options).ConfigureAwait(false); + IDisposable IMessageChannel.EnterTypingState(RequestOptions options) + => EnterTypingState(options); + + //IGuildChannel + async Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetUserAsync(id, options).ConfigureAwait(false); + else + return null; + } + IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetUsersAsync(options); + else + return AsyncEnumerable.Empty>(); + } + + //IChannel + async Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetUserAsync(id, options).ConfigureAwait(false); + else + return null; + } + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetUsersAsync(options); + else + return AsyncEnumerable.Empty>(); + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Channels/RestVirtualMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestVirtualMessageChannel.cs new file mode 100644 index 000000000..97a0a93e6 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Channels/RestVirtualMessageChannel.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord.Rest +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + internal class RestVirtualMessageChannel : RestEntity, IMessageChannel + { + public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public string Mention => MentionUtils.MentionChannel(Id); + + internal RestVirtualMessageChannel(BaseDiscordClient discord, ulong id) + : base(discord, id) + { + } + internal static RestVirtualMessageChannel Create(BaseDiscordClient discord, ulong id) + { + return new RestVirtualMessageChannel(discord, id); + } + + public Task GetMessageAsync(ulong id, RequestOptions options = null) + => ChannelHelper.GetMessageAsync(this, Discord, id, null, options); + public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, null, options); + public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, null, options); + public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, null, options); + public Task> GetPinnedMessagesAsync(RequestOptions options = null) + => ChannelHelper.GetPinnedMessagesAsync(this, Discord, null, options); + + public Task SendMessageAsync(string text, bool isTTS, RequestOptions options = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, null, options); + public Task SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, null, options); + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, null, options); + + public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + + public Task TriggerTypingAsync(RequestOptions options = null) + => ChannelHelper.TriggerTypingAsync(this, Discord, options); + public IDisposable EnterTypingState(RequestOptions options = null) + => ChannelHelper.EnterTypingState(this, Discord, options); + + private string DebuggerDisplay => $"({Id}, Text)"; + + //IMessageChannel + async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetMessageAsync(id, options); + else + return null; + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(limit, options); + else + return AsyncEnumerable.Empty>(); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(fromMessageId, dir, limit, options); + else + return AsyncEnumerable.Empty>(); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(fromMessage, dir, limit, options); + else + return AsyncEnumerable.Empty>(); + } + async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) + => await GetPinnedMessagesAsync(options); + + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) + => await SendFileAsync(filePath, text, isTTS, options); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) + => await SendFileAsync(stream, filename, text, isTTS, options); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, RequestOptions options) + => await SendMessageAsync(text, isTTS, options); + IDisposable IMessageChannel.EnterTypingState(RequestOptions options) + => EnterTypingState(options); + + //IChannel + string IChannel.Name { get { throw new NotSupportedException(); } } + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + { + throw new NotSupportedException(); + } + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs new file mode 100644 index 000000000..a19a5cb38 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs @@ -0,0 +1,53 @@ +using Discord.API.Rest; +using Discord.Audio; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord.Rest +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestVoiceChannel : RestGuildChannel, IVoiceChannel, IRestAudioChannel + { + public int Bitrate { get; private set; } + public int UserLimit { get; private set; } + + internal RestVoiceChannel(BaseDiscordClient discord, IGuild guild, ulong id) + : base(discord, guild, id) + { + } + internal new static RestVoiceChannel Create(BaseDiscordClient discord, IGuild guild, Model model) + { + var entity = new RestVoiceChannel(discord, guild, model.Id); + entity.Update(model); + return entity; + } + internal override void Update(Model model) + { + base.Update(model); + + Bitrate = model.Bitrate.Value; + UserLimit = model.UserLimit.Value; + } + + public async Task ModifyAsync(Action func, RequestOptions options = null) + { + var model = await ChannelHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); + Update(model); + } + + private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; + + //IVoiceChannel + Task IVoiceChannel.ConnectAsync() { throw new NotSupportedException(); } + + //IGuildChannel + Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(null); + IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + => AsyncEnumerable.Empty>(); + } +} diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs new file mode 100644 index 000000000..a27475e3c --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -0,0 +1,210 @@ +using Discord.API.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using EmbedModel = Discord.API.GuildEmbed; +using Model = Discord.API.Guild; +using RoleModel = Discord.API.Role; + +namespace Discord.Rest +{ + internal static class GuildHelper + { + //General + public static async Task ModifyAsync(IGuild guild, BaseDiscordClient client, + Action func, RequestOptions options) + { + if (func == null) throw new NullReferenceException(nameof(func)); + + var args = new ModifyGuildParams(); + func(args); + + if (args.Splash.IsSpecified && guild.SplashId != null) + args.Splash = new API.Image(guild.SplashId); + if (args.Icon.IsSpecified && guild.IconId != null) + args.Icon = new API.Image(guild.IconId); + + return await client.ApiClient.ModifyGuildAsync(guild.Id, args, options).ConfigureAwait(false); + } + public static async Task ModifyEmbedAsync(IGuild guild, BaseDiscordClient client, + Action func, RequestOptions options) + { + if (func == null) throw new NullReferenceException(nameof(func)); + + var args = new ModifyGuildEmbedParams(); + func(args); + return await client.ApiClient.ModifyGuildEmbedAsync(guild.Id, args, options).ConfigureAwait(false); + } + public static async Task ModifyChannelsAsync(IGuild guild, BaseDiscordClient client, + IEnumerable args, RequestOptions options) + { + await client.ApiClient.ModifyGuildChannelsAsync(guild.Id, args, options).ConfigureAwait(false); + } + public static async Task> ModifyRolesAsync(IGuild guild, BaseDiscordClient client, + IEnumerable args, RequestOptions options) + { + return await client.ApiClient.ModifyGuildRolesAsync(guild.Id, args, options).ConfigureAwait(false); + } + public static async Task LeaveAsync(IGuild guild, BaseDiscordClient client, + RequestOptions options) + { + await client.ApiClient.LeaveGuildAsync(guild.Id, options).ConfigureAwait(false); + } + public static async Task DeleteAsync(IGuild guild, BaseDiscordClient client, + RequestOptions options) + { + await client.ApiClient.DeleteGuildAsync(guild.Id, options).ConfigureAwait(false); + } + + //Bans + public static async Task> GetBansAsync(IGuild guild, BaseDiscordClient client, + RequestOptions options) + { + var models = await client.ApiClient.GetGuildBansAsync(guild.Id, options).ConfigureAwait(false); + return models.Select(x => RestBan.Create(client, x)).ToImmutableArray(); + } + + public static async Task AddBanAsync(IGuild guild, BaseDiscordClient client, + ulong userId, int pruneDays, RequestOptions options) + { + var args = new CreateGuildBanParams { DeleteMessageDays = pruneDays }; + await client.ApiClient.CreateGuildBanAsync(guild.Id, userId, args, options).ConfigureAwait(false); + } + public static async Task RemoveBanAsync(IGuild guild, BaseDiscordClient client, + ulong userId, RequestOptions options) + { + await client.ApiClient.RemoveGuildBanAsync(guild.Id, userId, options).ConfigureAwait(false); + } + + //Channels + public static async Task GetChannelAsync(IGuild guild, BaseDiscordClient client, + ulong id, RequestOptions options) + { + var model = await client.ApiClient.GetChannelAsync(guild.Id, id, options).ConfigureAwait(false); + if (model != null) + return RestGuildChannel.Create(client, guild, model); + return null; + } + public static async Task> GetChannelsAsync(IGuild guild, BaseDiscordClient client, + RequestOptions options) + { + var models = await client.ApiClient.GetGuildChannelsAsync(guild.Id, options).ConfigureAwait(false); + return models.Select(x => RestGuildChannel.Create(client, guild, x)).ToImmutableArray(); + } + public static async Task CreateTextChannelAsync(IGuild guild, BaseDiscordClient client, + string name, RequestOptions options) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + + var args = new CreateGuildChannelParams(name, ChannelType.Text); + var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); + return RestTextChannel.Create(client, guild, model); + } + public static async Task CreateVoiceChannelAsync(IGuild guild, BaseDiscordClient client, + string name, RequestOptions options) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + + var args = new CreateGuildChannelParams(name, ChannelType.Voice); + var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); + return RestVoiceChannel.Create(client, guild, model); + } + + //Integrations + public static async Task> GetIntegrationsAsync(IGuild guild, BaseDiscordClient client, + RequestOptions options) + { + var models = await client.ApiClient.GetGuildIntegrationsAsync(guild.Id, options).ConfigureAwait(false); + return models.Select(x => RestGuildIntegration.Create(client, x)).ToImmutableArray(); + } + public static async Task CreateIntegrationAsync(IGuild guild, BaseDiscordClient client, + ulong id, string type, RequestOptions options) + { + var args = new CreateGuildIntegrationParams(id, type); + var model = await client.ApiClient.CreateGuildIntegrationAsync(guild.Id, args, options).ConfigureAwait(false); + return RestGuildIntegration.Create(client, model); + } + + //Invites + public static async Task> GetInvitesAsync(IGuild guild, BaseDiscordClient client, + RequestOptions options) + { + var models = await client.ApiClient.GetGuildInvitesAsync(guild.Id, options).ConfigureAwait(false); + return models.Select(x => RestInviteMetadata.Create(client, x)).ToImmutableArray(); + } + + //Roles + public static async Task CreateRoleAsync(IGuild guild, BaseDiscordClient client, + string name, GuildPermissions? permissions, Color? color, bool isHoisted, RequestOptions options) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + + var model = await client.ApiClient.CreateGuildRoleAsync(guild.Id, options).ConfigureAwait(false); + var role = RestRole.Create(client, model); + + await role.ModifyAsync(x => + { + x.Name = name; + x.Permissions = (permissions ?? role.Permissions).RawValue; + x.Color = (color ?? Color.Default).RawValue; + x.Hoist = isHoisted; + }, options).ConfigureAwait(false); + + return role; + } + + //Users + public static async Task GetUserAsync(IGuild guild, BaseDiscordClient client, + ulong id, RequestOptions options) + { + var model = await client.ApiClient.GetGuildMemberAsync(guild.Id, id, options).ConfigureAwait(false); + if (model != null) + return RestGuildUser.Create(client, guild, model); + return null; + } + public static async Task GetCurrentUserAsync(IGuild guild, BaseDiscordClient client, + RequestOptions options) + { + return await GetUserAsync(guild, client, client.CurrentUser.Id, options).ConfigureAwait(false); + } + public static IAsyncEnumerable> GetUsersAsync(IGuild guild, BaseDiscordClient client, + ulong? fromUserId, int? limit, RequestOptions options) + { + return new PagedAsyncEnumerable( + DiscordConfig.MaxMessagesPerBatch, + async (info, ct) => + { + var args = new GetGuildMembersParams + { + Limit = info.PageSize + }; + if (info.Position != null) + args.AfterUserId = info.Position.Value; + var models = await client.ApiClient.GetGuildMembersAsync(guild.Id, args, options); + return models.Select(x => RestGuildUser.Create(client, guild, x)).ToImmutableArray(); + }, + nextPage: (info, lastPage) => + { + info.Position = lastPage.Max(x => x.Id); + if (lastPage.Count != DiscordConfig.MaxMessagesPerBatch) + info.Remaining = 0; + }, + start: fromUserId, + count: limit + ); + } + public static async Task PruneUsersAsync(IGuild guild, BaseDiscordClient client, + int days, bool simulate, RequestOptions options) + { + var args = new GuildPruneParams(days); + GetGuildPruneCountResponse model; + if (simulate) + model = await client.ApiClient.GetGuildPruneCountAsync(guild.Id, args, options).ConfigureAwait(false); + else + model = await client.ApiClient.BeginGuildPruneAsync(guild.Id, args, options).ConfigureAwait(false); + return model.Pruned; + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestBan.cs b/src/Discord.Net.Rest/Entities/Guilds/RestBan.cs new file mode 100644 index 000000000..104bec903 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Guilds/RestBan.cs @@ -0,0 +1,28 @@ +using System.Diagnostics; +using Model = Discord.API.Ban; + +namespace Discord.Rest +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestBan : IBan + { + public RestUser User { get; } + public string Reason { get; } + + internal RestBan(RestUser user, string reason) + { + User = user; + Reason = reason; + } + internal static RestBan Create(BaseDiscordClient client, Model model) + { + return new RestBan(RestUser.Create(client, model.User), model.Reason); + } + + public override string ToString() => User.ToString(); + private string DebuggerDisplay => $"{User}: {Reason}"; + + //IBan + IUser IBan.User => User; + } +} diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs new file mode 100644 index 000000000..a0ba7a6de --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -0,0 +1,266 @@ +using Discord.API.Rest; +using Discord.Audio; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.Guild; +using EmbedModel = Discord.API.GuildEmbed; +using System.Linq; + +namespace Discord.Rest +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestGuild : RestEntity, IGuild, IUpdateable + { + private ImmutableDictionary _roles; + private ImmutableArray _emojis; + private ImmutableArray _features; + + 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; } + internal bool Available { get; private set; } + + public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public ulong DefaultChannelId => Id; + public string IconUrl => API.CDN.GetGuildIconUrl(Id, IconId); + public string SplashUrl => API.CDN.GetGuildSplashUrl(Id, SplashId); + + public RestRole EveryoneRole => GetRole(Id); + public IReadOnlyCollection Roles => _roles.ToReadOnlyCollection(); + public IReadOnlyCollection Emojis => _emojis; + public IReadOnlyCollection Features => _features; + + internal RestGuild(BaseDiscordClient client, ulong id) + : base(client, id) + { + } + internal static RestGuild Create(BaseDiscordClient discord, Model model) + { + var entity = new RestGuild(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(model.Emojis.Length); + for (int i = 0; i < model.Emojis.Length; i++) + emojis.Add(GuildEmoji.Create(model.Emojis[i])); + _emojis = emojis.ToImmutableArray(); + } + else + _emojis = ImmutableArray.Create(); + + if (model.Features != null) + _features = model.Features.ToImmutableArray(); + else + _features = ImmutableArray.Create(); + + var roles = ImmutableDictionary.CreateBuilder(); + if (model.Roles != null) + { + for (int i = 0; i < model.Roles.Length; i++) + roles[model.Roles[i].Id] = RestRole.Create(Discord, model.Roles[i]); + } + _roles = roles.ToImmutable(); + + Available = true; + } + internal void Update(EmbedModel model) + { + EmbedChannelId = model.ChannelId; + IsEmbeddable = model.Enabled; + } + + //General + public async Task UpdateAsync(RequestOptions options = null) + => Update(await Discord.ApiClient.GetGuildAsync(Id, options).ConfigureAwait(false)); + public Task DeleteAsync(RequestOptions options = null) + => GuildHelper.DeleteAsync(this, Discord, options); + + public async Task ModifyAsync(Action func, RequestOptions options = null) + { + var model = await GuildHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); + Update(model); + } + public async Task ModifyEmbedAsync(Action func, RequestOptions options = null) + { + var model = await GuildHelper.ModifyEmbedAsync(this, Discord, func, options).ConfigureAwait(false); + Update(model); + } + public async Task ModifyChannelsAsync(IEnumerable args, RequestOptions options = null) + { + var arr = args.ToArray(); + await GuildHelper.ModifyChannelsAsync(this, Discord, arr, options); + } + public async Task ModifyRolesAsync(IEnumerable args, RequestOptions options = null) + { + var models = await GuildHelper.ModifyRolesAsync(this, Discord, args, options).ConfigureAwait(false); + foreach (var model in models) + { + var role = GetRole(model.Id); + if (role != null) + role.Update(model); + } + } + + public Task LeaveAsync(RequestOptions options = null) + => GuildHelper.LeaveAsync(this, Discord, options); + + //Bans + public Task> GetBansAsync(RequestOptions options = null) + => GuildHelper.GetBansAsync(this, Discord, options); + + public Task AddBanAsync(IUser user, int pruneDays = 0, RequestOptions options = null) + => GuildHelper.AddBanAsync(this, Discord, user.Id, pruneDays, options); + public Task AddBanAsync(ulong userId, int pruneDays = 0, RequestOptions options = null) + => GuildHelper.AddBanAsync(this, Discord, userId, pruneDays, options); + + public Task RemoveBanAsync(IUser user, RequestOptions options = null) + => GuildHelper.RemoveBanAsync(this, Discord, user.Id, options); + public Task RemoveBanAsync(ulong userId, RequestOptions options = null) + => GuildHelper.RemoveBanAsync(this, Discord, userId, options); + + //Channels + public Task> GetChannelsAsync(RequestOptions options = null) + => GuildHelper.GetChannelsAsync(this, Discord, options); + public Task GetChannelAsync(ulong id, RequestOptions options = null) + => GuildHelper.GetChannelAsync(this, Discord, id, options); + public Task CreateTextChannelAsync(string name, RequestOptions options = null) + => GuildHelper.CreateTextChannelAsync(this, Discord, name, options); + public Task CreateVoiceChannelAsync(string name, RequestOptions options = null) + => GuildHelper.CreateVoiceChannelAsync(this, Discord, name, options); + + //Integrations + public Task> GetIntegrationsAsync(RequestOptions options = null) + => GuildHelper.GetIntegrationsAsync(this, Discord, options); + public Task CreateIntegrationAsync(ulong id, string type, RequestOptions options = null) + => GuildHelper.CreateIntegrationAsync(this, Discord, id, type, options); + + //Invites + public Task> GetInvitesAsync(RequestOptions options = null) + => GuildHelper.GetInvitesAsync(this, Discord, options); + + //Roles + public RestRole GetRole(ulong id) + { + RestRole value; + if (_roles.TryGetValue(id, out value)) + return value; + return null; + } + + public async Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), + bool isHoisted = false, RequestOptions options = null) + { + var role = await GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, options).ConfigureAwait(false); + _roles = _roles.Add(role.Id, role); + return role; + } + + //Users + public IAsyncEnumerable> GetUsersAsync(RequestOptions options = null) + => GuildHelper.GetUsersAsync(this, Discord, null, null, options); + public Task GetUserAsync(ulong id, RequestOptions options = null) + => GuildHelper.GetUserAsync(this, Discord, id, options); + public Task GetCurrentUserAsync(RequestOptions options = null) + => GuildHelper.GetUserAsync(this, Discord, Discord.CurrentUser.Id, options); + + public Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null) + => GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options); + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id})"; + + //IGuild + bool IGuild.Available => Available; + IAudioClient IGuild.AudioClient => null; + IRole IGuild.EveryoneRole => EveryoneRole; + IReadOnlyCollection IGuild.Roles => Roles; + + async Task> IGuild.GetBansAsync(RequestOptions options) + => await GetBansAsync(options).ConfigureAwait(false); + + async Task> IGuild.GetChannelsAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetChannelsAsync(options).ConfigureAwait(false); + else + return ImmutableArray.Create(); + } + async Task IGuild.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetChannelAsync(id, options).ConfigureAwait(false); + else + return null; + } + async Task IGuild.CreateTextChannelAsync(string name, RequestOptions options) + => await CreateTextChannelAsync(name, options).ConfigureAwait(false); + async Task IGuild.CreateVoiceChannelAsync(string name, RequestOptions options) + => await CreateVoiceChannelAsync(name, options).ConfigureAwait(false); + + async Task> IGuild.GetIntegrationsAsync(RequestOptions options) + => await GetIntegrationsAsync(options).ConfigureAwait(false); + async Task IGuild.CreateIntegrationAsync(ulong id, string type, RequestOptions options) + => await CreateIntegrationAsync(id, type, options).ConfigureAwait(false); + + async Task> IGuild.GetInvitesAsync(RequestOptions options) + => await GetInvitesAsync(options).ConfigureAwait(false); + + IRole IGuild.GetRole(ulong id) + => GetRole(id); + async Task IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted, RequestOptions options) + => await CreateRoleAsync(name, permissions, color, isHoisted, options).ConfigureAwait(false); + + async Task IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetUserAsync(id, options).ConfigureAwait(false); + else + return null; + } + async Task IGuild.GetCurrentUserAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetCurrentUserAsync(options).ConfigureAwait(false); + else + return null; + } + async Task> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return (await GetUsersAsync(options).Flatten().ConfigureAwait(false)).ToImmutableArray(); + else + return ImmutableArray.Create(); + } + Task IGuild.DownloadUsersAsync() { throw new NotSupportedException(); } + } +} diff --git a/src/Discord.Net/Entities/Guilds/GuildEmbed.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuildEmbed.cs similarity index 67% rename from src/Discord.Net/Entities/Guilds/GuildEmbed.cs rename to src/Discord.Net.Rest/Entities/Guilds/RestGuildEmbed.cs index f912fb076..f26a62d8d 100644 --- a/src/Discord.Net/Entities/Guilds/GuildEmbed.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuildEmbed.cs @@ -4,18 +4,20 @@ using Model = Discord.API.GuildEmbed; namespace Discord { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public struct GuildEmbed + public struct RestGuildEmbed { public bool IsEnabled { get; private set; } public ulong? ChannelId { get; private set; } - public GuildEmbed(bool isEnabled, ulong? channelId) + internal RestGuildEmbed(bool isEnabled, ulong? channelId) { ChannelId = channelId; IsEnabled = isEnabled; } - internal GuildEmbed(Model model) - : this(model.Enabled, model.ChannelId) { } + internal static RestGuildEmbed Create(Model model) + { + return new RestGuildEmbed(model.Enabled, model.ChannelId); + } public override string ToString() => ChannelId?.ToString(); private string DebuggerDisplay => $"{ChannelId} ({(IsEnabled ? "Enabled" : "Disabled")})"; diff --git a/src/Discord.Net/Rest/Entities/Guilds/GuildIntegration.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs similarity index 65% rename from src/Discord.Net/Rest/Entities/Guilds/GuildIntegration.cs rename to src/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs index 135c0f3f3..b90c492ab 100644 --- a/src/Discord.Net/Rest/Entities/Guilds/GuildIntegration.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs @@ -1,5 +1,4 @@ using Discord.API.Rest; -using Discord.Rest; using System; using System.Diagnostics; using System.Threading.Tasks; @@ -8,7 +7,7 @@ using Model = Discord.API.Integration; namespace Discord.Rest { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - internal class GuildIntegration : Entity, IGuildIntegration + public class RestGuildIntegration : RestEntity, IGuildIntegration { private long _syncedAtTicks; @@ -18,26 +17,26 @@ namespace Discord.Rest 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 ulong GuildId { get; private set; } + public ulong RoleId { get; private set; } + public RestUser 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) + internal RestGuildIntegration(BaseDiscordClient discord, ulong id) + : base(discord, id) { - Guild = guild; - Update(model, UpdateSource.Creation); } - - public void Update(Model model, UpdateSource source) + internal static RestGuildIntegration Create(BaseDiscordClient discord, Model model) { - if (source == UpdateSource.Rest && IsAttached) return; + var entity = new RestGuildIntegration(discord, model.Id); + entity.Update(model); + return entity; + } + public void Update(Model model) + { Name = model.Name; Type = model.Type; IsEnabled = model.Enabled; @@ -46,13 +45,13 @@ namespace Discord.Rest ExpireGracePeriod = model.ExpireGracePeriod; _syncedAtTicks = model.SyncedAt.UtcTicks; - Role = Guild.GetRole(model.RoleId); - User = new User(model.User); + RoleId = model.RoleId; + User = RestUser.Create(Discord, model.User); } public async Task DeleteAsync() { - await Discord.ApiClient.DeleteGuildIntegrationAsync(Guild.Id, Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteGuildIntegrationAsync(GuildId, Id).ConfigureAwait(false); } public async Task ModifyAsync(Action func) { @@ -60,20 +59,18 @@ namespace Discord.Rest var args = new ModifyGuildIntegrationParams(); func(args); - var model = await Discord.ApiClient.ModifyGuildIntegrationAsync(Guild.Id, Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.ModifyGuildIntegrationAsync(GuildId, Id, args).ConfigureAwait(false); - Update(model, UpdateSource.Rest); + Update(model); } public async Task SyncAsync() { - await Discord.ApiClient.SyncGuildIntegrationAsync(Guild.Id, Id).ConfigureAwait(false); + await Discord.ApiClient.SyncGuildIntegrationAsync(GuildId, 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; } } diff --git a/src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs similarity index 50% rename from src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs rename to src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs index 3e8818a41..69c2c9362 100644 --- a/src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs @@ -1,4 +1,4 @@ -using Discord.Rest; +using System; using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.UserGuild; @@ -6,7 +6,7 @@ using Model = Discord.API.UserGuild; namespace Discord.Rest { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - internal class UserGuild : SnowflakeEntity, IUserGuild + public class RestUserGuild : RestEntity, ISnowflakeEntity, IUserGuild { private string _iconId; @@ -14,33 +14,35 @@ namespace Discord.Rest public bool IsOwner { get; private set; } public GuildPermissions Permissions { get; private set; } - public override DiscordRestClient Discord { get; } - + public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); public string IconUrl => API.CDN.GetGuildIconUrl(Id, _iconId); - public UserGuild(DiscordRestClient discord, Model model) - : base(model.Id) + internal RestUserGuild(BaseDiscordClient discord, ulong id) + : base(discord, id) { - Discord = discord; - Update(model, UpdateSource.Creation); } - public void Update(Model model, UpdateSource source) + internal static RestUserGuild Create(BaseDiscordClient discord, Model model) { - if (source == UpdateSource.Rest && IsAttached) return; + var entity = new RestUserGuild(discord, model.Id); + entity.Update(model); + return entity; + } + public void Update(Model model) + { _iconId = model.Icon; IsOwner = model.Owner; Name = model.Name; Permissions = new GuildPermissions(model.Permissions); } - public async Task LeaveAsync() + public async Task LeaveAsync(RequestOptions options = null) { - await Discord.ApiClient.LeaveGuildAsync(Id).ConfigureAwait(false); + await Discord.ApiClient.LeaveGuildAsync(Id, options).ConfigureAwait(false); } - public async Task DeleteAsync() + public async Task DeleteAsync(RequestOptions options = null) { - await Discord.ApiClient.DeleteGuildAsync(Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteGuildAsync(Id, options).ConfigureAwait(false); } public override string ToString() => Name; diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestVoiceRegion.cs b/src/Discord.Net.Rest/Entities/Guilds/RestVoiceRegion.cs new file mode 100644 index 000000000..47fd2cd19 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Guilds/RestVoiceRegion.cs @@ -0,0 +1,38 @@ +using Discord.Rest; +using System.Diagnostics; +using Model = Discord.API.VoiceRegion; + +namespace Discord +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class RestVoiceRegion : RestEntity, IVoiceRegion + { + public string Name { get; private set; } + public bool IsVip { get; private set; } + public bool IsOptimal { get; private set; } + public string SampleHostname { get; private set; } + public int SamplePort { get; private set; } + + internal RestVoiceRegion(BaseDiscordClient client, string id) + : base(client, id) + { + } + internal static RestVoiceRegion Create(BaseDiscordClient client, Model model) + { + var entity = new RestVoiceRegion(client, model.Id); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { + Name = model.Name; + IsVip = model.IsVip; + IsOptimal = model.IsOptimal; + SampleHostname = model.SampleHostname; + SamplePort = model.SamplePort; + } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id}{(IsVip ? ", VIP" : "")}{(IsOptimal ? ", Optimal" : "")})"; + } +} diff --git a/src/Discord.Net.Rest/Entities/Invites/InviteHelper.cs b/src/Discord.Net.Rest/Entities/Invites/InviteHelper.cs new file mode 100644 index 000000000..8ec428178 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Invites/InviteHelper.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; +using Model = Discord.API.Invite; + +namespace Discord.Rest +{ + internal static class InviteHelper + { + public static async Task AcceptAsync(IInvite invite, BaseDiscordClient client, + RequestOptions options) + { + await client.ApiClient.AcceptInviteAsync(invite.Code, options).ConfigureAwait(false); + } + public static async Task DeleteAsync(IInvite invite, BaseDiscordClient client, + RequestOptions options) + { + await client.ApiClient.DeleteInviteAsync(invite.Code, options).ConfigureAwait(false); + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs b/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs new file mode 100644 index 000000000..4c870f3f4 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs @@ -0,0 +1,52 @@ +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.Invite; + +namespace Discord.Rest +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestInvite : RestEntity, IInvite, IUpdateable + { + public string ChannelName { get; private set; } + public string GuildName { get; private set; } + public ulong ChannelId { get; private set; } + public ulong GuildId { get; private set; } + + public string Code => Id; + public string Url => $"{DiscordConfig.InviteUrl}/{Code}"; + + internal RestInvite(BaseDiscordClient discord, string id) + : base(discord, id) + { + } + internal static RestInvite Create(BaseDiscordClient discord, Model model) + { + var entity = new RestInvite(discord, model.Code); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { + GuildId = model.Guild.Id; + ChannelId = model.Channel.Id; + GuildName = model.Guild.Name; + ChannelName = model.Channel.Name; + } + + public async Task UpdateAsync(RequestOptions options = null) + { + var model = await Discord.ApiClient.GetInviteAsync(Code, options).ConfigureAwait(false); + Update(model); + } + public Task DeleteAsync(RequestOptions options = null) + => InviteHelper.DeleteAsync(this, Discord, options); + + public Task AcceptAsync(RequestOptions options = null) + => InviteHelper.AcceptAsync(this, Discord, options); + + public override string ToString() => Url; + private string DebuggerDisplay => $"{Url} ({GuildName} / {ChannelName})"; + + string IEntity.Id => Code; + } +} diff --git a/src/Discord.Net/Rest/Entities/Invites/InviteMetadata.cs b/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs similarity index 52% rename from src/Discord.Net/Rest/Entities/Invites/InviteMetadata.cs rename to src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs index 05a10514c..cd1a66311 100644 --- a/src/Discord.Net/Rest/Entities/Invites/InviteMetadata.cs +++ b/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs @@ -1,10 +1,10 @@ -using Discord.Rest; -using System; +using System; +using System.Diagnostics; using Model = Discord.API.InviteMetadata; namespace Discord.Rest { - internal class InviteMetadata : Invite, IInviteMetadata + public class RestInviteMetadata : RestInvite, IInviteMetadata { private long _createdAtTicks; @@ -13,20 +13,24 @@ namespace Discord.Rest public int? MaxAge { get; private set; } public int? MaxUses { get; private set; } public int Uses { get; private set; } - public IUser Inviter { get; private set; } + public RestUser Inviter { get; private set; } public DateTimeOffset CreatedAt => DateTimeUtils.FromTicks(_createdAtTicks); - public InviteMetadata(DiscordRestClient client, Model model) - : base(client, model) + internal RestInviteMetadata(BaseDiscordClient discord, string id) + : base(discord, id) { - Update(model, UpdateSource.Creation); } - public void Update(Model model, UpdateSource source) + internal static RestInviteMetadata Create(BaseDiscordClient discord, Model model) { - if (source == UpdateSource.Rest && IsAttached) return; - - Inviter = new User(model.Inviter); + var entity = new RestInviteMetadata(discord, model.Code); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { + base.Update(model); + Inviter = RestUser.Create(Discord, model.Inviter); IsRevoked = model.Revoked; IsTemporary = model.Temporary; MaxAge = model.MaxAge != 0 ? model.MaxAge : (int?)null; @@ -34,5 +38,7 @@ namespace Discord.Rest Uses = model.Uses; _createdAtTicks = model.CreatedAt.UtcTicks; } + + IUser IInviteMetadata.Inviter => Inviter; } } diff --git a/src/Discord.Net.Rest/Entities/Messages/Attachment.cs b/src/Discord.Net.Rest/Entities/Messages/Attachment.cs new file mode 100644 index 000000000..e185234ac --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Messages/Attachment.cs @@ -0,0 +1,37 @@ +using System.Diagnostics; +using Model = Discord.API.Attachment; + +namespace Discord +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class Attachment : IAttachment + { + public ulong Id { get; } + public string Filename { get; } + public string Url { get; } + public string ProxyUrl { get; } + public int Size { get; } + public int? Height { get; } + public int? Width { get; } + + internal Attachment(ulong id, string filename, string url, string proxyUrl, int size, int? height, int? width) + { + Id = id; + Filename = filename; + Url = url; + ProxyUrl = proxyUrl; + Size = size; + Height = height; + Width = width; + } + internal static Attachment Create(Model model) + { + return new Attachment(model.Id, model.Filename, model.Url, model.ProxyUrl, model.Size, + model.Height.IsSpecified ? model.Height.Value : (int?)null, + model.Width.IsSpecified ? model.Width.Value : (int?)null); + } + + public override string ToString() => Filename; + private string DebuggerDisplay => $"{Filename} ({Size} bytes)"; + } +} diff --git a/src/Discord.Net.Rest/Entities/Messages/Embed.cs b/src/Discord.Net.Rest/Entities/Messages/Embed.cs new file mode 100644 index 000000000..20979534e --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Messages/Embed.cs @@ -0,0 +1,35 @@ +using System.Diagnostics; +using Model = Discord.API.Embed; + +namespace Discord +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class Embed : IEmbed + { + public string Description { get; } + public string Url { get; } + public string Title { get; } + public string Type { get; } + public EmbedProvider? Provider { get; } + public EmbedThumbnail? Thumbnail { get; } + + internal Embed(string type, string title, string description, string url, EmbedProvider? provider, EmbedThumbnail? thumbnail) + { + Type = type; + Title = title; + Description = description; + Url = url; + Provider = provider; + Thumbnail = thumbnail; + } + internal static Embed Create(Model model) + { + return new Embed(model.Type, model.Title, model.Description, model.Url, + model.Provider.IsSpecified ? EmbedProvider.Create(model.Provider.Value) : (EmbedProvider?)null, + model.Thumbnail.IsSpecified ? EmbedThumbnail.Create(model.Thumbnail.Value) : (EmbedThumbnail?)null); + } + + public override string ToString() => Title; + private string DebuggerDisplay => $"{Title} ({Type})"; + } +} diff --git a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs new file mode 100644 index 000000000..358f6f5a9 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -0,0 +1,127 @@ +using Discord.API.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Message; + +namespace Discord.Rest +{ + internal static class MessageHelper + { + public static async Task ModifyAsync(IMessage msg, BaseDiscordClient client, Action func, + RequestOptions options) + { + var args = new ModifyMessageParams(); + func(args); + return await client.ApiClient.ModifyMessageAsync(msg.Channel.Id, msg.Id, args, options).ConfigureAwait(false); + } + public static async Task DeleteAsync(IMessage msg, BaseDiscordClient client, + RequestOptions options) + { + await client.ApiClient.DeleteMessageAsync(msg.Channel.Id, msg.Id, options).ConfigureAwait(false); + } + + public static async Task PinAsync(IMessage msg, BaseDiscordClient client, + RequestOptions options) + { + await client.ApiClient.AddPinAsync(msg.Channel.Id, msg.Id, options).ConfigureAwait(false); + } + public static async Task UnpinAsync(IMessage msg, BaseDiscordClient client, + RequestOptions options) + { + await client.ApiClient.RemovePinAsync(msg.Channel.Id, msg.Id, options).ConfigureAwait(false); + } + + public static ImmutableArray ParseTags(string text, IMessageChannel channel, IGuild guild, ImmutableArray userMentions) + { + var tags = new SortedList(); + + int index = 0; + while (true) + { + index = text.IndexOf('<', index); + if (index == -1) break; + int endIndex = text.IndexOf('>', index + 1); + if (endIndex == -1) break; + string content = text.Substring(index, endIndex - index + 1); + + ulong id; + if (MentionUtils.TryParseUser(content, out id)) + { + IUser mentionedUser = null; + foreach (var mention in userMentions) + { + if (mention.Id == id) + { + mentionedUser = channel?.GetUserAsync(id, CacheMode.CacheOnly).GetAwaiter().GetResult(); + if (mentionedUser == null) + mentionedUser = mention; + break; + } + } + tags.Add(index, new Tag(TagType.UserMention, index, content.Length, id, mentionedUser)); + } + else if (MentionUtils.TryParseChannel(content, out id)) + { + IChannel mentionedChannel = null; + if (guild != null) + mentionedChannel = guild.GetChannelAsync(id, CacheMode.CacheOnly).GetAwaiter().GetResult(); + tags.Add(index, new Tag(TagType.ChannelMention, index, content.Length, id, mentionedChannel)); + } + else if (MentionUtils.TryParseRole(content, out id)) + { + IRole mentionedRole = null; + if (guild != null) + mentionedRole = guild.GetRole(id); + tags.Add(index, new Tag(TagType.RoleMention, index, content.Length, id, mentionedRole)); + } + else + { + Emoji emoji; + if (Emoji.TryParse(content, out emoji)) + tags.Add(index, new Tag(TagType.Emoji, index, content.Length, id, emoji)); + } + index = endIndex + 1; + } + + index = 0; + while (true) + { + index = text.IndexOf("@everyone", index); + if (index == -1) break; + + tags.Add(index, new Tag(TagType.EveryoneMention, index, "@everyone".Length, 0, null)); + index++; + } + + index = 0; + while (true) + { + index = text.IndexOf("@here", index); + if (index == -1) break; + + tags.Add(index, new Tag(TagType.HereMention, index, "@here".Length, 0, null)); + index++; + } + + return tags.Values.ToImmutableArray(); + } + public static ImmutableArray FilterTagsByKey(TagType type, ImmutableArray tags) + { + return tags + .Where(x => x.Type == type) + .Select(x => x.Key) + .ToImmutableArray(); + } + public static ImmutableArray FilterTagsByValue(TagType type, ImmutableArray tags) + { + return tags + .Where(x => x.Type == type) + .Select(x => (T)x.Value) + .Where(x => x != null) + .ToImmutableArray(); + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs new file mode 100644 index 000000000..e89d6faf7 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Message; + +namespace Discord.Rest +{ + public abstract class RestMessage : RestEntity, IMessage, IUpdateable + { + internal readonly IGuild _guild; + private long _timestampTicks; + + public IMessageChannel Channel { get; } + public RestUser Author { get; } + + public string Content { get; private set; } + + public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public virtual bool IsTTS => false; + public virtual bool IsPinned => false; + public virtual DateTimeOffset? EditedTimestamp => null; + public virtual IReadOnlyCollection Attachments => ImmutableArray.Create(); + public virtual IReadOnlyCollection Embeds => ImmutableArray.Create(); + public virtual IReadOnlyCollection MentionedChannelIds => ImmutableArray.Create(); + public virtual IReadOnlyCollection MentionedRoleIds => ImmutableArray.Create(); + public virtual IReadOnlyCollection MentionedUsers => ImmutableArray.Create(); + public virtual IReadOnlyCollection Tags => ImmutableArray.Create(); + public virtual ulong? WebhookId => null; + public bool IsWebhook => WebhookId != null; + + public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); + + internal RestMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, RestUser author, IGuild guild) + : base(discord, id) + { + Channel = channel; + Author = author; + _guild = guild; + } + internal static RestMessage Create(BaseDiscordClient discord, IGuild guild, Model model) + { + if (model.Type == MessageType.Default) + return RestUserMessage.Create(discord, guild, model); + else + return RestSystemMessage.Create(discord, guild, 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(RequestOptions options = null) + { + var model = await Discord.ApiClient.GetChannelMessageAsync(Channel.Id, Id, options).ConfigureAwait(false); + Update(model); + } + + public override string ToString() => Content; + + MessageType IMessage.Type => MessageType.Default; + IUser IMessage.Author => Author; + IReadOnlyCollection IMessage.Attachments => Attachments; + IReadOnlyCollection IMessage.Embeds => Embeds; + IReadOnlyCollection IMessage.MentionedUserIds => MentionedUsers.Select(x => x.Id).ToImmutableArray(); + } +} diff --git a/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs new file mode 100644 index 000000000..0725ab603 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs @@ -0,0 +1,32 @@ +using System.Diagnostics; +using Model = Discord.API.Message; + +namespace Discord.Rest +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestSystemMessage : RestMessage, ISystemMessage + { + public MessageType Type { get; private set; } + + internal RestSystemMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, RestUser author, IGuild guild) + : base(discord, id, channel, author, guild) + { + } + internal new static RestSystemMessage Create(BaseDiscordClient discord, IGuild guild, Model model) + { + var entity = new RestSystemMessage(discord, model.Id, + RestVirtualMessageChannel.Create(discord, model.ChannelId), + RestUser.Create(discord, model.Author.Value), guild); + entity.Update(model); + return entity; + } + internal override void Update(Model model) + { + base.Update(model); + + Type = model.Type; + } + + private string DebuggerDisplay => $"{Author}: {Content} ({Id}, {Type})"; + } +} diff --git a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs new file mode 100644 index 000000000..21f87c18f --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs @@ -0,0 +1,133 @@ +using Discord.API.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Message; + +namespace Discord.Rest +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestUserMessage : RestMessage, IUserMessage + { + private bool _isMentioningEveryone, _isTTS, _isPinned; + private long? _editedTimestampTicks; + private ulong? _webhookId; + private ImmutableArray _attachments; + private ImmutableArray _embeds; + private ImmutableArray _tags; + + public override bool IsTTS => _isTTS; + public override bool IsPinned => _isPinned; + public override ulong? WebhookId => _webhookId; + public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks); + public override IReadOnlyCollection Attachments => _attachments; + public override IReadOnlyCollection Embeds => _embeds; + public override IReadOnlyCollection MentionedChannelIds => MessageHelper.FilterTagsByKey(TagType.ChannelMention, _tags); + public override IReadOnlyCollection MentionedRoleIds => MessageHelper.FilterTagsByKey(TagType.RoleMention, _tags); + public override IReadOnlyCollection MentionedUsers => MessageHelper.FilterTagsByValue(TagType.UserMention, _tags); + public override IReadOnlyCollection Tags => _tags; + + internal RestUserMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, RestUser author, IGuild guild) + : base(discord, id, channel, author, guild) + { + } + internal new static RestUserMessage Create(BaseDiscordClient discord, IGuild guild, Model model) + { + var entity = new RestUserMessage(discord, model.Id, + RestVirtualMessageChannel.Create(discord, model.ChannelId), + RestUser.Create(discord, model.Author.Value), guild); + 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.WebhookId.IsSpecified) + _webhookId = model.WebhookId.Value; + + if (model.Attachments.IsSpecified) + { + var value = model.Attachments.Value; + if (value.Length > 0) + { + var attachments = ImmutableArray.CreateBuilder(value.Length); + for (int i = 0; i < value.Length; i++) + attachments.Add(Attachment.Create(value[i])); + _attachments = attachments.ToImmutable(); + } + else + _attachments = ImmutableArray.Create(); + } + + if (model.Embeds.IsSpecified) + { + var value = model.Embeds.Value; + if (value.Length > 0) + { + var embeds = ImmutableArray.CreateBuilder(value.Length); + for (int i = 0; i < value.Length; i++) + embeds.Add(Embed.Create(value[i])); + _embeds = embeds.ToImmutable(); + } + else + _embeds = ImmutableArray.Create(); + } + + ImmutableArray mentions = ImmutableArray.Create(); + if (model.UserMentions.IsSpecified) + { + var value = model.UserMentions.Value; + if (value.Length > 0) + { + var newMentions = ImmutableArray.CreateBuilder(value.Length); + for (int i = 0; i < value.Length; i++) + { + var val = value[i]; + if (val.Object != null) + newMentions.Add(RestUser.Create(Discord, val.Object)); + } + mentions = newMentions.ToImmutable(); + } + } + + if (model.Content.IsSpecified) + { + var text = model.Content.Value; + _tags = MessageHelper.ParseTags(text, null, _guild, mentions); + model.Content = text; + } + } + + public async Task ModifyAsync(Action func, RequestOptions options) + { + var model = await MessageHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); + Update(model); + } + public Task DeleteAsync(RequestOptions options) + => MessageHelper.DeleteAsync(this, Discord, options); + + public Task PinAsync(RequestOptions options) + => MessageHelper.PinAsync(this, Discord, options); + public Task UnpinAsync(RequestOptions options) + => MessageHelper.UnpinAsync(this, Discord, options); + + public string Resolve(TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, + TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) + => MentionUtils.Resolve(this, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); + + private string DebuggerDisplay => $"{Author}: {Content} ({Id}{(Attachments.Count > 0 ? $", {Attachments.Count} Attachments" : "")})"; + } +} diff --git a/src/Discord.Net/Rest/Entities/Application.cs b/src/Discord.Net.Rest/Entities/RestApplication.cs similarity index 50% rename from src/Discord.Net/Rest/Entities/Application.cs rename to src/Discord.Net.Rest/Entities/RestApplication.cs index 2e743daf4..62b434044 100644 --- a/src/Discord.Net/Rest/Entities/Application.cs +++ b/src/Discord.Net.Rest/Entities/RestApplication.cs @@ -1,11 +1,12 @@ -using Discord.Rest; -using System; +using System; +using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.Application; namespace Discord.Rest { - internal class Application : SnowflakeEntity, IApplication + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestApplication : RestEntity, IApplication { protected string _iconId; @@ -14,39 +15,43 @@ namespace Discord.Rest public string[] RPCOrigins { get; private set; } public ulong Flags { get; private set; } - public override DiscordRestClient Discord { get; } public IUser Owner { get; private set; } + public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); public string IconUrl => API.CDN.GetApplicationIconUrl(Id, _iconId); - public Application(DiscordRestClient discord, Model model) - : base(model.Id) + internal RestApplication(BaseDiscordClient discord, ulong id) + : base(discord, id) { - Discord = discord; - - Update(model, UpdateSource.Creation); } - - internal void Update(Model model, UpdateSource source) + internal static RestApplication Create(BaseDiscordClient discord, Model model) { - if (source == UpdateSource.Rest && IsAttached) return; - + var entity = new RestApplication(discord, model.Id); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { Description = model.Description; RPCOrigins = model.RPCOrigins; Name = model.Name; - Flags = model.Flags; - Owner = new User(model.Owner); _iconId = model.Icon; + + if (model.Flags.IsSpecified) + Flags = model.Flags.Value; //TODO: Do we still need this? + if (model.Owner.IsSpecified) + Owner = RestUser.Create(Discord, model.Owner.Value); } public async Task UpdateAsync() { - if (IsAttached) throw new NotSupportedException(); - var response = await Discord.ApiClient.GetMyApplicationAsync().ConfigureAwait(false); if (response.Id != Id) throw new InvalidOperationException("Unable to update this object from a different application token."); - Update(response, UpdateSource.Rest); + Update(response); } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id})"; } } diff --git a/src/Discord.Net.Rest/Entities/RestEntity.cs b/src/Discord.Net.Rest/Entities/RestEntity.cs new file mode 100644 index 000000000..adf7b6921 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/RestEntity.cs @@ -0,0 +1,19 @@ +using System; + +namespace Discord.Rest +{ + public abstract class RestEntity : IEntity + where T : IEquatable + { + public BaseDiscordClient Discord { get; } + public T Id { get; } + + internal RestEntity(BaseDiscordClient discord, T id) + { + Discord = discord; + Id = id; + } + + IDiscordClient IEntity.Discord => Discord; + } +} diff --git a/src/Discord.Net.Rest/Entities/Roles/RestRole.cs b/src/Discord.Net.Rest/Entities/Roles/RestRole.cs new file mode 100644 index 000000000..6e81ce9df --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Roles/RestRole.cs @@ -0,0 +1,60 @@ +using Discord.API.Rest; +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.Role; + +namespace Discord.Rest +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestRole : RestEntity, IRole + { + public RestGuild Guild { get; } + public Color Color { get; private set; } + public bool IsHoisted { get; private set; } + public bool IsManaged { get; private set; } + public bool IsMentionable { get; private set; } + public string Name { get; private set; } + public GuildPermissions Permissions { get; private set; } + public int Position { get; private set; } + + public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public bool IsEveryone => Id == Guild.Id; + public string Mention => MentionUtils.MentionRole(Id); + + internal RestRole(BaseDiscordClient discord, ulong id) + : base(discord, id) + { + } + internal static RestRole Create(BaseDiscordClient discord, Model model) + { + var entity = new RestRole(discord, model.Id); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { + Name = model.Name; + IsHoisted = model.Hoist; + IsManaged = model.Managed; + IsMentionable = model.Mentionable; + Position = model.Position; + Color = new Color(model.Color); + Permissions = new GuildPermissions(model.Permissions); + } + + public async Task ModifyAsync(Action func, RequestOptions options = null) + { + var model = await RoleHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); + Update(model); + } + public Task DeleteAsync(RequestOptions options = null) + => RoleHelper.DeleteAsync(this, Discord, options); + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id})"; + + //IRole + IGuild IRole.Guild => Guild; + } +} diff --git a/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs b/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs new file mode 100644 index 000000000..0b102098c --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs @@ -0,0 +1,24 @@ +using Discord.API.Rest; +using System; +using System.Threading.Tasks; +using Model = Discord.API.Role; + +namespace Discord.Rest +{ + internal static class RoleHelper + { + //General + public static async Task DeleteAsync(IRole role, BaseDiscordClient client, + RequestOptions options) + { + await client.ApiClient.DeleteGuildRoleAsync(role.Guild.Id, role.Id, options).ConfigureAwait(false); + } + public static async Task ModifyAsync(IRole role, BaseDiscordClient client, + Action func, RequestOptions options) + { + var args = new ModifyGuildRoleParams(); + func(args); + return await client.ApiClient.ModifyGuildRoleAsync(role.Guild.Id, role.Id, args, options).ConfigureAwait(false); + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Users/RestConnection.cs b/src/Discord.Net.Rest/Entities/Users/RestConnection.cs new file mode 100644 index 000000000..b8b83be3e --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Users/RestConnection.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using Model = Discord.API.Connection; + +namespace Discord +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestConnection : IConnection + { + public string Id { get; } + public string Type { get; } + public string Name { get; } + public bool IsRevoked { get; } + public IReadOnlyCollection IntegrationIds { get; } + + internal RestConnection(string id, string type, string name, bool isRevoked, IReadOnlyCollection integrationIds) + { + Id = id; + Type = type; + Name = name; + IsRevoked = isRevoked; + + IntegrationIds = integrationIds; + } + internal static RestConnection Create(Model model) + { + return new RestConnection(model.Id, model.Type, model.Name, model.Revoked, model.Integrations.ToImmutableArray()); + } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id}, {Type}{(IsRevoked ? ", Revoked" : "")})"; + } +} diff --git a/src/Discord.Net.Rest/Entities/Users/RestGroupUser.cs b/src/Discord.Net.Rest/Entities/Users/RestGroupUser.cs new file mode 100644 index 000000000..951bd2e7c --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Users/RestGroupUser.cs @@ -0,0 +1,29 @@ +using System.Diagnostics; +using Model = Discord.API.User; + +namespace Discord.Rest +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestGroupUser : RestUser, IGroupUser + { + internal RestGroupUser(BaseDiscordClient discord, ulong id) + : base(discord, id) + { + } + internal new static RestGroupUser Create(BaseDiscordClient discord, Model model) + { + var entity = new RestGroupUser(discord, model.Id); + entity.Update(model); + return entity; + } + + //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; + } +} diff --git a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs new file mode 100644 index 000000000..6d1cac024 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs @@ -0,0 +1,96 @@ +using Discord.API.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.GuildMember; + +namespace Discord.Rest +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestGuildUser : RestUser, IGuildUser, IUpdateable + { + private long? _joinedAtTicks; + private ImmutableArray _roleIds; + + public string Nickname { get; private set; } + internal IGuild Guild { get; private set; } + public bool IsDeafened { get; private set; } + public bool IsMuted { get; private set; } + + public ulong GuildId => Guild.Id; + public GuildPermissions GuildPermissions + { + get + { + if (!Guild.Available) + throw new InvalidOperationException("Resolving permissions requires the parent guild to be downloaded."); + return new GuildPermissions(Permissions.ResolveGuild(Guild, this)); + } + } + public IReadOnlyCollection RoleIds => _roleIds; + + public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); + + internal RestGuildUser(BaseDiscordClient discord, IGuild guild, ulong id) + : base(discord, id) + { + Guild = guild; + } + internal static RestGuildUser Create(BaseDiscordClient discord, IGuild guild, Model model) + { + var entity = new RestGuildUser(discord, guild, model.User.Id); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { + _joinedAtTicks = model.JoinedAt.UtcTicks; + if (model.Nick.IsSpecified) + Nickname = model.Nick.Value; + IsDeafened = model.Deaf; + IsMuted = model.Mute; + UpdateRoles(model.Roles); + } + private void UpdateRoles(ulong[] roleIds) + { + var roles = ImmutableArray.CreateBuilder(roleIds.Length + 1); + roles.Add(Guild.Id); + for (int i = 0; i < roleIds.Length; i++) + roles.Add(roleIds[i]); + _roleIds = roles.ToImmutable(); + } + + public override async Task UpdateAsync(RequestOptions options = null) + { + var model = await Discord.ApiClient.GetGuildMemberAsync(GuildId, Id, options).ConfigureAwait(false); + Update(model); + } + public async Task ModifyAsync(Action func, RequestOptions options = null) + { + var args = await UserHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); + if (args.Deaf.IsSpecified) + IsDeafened = args.Deaf.Value; + if (args.Mute.IsSpecified) + IsMuted = args.Mute.Value; + if (args.RoleIds.IsSpecified) + UpdateRoles(args.RoleIds.Value); + } + public Task KickAsync(RequestOptions options = null) + => UserHelper.KickAsync(this, Discord, options); + + public ChannelPermissions GetPermissions(IGuildChannel channel) + { + var guildPerms = GuildPermissions; + return new ChannelPermissions(Permissions.ResolveChannel(Guild, this, channel, guildPerms.RawValue)); + } + + //IVoiceState + bool IVoiceState.IsSelfDeafened => false; + bool IVoiceState.IsSelfMuted => false; + bool IVoiceState.IsSuppressed => false; + IVoiceChannel IVoiceState.VoiceChannel => null; + string IVoiceState.VoiceSessionId => null; + } +} diff --git a/src/Discord.Net.Rest/Entities/Users/RestSelfUser.cs b/src/Discord.Net.Rest/Entities/Users/RestSelfUser.cs new file mode 100644 index 000000000..368d8c798 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Users/RestSelfUser.cs @@ -0,0 +1,56 @@ +using Discord.API.Rest; +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.User; + +namespace Discord.Rest +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestSelfUser : RestUser, ISelfUser + { + public string Email { get; private set; } + public bool IsVerified { get; private set; } + public bool IsMfaEnabled { get; private set; } + + internal RestSelfUser(BaseDiscordClient discord, ulong id) + : base(discord, id) + { + } + internal new static RestSelfUser Create(BaseDiscordClient discord, Model model) + { + var entity = new RestSelfUser(discord, model.Id); + entity.Update(model); + return entity; + } + internal override void Update(Model model) + { + base.Update(model); + + if (model.Email.IsSpecified) + Email = model.Email.Value; + if (model.Verified.IsSpecified) + IsVerified = model.Verified.Value; + if (model.MfaEnabled.IsSpecified) + IsMfaEnabled = model.MfaEnabled.Value; + } + + public override async Task UpdateAsync(RequestOptions options = null) + { + var model = await Discord.ApiClient.GetMyUserAsync(options).ConfigureAwait(false); + if (model.Id != Id) + throw new InvalidOperationException("Unable to update this object using a different token."); + Update(model); + } + + public async Task ModifyAsync(Action func, RequestOptions options = null) + { + if (Id != Discord.CurrentUser.Id) + throw new InvalidOperationException("Unable to modify this object using a different token."); + var model = await UserHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); + Update(model); + } + + Task ISelfUser.ModifyStatusAsync(Action func, RequestOptions options) { throw new NotSupportedException(); } + } +} diff --git a/src/Discord.Net.Rest/Entities/Users/RestUser.cs b/src/Discord.Net.Rest/Entities/Users/RestUser.cs new file mode 100644 index 000000000..25419932f --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -0,0 +1,63 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.User; + +namespace Discord.Rest +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestUser : RestEntity, IUser, IUpdateable + { + public bool IsBot { get; private set; } + public string Username { get; private set; } + public ushort DiscriminatorValue { get; private set; } + public string AvatarId { get; private set; } + + public string AvatarUrl => API.CDN.GetUserAvatarUrl(Id, AvatarId); + public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public string Discriminator => DiscriminatorValue.ToString("D4"); + public string Mention => MentionUtils.MentionUser(Id); + public virtual Game? Game => null; + public virtual UserStatus Status => UserStatus.Unknown; + + internal RestUser(BaseDiscordClient discord, ulong id) + : base(discord, id) + { + } + internal static RestUser Create(BaseDiscordClient discord, Model model) + { + var entity = new RestUser(discord, model.Id); + entity.Update(model); + return entity; + } + internal virtual void Update(Model model) + { + if (model.Avatar.IsSpecified) + AvatarId = model.Avatar.Value; + if (model.Discriminator.IsSpecified) + DiscriminatorValue = ushort.Parse(model.Discriminator.Value); + if (model.Bot.IsSpecified) + IsBot = model.Bot.Value; + if (model.Username.IsSpecified) + Username = model.Username.Value; + } + + public virtual async Task UpdateAsync(RequestOptions options = null) + { + var model = await Discord.ApiClient.GetUserAsync(Id, options).ConfigureAwait(false); + Update(model); + } + + public Task CreateDMChannelAsync(RequestOptions options = null) + => UserHelper.CreateDMChannelAsync(this, Discord, options); + + public override string ToString() => $"{Username}#{Discriminator}"; + internal string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; + + //IUser + Task IUser.GetDMChannelAsync(CacheMode mode, RequestOptions options) + => Task.FromResult(null); + async Task IUser.CreateDMChannelAsync(RequestOptions options) + => await CreateDMChannelAsync(options).ConfigureAwait(false); + } +} diff --git a/src/Discord.Net.Rest/Entities/Users/UserHelper.cs b/src/Discord.Net.Rest/Entities/Users/UserHelper.cs new file mode 100644 index 000000000..545703f0d --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Users/UserHelper.cs @@ -0,0 +1,39 @@ +using Discord.API.Rest; +using System; +using System.Threading.Tasks; +using Model = Discord.API.User; + +namespace Discord.Rest +{ + internal static class UserHelper + { + public static async Task ModifyAsync(ISelfUser user, BaseDiscordClient client, Action func, + RequestOptions options) + { + var args = new ModifyCurrentUserParams(); + func(args); + return await client.ApiClient.ModifySelfAsync(args, options).ConfigureAwait(false); + } + public static async Task ModifyAsync(IGuildUser user, BaseDiscordClient client, Action func, + RequestOptions options) + { + var args = new ModifyGuildMemberParams(); + func(args); + await client.ApiClient.ModifyGuildMemberAsync(user.GuildId, user.Id, args, options).ConfigureAwait(false); + return args; + } + + public static async Task KickAsync(IGuildUser user, BaseDiscordClient client, + RequestOptions options) + { + await client.ApiClient.RemoveGuildMemberAsync(user.GuildId, user.Id, options).ConfigureAwait(false); + } + + public static async Task CreateDMChannelAsync(IUser user, BaseDiscordClient client, + RequestOptions options) + { + var args = new CreateDMChannelParams(user.Id); + return RestDMChannel.Create(client, await client.ApiClient.CreateDMChannelAsync(args, options).ConfigureAwait(false)); + } + } +} diff --git a/src/Discord.Net.Rest/Utils/TypingNotifier.cs b/src/Discord.Net.Rest/Utils/TypingNotifier.cs new file mode 100644 index 000000000..45b715a76 --- /dev/null +++ b/src/Discord.Net.Rest/Utils/TypingNotifier.cs @@ -0,0 +1,46 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Rest +{ + internal class TypingNotifier : IDisposable + { + private readonly BaseDiscordClient _client; + private readonly CancellationTokenSource _cancelToken; + private readonly IMessageChannel _channel; + private readonly RequestOptions _options; + + public TypingNotifier(BaseDiscordClient discord, IMessageChannel channel, RequestOptions options) + { + _client = discord; + _cancelToken = new CancellationTokenSource(); + _channel = channel; + _options = options; + var _ = Run(); + } + + private async Task Run() + { + try + { + var token = _cancelToken.Token; + while (!_cancelToken.IsCancellationRequested) + { + try + { + await _channel.TriggerTypingAsync(_options).ConfigureAwait(false); + } + catch { } + await Task.Delay(9750, token).ConfigureAwait(false); + } + } + catch (OperationCanceledException) { } + } + + public void Dispose() + { + _cancelToken.Cancel(); + } + } +} diff --git a/src/Discord.Net.Rest/project.json b/src/Discord.Net.Rest/project.json new file mode 100644 index 000000000..8024001b9 --- /dev/null +++ b/src/Discord.Net.Rest/project.json @@ -0,0 +1,32 @@ +{ + "version": "1.0.0-beta2-*", + + "configurations": { + "Release": { + "buildOptions": { + "define": [ "RELEASE" ], + "nowarn": [ "CS1573", "CS1591" ], + "optimize": true, + "warningsAsErrors": true, + "xmlDoc": true + } + } + }, + + "dependencies": { + "Discord.Net.Core": { + "target": "project" + }, + "System.IO.FileSystem": "4.0.1" + }, + + "frameworks": { + "netstandard1.3": { + "imports": [ + "dotnet5.4", + "dnxcore50", + "portable-net45+win8" + ] + } + } +} diff --git a/src/Discord.Net/API/DiscordRpcAPIClient.cs b/src/Discord.Net.Rpc/API/DiscordRpcApiClient.cs similarity index 73% rename from src/Discord.Net/API/DiscordRpcAPIClient.cs rename to src/Discord.Net.Rpc/API/DiscordRpcApiClient.cs index 37a2aa5a6..050783f28 100644 --- a/src/Discord.Net/API/DiscordRpcAPIClient.cs +++ b/src/Discord.Net.Rpc/API/DiscordRpcApiClient.cs @@ -8,6 +8,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Text; @@ -67,13 +68,16 @@ 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; _origin = origin; + FetchCurrentUser = false; + _requestQueue = requestQueue ?? new RequestQueue(); _requests = new ConcurrentDictionary(); @@ -91,7 +95,7 @@ namespace Discord.API using (var reader = new StreamReader(decompressed)) using (var jsonReader = new JsonTextReader(reader)) { - var msg = _serializer.Deserialize(jsonReader); + var msg = _serializer.Deserialize(jsonReader); await _receivedRpcEvent.InvokeAsync(msg.Cmd, msg.Event, msg.Data).ConfigureAwait(false); if (msg.Nonce.IsSpecified && msg.Nonce.Value.HasValue) ProcessMessage(msg); @@ -103,7 +107,7 @@ namespace Discord.API using (var reader = new StringReader(text)) using (var jsonReader = new JsonTextReader(reader)) { - var msg = _serializer.Deserialize(jsonReader); + var msg = _serializer.Deserialize(jsonReader); await _receivedRpcEvent.InvokeAsync(msg.Cmd, msg.Event, msg.Data).ConfigureAwait(false); if (msg.Nonce.IsSpecified && msg.Nonce.Value.HasValue) ProcessMessage(msg); @@ -169,7 +173,7 @@ namespace Discord.API if (!success) throw new Exception("Unable to connect to the RPC server."); - + SetBaseUrl($"https://{uuid}.discordapp.io:{port}/"); ConnectionState = ConnectionState.Connected; } @@ -206,24 +210,20 @@ namespace Discord.API } //Core - public Task SendRpcAsync(string cmd, object payload, GlobalBucket bucket = GlobalBucket.GeneralRpc, - Optional evt = default(Optional), bool ignoreState = false, RequestOptions options = null) - where TResponse : class - => SendRpcAsyncInternal(cmd, payload, BucketGroup.Global, (int)bucket, 0, evt, ignoreState, options); - public Task SendRpcAsync(string cmd, object payload, GuildBucket bucket, ulong guildId, - Optional evt = default(Optional), bool ignoreState = false, RequestOptions options = null) + public async Task SendRpcAsync(string cmd, object payload, Optional evt = default(Optional), RequestOptions options = null) where TResponse : class - => SendRpcAsyncInternal(cmd, payload, BucketGroup.Guild, (int)bucket, guildId, evt, ignoreState, options); - private async Task SendRpcAsyncInternal(string cmd, object payload, BucketGroup group, int bucketId, ulong guildId, - Optional evt, bool ignoreState, RequestOptions options) + { + return await SendRpcAsyncInternal(cmd, payload, evt, options).ConfigureAwait(false); + } + private async Task SendRpcAsyncInternal(string cmd, object payload, Optional evt, RequestOptions options) where TResponse : class { - if (!ignoreState) + if (!options.IgnoreState) CheckState(); byte[] bytes = null; var guid = Guid.NewGuid(); - payload = new API.Rpc.RpcMessage { Cmd = cmd, Event = evt, Args = payload, Nonce = guid }; + payload = new API.Rpc.RpcFrame { Cmd = cmd, Event = evt, Args = payload, Nonce = guid }; if (payload != null) { var json = SerializeJson(payload); @@ -233,7 +233,7 @@ namespace Discord.API var requestTracker = new RpcRequest(options); _requests[guid] = requestTracker; - await _requestQueue.SendAsync(new WebSocketRequest(_webSocketClient, bytes, true, options), group, bucketId, guildId).ConfigureAwait(false); + await _requestQueue.SendAsync(new WebSocketRequest(_webSocketClient, null, bytes, true, options)).ConfigureAwait(false); await _sentRpcMessageEvent.InvokeAsync(cmd).ConfigureAwait(false); return await requestTracker.Promise.Task.ConfigureAwait(false); } @@ -241,75 +241,115 @@ namespace Discord.API //Rpc public async Task SendAuthenticateAsync(RequestOptions options = null) { + options = RequestOptions.CreateOrClone(options); var msg = new AuthenticateParams { AccessToken = _authToken }; - return await SendRpcAsync("AUTHENTICATE", msg, ignoreState: true, options: options).ConfigureAwait(false); + options.IgnoreState = true; + return await SendRpcAsync("AUTHENTICATE", msg, options: options).ConfigureAwait(false); } - public async Task SendAuthorizeAsync(string[] scopes, string rpcToken = null, RequestOptions options = null) + public async Task SendAuthorizeAsync(IReadOnlyCollection scopes, string rpcToken = null, RequestOptions options = null) { + options = RequestOptions.CreateOrClone(options); var msg = new AuthorizeParams { ClientId = _clientId, Scopes = scopes, RpcToken = rpcToken != null ? rpcToken : Optional.Create() }; - if (options == null) - options = new RequestOptions(); if (options.Timeout == null) options.Timeout = 60000; //This requires manual input on the user's end, lets give them more time - return await SendRpcAsync("AUTHORIZE", msg, ignoreState: true, options: options).ConfigureAwait(false); + options.IgnoreState = true; + return await SendRpcAsync("AUTHORIZE", msg, options: options).ConfigureAwait(false); } public async Task SendGetGuildsAsync(RequestOptions options = null) { + options = RequestOptions.CreateOrClone(options); return await SendRpcAsync("GET_GUILDS", null, options: options).ConfigureAwait(false); } - public async Task SendGetGuildAsync(ulong guildId, RequestOptions options = null) + public async Task SendGetGuildAsync(ulong guildId, RequestOptions options = null) { + options = RequestOptions.CreateOrClone(options); var msg = new GetGuildParams { GuildId = guildId }; - return await SendRpcAsync("GET_GUILD", msg, options: options).ConfigureAwait(false); + return await SendRpcAsync("GET_GUILD", msg, options: options).ConfigureAwait(false); } public async Task SendGetChannelsAsync(ulong guildId, RequestOptions options = null) { + options = RequestOptions.CreateOrClone(options); var msg = new GetChannelsParams { GuildId = guildId }; return await SendRpcAsync("GET_CHANNELS", msg, options: options).ConfigureAwait(false); } - public async Task SendGetChannelAsync(ulong channelId, RequestOptions options = null) + public async Task SendGetChannelAsync(ulong channelId, RequestOptions options = null) { + options = RequestOptions.CreateOrClone(options); var msg = new GetChannelParams { ChannelId = channelId }; - return await SendRpcAsync("GET_CHANNEL", msg, options: options).ConfigureAwait(false); + return await SendRpcAsync("GET_CHANNEL", msg, options: options).ConfigureAwait(false); + } + + public async Task SendSelectTextChannelAsync(ulong channelId, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + var msg = new SelectChannelParams + { + ChannelId = channelId + }; + return await SendRpcAsync("SELECT_TEXT_CHANNEL", msg, options: options).ConfigureAwait(false); + } + public async Task SendSelectVoiceChannelAsync(ulong channelId, bool force = false, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + var msg = new SelectChannelParams + { + ChannelId = channelId, + Force = force + }; + return await SendRpcAsync("SELECT_VOICE_CHANNEL", msg, options: options).ConfigureAwait(false); } - public async Task SendSetLocalVolumeAsync(int volume, RequestOptions options = null) + public async Task SendGlobalSubscribeAsync(string evt, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + return await SendRpcAsync("SUBSCRIBE", null, evt: evt, options: options).ConfigureAwait(false); + } + public async Task SendGlobalUnsubscribeAsync(string evt, RequestOptions options = null) { - var msg = new SetLocalVolumeParams + options = RequestOptions.CreateOrClone(options); + return await SendRpcAsync("UNSUBSCRIBE", null, evt: evt, options: options).ConfigureAwait(false); + } + + public async Task SendGuildSubscribeAsync(string evt, ulong guildId, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + var msg = new GuildSubscriptionParams { - Volume = volume + GuildId = guildId }; - return await SendRpcAsync("SET_LOCAL_VOLUME", msg, options: options).ConfigureAwait(false); + return await SendRpcAsync("SUBSCRIBE", msg, evt: evt, options: options).ConfigureAwait(false); } - public async Task SendSelectVoiceChannelAsync(ulong channelId, RequestOptions options = null) + public async Task SendGuildUnsubscribeAsync(string evt, ulong guildId, RequestOptions options = null) { - var msg = new SelectVoiceChannelParams + options = RequestOptions.CreateOrClone(options); + var msg = new GuildSubscriptionParams { - ChannelId = channelId + GuildId = guildId }; - return await SendRpcAsync("SELECT_VOICE_CHANNEL", msg, options: options).ConfigureAwait(false); + return await SendRpcAsync("UNSUBSCRIBE", msg, evt: evt, options: options).ConfigureAwait(false); } public async Task SendChannelSubscribeAsync(string evt, ulong channelId, RequestOptions options = null) { + options = RequestOptions.CreateOrClone(options); var msg = new ChannelSubscriptionParams { ChannelId = channelId @@ -318,6 +358,7 @@ namespace Discord.API } public async Task SendChannelUnsubscribeAsync(string evt, ulong channelId, RequestOptions options = null) { + options = RequestOptions.CreateOrClone(options); var msg = new ChannelSubscriptionParams { ChannelId = channelId @@ -325,24 +366,24 @@ namespace Discord.API return await SendRpcAsync("UNSUBSCRIBE", msg, evt: evt, options: options).ConfigureAwait(false); } - public async Task SendGuildSubscribeAsync(string evt, ulong guildId, RequestOptions options = null) + public async Task GetVoiceSettingsAsync(RequestOptions options = null) { - var msg = new GuildSubscriptionParams - { - GuildId = guildId - }; - return await SendRpcAsync("SUBSCRIBE", msg, evt: evt, options: options).ConfigureAwait(false); + options = RequestOptions.CreateOrClone(options); + return await SendRpcAsync("GET_VOICE_SETTINGS", null, options: options).ConfigureAwait(false); } - public async Task SendGuildUnsubscribeAsync(string evt, ulong guildId, RequestOptions options = null) + public async Task SetVoiceSettingsAsync(API.Rpc.VoiceSettings settings, RequestOptions options = null) { - var msg = new GuildSubscriptionParams - { - GuildId = guildId - }; - return await SendRpcAsync("UNSUBSCRIBE", msg, evt: evt, options: options).ConfigureAwait(false); + options = RequestOptions.CreateOrClone(options); + await SendRpcAsync("SET_VOICE_SETTINGS", settings, options: options).ConfigureAwait(false); + } + public async Task SetUserVoiceSettingsAsync(ulong userId, API.Rpc.UserVoiceSettings settings, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + settings.UserId = userId; + await SendRpcAsync("SET_USER_VOICE_SETTINGS", settings, options: options).ConfigureAwait(false); } - private bool ProcessMessage(API.Rpc.RpcMessage msg) + private bool ProcessMessage(API.Rpc.RpcFrame msg) { RpcRequest requestTracker; if (_requests.TryGetValue(msg.Nonce.Value.Value, out requestTracker)) diff --git a/src/Discord.Net/API/Rpc/AuthenticateParams.cs b/src/Discord.Net.Rpc/API/Rpc/AuthenticateParams.cs similarity index 100% rename from src/Discord.Net/API/Rpc/AuthenticateParams.cs rename to src/Discord.Net.Rpc/API/Rpc/AuthenticateParams.cs diff --git a/src/Discord.Net/API/Rpc/AuthenticateResponse.cs b/src/Discord.Net.Rpc/API/Rpc/AuthenticateResponse.cs similarity index 100% rename from src/Discord.Net/API/Rpc/AuthenticateResponse.cs rename to src/Discord.Net.Rpc/API/Rpc/AuthenticateResponse.cs diff --git a/src/Discord.Net/API/Rpc/AuthorizeParams.cs b/src/Discord.Net.Rpc/API/Rpc/AuthorizeParams.cs similarity index 77% rename from src/Discord.Net/API/Rpc/AuthorizeParams.cs rename to src/Discord.Net.Rpc/API/Rpc/AuthorizeParams.cs index 606e96f9a..367aafd41 100644 --- a/src/Discord.Net/API/Rpc/AuthorizeParams.cs +++ b/src/Discord.Net.Rpc/API/Rpc/AuthorizeParams.cs @@ -1,5 +1,6 @@ #pragma warning disable CS1591 using Newtonsoft.Json; +using System.Collections.Generic; namespace Discord.API.Rpc { @@ -8,7 +9,7 @@ namespace Discord.API.Rpc [JsonProperty("client_id")] public string ClientId { get; set; } [JsonProperty("scopes")] - public string[] Scopes { get; set; } + public IReadOnlyCollection Scopes { get; set; } [JsonProperty("rpc_token")] public Optional RpcToken { get; set; } } diff --git a/src/Discord.Net/API/Rpc/AuthorizeResponse.cs b/src/Discord.Net.Rpc/API/Rpc/AuthorizeResponse.cs similarity index 93% rename from src/Discord.Net/API/Rpc/AuthorizeResponse.cs rename to src/Discord.Net.Rpc/API/Rpc/AuthorizeResponse.cs index 1b674e959..a4f42b6f5 100644 --- a/src/Discord.Net/API/Rpc/AuthorizeResponse.cs +++ b/src/Discord.Net.Rpc/API/Rpc/AuthorizeResponse.cs @@ -1,6 +1,5 @@ #pragma warning disable CS1591 using Newtonsoft.Json; -using System; namespace Discord.API.Rpc { diff --git a/src/Discord.Net.Rpc/API/Rpc/Channel.cs b/src/Discord.Net.Rpc/API/Rpc/Channel.cs new file mode 100644 index 000000000..1b8f3775c --- /dev/null +++ b/src/Discord.Net.Rpc/API/Rpc/Channel.cs @@ -0,0 +1,34 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class Channel + { + //Shared + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("type")] + public ChannelType Type { get; set; } + + //GuildChannel + [JsonProperty("guild_id")] + public Optional GuildId { get; set; } + [JsonProperty("name")] + public Optional Name { get; set; } + [JsonProperty("position")] + public Optional Position { get; set; } + + //IMessageChannel + [JsonProperty("messages")] + public Message[] Messages { get; set; } + + //VoiceChannel + [JsonProperty("bitrate")] + public Optional Bitrate { get; set; } + [JsonProperty("user_limit")] + public Optional UserLimit { get; set; } + [JsonProperty("voice_states")] + public ExtendedVoiceState[] VoiceStates { get; set; } + } +} diff --git a/src/Discord.Net/API/Rpc/ChannelSubscriptionParams.cs b/src/Discord.Net.Rpc/API/Rpc/ChannelSubscriptionParams.cs similarity index 100% rename from src/Discord.Net/API/Rpc/ChannelSubscriptionParams.cs rename to src/Discord.Net.Rpc/API/Rpc/ChannelSubscriptionParams.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/ChannelSummary.cs b/src/Discord.Net.Rpc/API/Rpc/ChannelSummary.cs new file mode 100644 index 000000000..34acd049b --- /dev/null +++ b/src/Discord.Net.Rpc/API/Rpc/ChannelSummary.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class ChannelSummary + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("type")] + public ChannelType Type { get; set; } + } +} diff --git a/src/Discord.Net/API/Rpc/ErrorEvent.cs b/src/Discord.Net.Rpc/API/Rpc/ErrorEvent.cs similarity index 100% rename from src/Discord.Net/API/Rpc/ErrorEvent.cs rename to src/Discord.Net.Rpc/API/Rpc/ErrorEvent.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/ExtendedVoiceState.cs b/src/Discord.Net.Rpc/API/Rpc/ExtendedVoiceState.cs new file mode 100644 index 000000000..032914f0f --- /dev/null +++ b/src/Discord.Net.Rpc/API/Rpc/ExtendedVoiceState.cs @@ -0,0 +1,21 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class ExtendedVoiceState + { + [JsonProperty("user")] + public User User { get; set; } + [JsonProperty("voice_state")] + public Optional VoiceState { get; set; } + [JsonProperty("nick")] + public Optional Nickname { get; set; } + [JsonProperty("volume")] + public Optional Volume { get; set; } + [JsonProperty("mute")] + public Optional Mute { get; set; } + [JsonProperty("pan")] + public Optional Pan { get; set; } + } +} diff --git a/src/Discord.Net/API/Rpc/GetChannelParams.cs b/src/Discord.Net.Rpc/API/Rpc/GetChannelParams.cs similarity index 100% rename from src/Discord.Net/API/Rpc/GetChannelParams.cs rename to src/Discord.Net.Rpc/API/Rpc/GetChannelParams.cs diff --git a/src/Discord.Net/API/Rpc/GetChannelsParams.cs b/src/Discord.Net.Rpc/API/Rpc/GetChannelsParams.cs similarity index 100% rename from src/Discord.Net/API/Rpc/GetChannelsParams.cs rename to src/Discord.Net.Rpc/API/Rpc/GetChannelsParams.cs diff --git a/src/Discord.Net/API/Rpc/GetChannelsResponse.cs b/src/Discord.Net.Rpc/API/Rpc/GetChannelsResponse.cs similarity index 61% rename from src/Discord.Net/API/Rpc/GetChannelsResponse.cs rename to src/Discord.Net.Rpc/API/Rpc/GetChannelsResponse.cs index 5c428d7ba..e105341a1 100644 --- a/src/Discord.Net/API/Rpc/GetChannelsResponse.cs +++ b/src/Discord.Net.Rpc/API/Rpc/GetChannelsResponse.cs @@ -1,11 +1,12 @@ #pragma warning disable CS1591 using Newtonsoft.Json; +using System.Collections.Generic; namespace Discord.API.Rpc { public class GetChannelsResponse { [JsonProperty("channels")] - public RpcChannel[] Channels { get; set; } + public IReadOnlyCollection Channels { get; set; } } } diff --git a/src/Discord.Net/API/Rpc/GetGuildParams.cs b/src/Discord.Net.Rpc/API/Rpc/GetGuildParams.cs similarity index 100% rename from src/Discord.Net/API/Rpc/GetGuildParams.cs rename to src/Discord.Net.Rpc/API/Rpc/GetGuildParams.cs diff --git a/src/Discord.Net/API/Rpc/GetGuildsParams.cs b/src/Discord.Net.Rpc/API/Rpc/GetGuildsParams.cs similarity index 99% rename from src/Discord.Net/API/Rpc/GetGuildsParams.cs rename to src/Discord.Net.Rpc/API/Rpc/GetGuildsParams.cs index 9e45db44a..a1ff5f210 100644 --- a/src/Discord.Net/API/Rpc/GetGuildsParams.cs +++ b/src/Discord.Net.Rpc/API/Rpc/GetGuildsParams.cs @@ -1,4 +1,5 @@ #pragma warning disable CS1591 + namespace Discord.API.Rpc { public class GetGuildsParams diff --git a/src/Discord.Net/API/Rpc/GetGuildsResponse.cs b/src/Discord.Net.Rpc/API/Rpc/GetGuildsResponse.cs similarity index 76% rename from src/Discord.Net/API/Rpc/GetGuildsResponse.cs rename to src/Discord.Net.Rpc/API/Rpc/GetGuildsResponse.cs index df7739709..e69bedeae 100644 --- a/src/Discord.Net/API/Rpc/GetGuildsResponse.cs +++ b/src/Discord.Net.Rpc/API/Rpc/GetGuildsResponse.cs @@ -6,6 +6,6 @@ namespace Discord.API.Rpc public class GetGuildsResponse { [JsonProperty("guilds")] - public RpcUserGuild[] Guilds { get; set; } + public GuildSummary[] Guilds { get; set; } } } diff --git a/src/Discord.Net.Rpc/API/Rpc/Guild.cs b/src/Discord.Net.Rpc/API/Rpc/Guild.cs new file mode 100644 index 000000000..1d6bf3678 --- /dev/null +++ b/src/Discord.Net.Rpc/API/Rpc/Guild.cs @@ -0,0 +1,18 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Discord.API.Rpc +{ + public class Guild + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("icon_url")] + public string IconUrl { get; set; } + [JsonProperty("members")] + public IEnumerable Members { get; set; } + } +} diff --git a/src/Discord.Net.Rpc/API/Rpc/GuildMember.cs b/src/Discord.Net.Rpc/API/Rpc/GuildMember.cs new file mode 100644 index 000000000..af74dd919 --- /dev/null +++ b/src/Discord.Net.Rpc/API/Rpc/GuildMember.cs @@ -0,0 +1,15 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class GuildMember + { + [JsonProperty("user")] + public User User { get; set; } + [JsonProperty("status")] + public UserStatus Status { get; set; } + /*[JsonProperty("activity")] + public object Activity { get; set; }*/ + } +} diff --git a/src/Discord.Net/API/Rpc/GuildStatusEvent.cs b/src/Discord.Net.Rpc/API/Rpc/GuildStatusEvent.cs similarity index 100% rename from src/Discord.Net/API/Rpc/GuildStatusEvent.cs rename to src/Discord.Net.Rpc/API/Rpc/GuildStatusEvent.cs diff --git a/src/Discord.Net/API/Rpc/GuildSubscriptionParams.cs b/src/Discord.Net.Rpc/API/Rpc/GuildSubscriptionParams.cs similarity index 100% rename from src/Discord.Net/API/Rpc/GuildSubscriptionParams.cs rename to src/Discord.Net.Rpc/API/Rpc/GuildSubscriptionParams.cs diff --git a/src/Discord.Net/API/Rpc/RpcUserGuild.cs b/src/Discord.Net.Rpc/API/Rpc/GuildSummary.cs similarity index 67% rename from src/Discord.Net/API/Rpc/RpcUserGuild.cs rename to src/Discord.Net.Rpc/API/Rpc/GuildSummary.cs index 4df8e1f7e..c36da5267 100644 --- a/src/Discord.Net/API/Rpc/RpcUserGuild.cs +++ b/src/Discord.Net.Rpc/API/Rpc/GuildSummary.cs @@ -1,9 +1,8 @@ -#pragma warning disable CS1591 -using Newtonsoft.Json; +using Newtonsoft.Json; namespace Discord.API.Rpc { - public class RpcUserGuild + public class GuildSummary { [JsonProperty("id")] public ulong Id { get; set; } diff --git a/src/Discord.Net.Rpc/API/Rpc/Message.cs b/src/Discord.Net.Rpc/API/Rpc/Message.cs new file mode 100644 index 000000000..a72fba123 --- /dev/null +++ b/src/Discord.Net.Rpc/API/Rpc/Message.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class Message : Discord.API.Message + { + [JsonProperty("blocked")] + public Optional IsBlocked { get; } + [JsonProperty("content_parsed")] + public Optional ContentParsed { get; } + [JsonProperty("author_color")] + public Optional AuthorColor { get; } //#Hex + + [JsonProperty("mentions")] + public new Optional UserMentions { get; set; } + } +} diff --git a/src/Discord.Net/API/Rpc/MessageEvent.cs b/src/Discord.Net.Rpc/API/Rpc/MessageEvent.cs similarity index 100% rename from src/Discord.Net/API/Rpc/MessageEvent.cs rename to src/Discord.Net.Rpc/API/Rpc/MessageEvent.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/Pan.cs b/src/Discord.Net.Rpc/API/Rpc/Pan.cs new file mode 100644 index 000000000..e2a97c369 --- /dev/null +++ b/src/Discord.Net.Rpc/API/Rpc/Pan.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class Pan + { + [JsonProperty("left")] + public float Left { get; set; } + [JsonProperty("right")] + public float Right { get; set; } + } +} diff --git a/src/Discord.Net/API/Rpc/ReadyEvent.cs b/src/Discord.Net.Rpc/API/Rpc/ReadyEvent.cs similarity index 100% rename from src/Discord.Net/API/Rpc/ReadyEvent.cs rename to src/Discord.Net.Rpc/API/Rpc/ReadyEvent.cs diff --git a/src/Discord.Net/API/Rpc/RpcConfig.cs b/src/Discord.Net.Rpc/API/Rpc/RpcConfig.cs similarity index 100% rename from src/Discord.Net/API/Rpc/RpcConfig.cs rename to src/Discord.Net.Rpc/API/Rpc/RpcConfig.cs diff --git a/src/Discord.Net/API/Rpc/SelectVoiceChannelParams.cs b/src/Discord.Net.Rpc/API/Rpc/SelectChannelParams.cs similarity index 60% rename from src/Discord.Net/API/Rpc/SelectVoiceChannelParams.cs rename to src/Discord.Net.Rpc/API/Rpc/SelectChannelParams.cs index e7f980c32..52c9b00e8 100644 --- a/src/Discord.Net/API/Rpc/SelectVoiceChannelParams.cs +++ b/src/Discord.Net.Rpc/API/Rpc/SelectChannelParams.cs @@ -3,9 +3,11 @@ using Newtonsoft.Json; namespace Discord.API.Rpc { - public class SelectVoiceChannelParams + public class SelectChannelParams { [JsonProperty("channel_id")] public ulong? ChannelId { get; set; } + [JsonProperty("force")] + public Optional Force { get; set; } } } diff --git a/src/Discord.Net/API/Rpc/SetLocalVolumeParams.cs b/src/Discord.Net.Rpc/API/Rpc/SetLocalVolumeParams.cs similarity index 100% rename from src/Discord.Net/API/Rpc/SetLocalVolumeParams.cs rename to src/Discord.Net.Rpc/API/Rpc/SetLocalVolumeParams.cs diff --git a/src/Discord.Net/API/Rpc/SetLocalVolumeResponse.cs b/src/Discord.Net.Rpc/API/Rpc/SetLocalVolumeResponse.cs similarity index 100% rename from src/Discord.Net/API/Rpc/SetLocalVolumeResponse.cs rename to src/Discord.Net.Rpc/API/Rpc/SetLocalVolumeResponse.cs diff --git a/src/Discord.Net/API/Rpc/SpeakingEvent.cs b/src/Discord.Net.Rpc/API/Rpc/SpeakingEvent.cs similarity index 51% rename from src/Discord.Net/API/Rpc/SpeakingEvent.cs rename to src/Discord.Net.Rpc/API/Rpc/SpeakingEvent.cs index 828556818..4d8804d2f 100644 --- a/src/Discord.Net/API/Rpc/SpeakingEvent.cs +++ b/src/Discord.Net.Rpc/API/Rpc/SpeakingEvent.cs @@ -1,7 +1,11 @@ #pragma warning disable CS1591 +using Newtonsoft.Json; + namespace Discord.API.Rpc { public class SpeakingEvent { + [JsonProperty("user_id")] + public ulong UserId { get; set; } } } diff --git a/src/Discord.Net/API/Rpc/SubscriptionResponse.cs b/src/Discord.Net.Rpc/API/Rpc/SubscriptionResponse.cs similarity index 100% rename from src/Discord.Net/API/Rpc/SubscriptionResponse.cs rename to src/Discord.Net.Rpc/API/Rpc/SubscriptionResponse.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/UserVoiceSettings.cs b/src/Discord.Net.Rpc/API/Rpc/UserVoiceSettings.cs new file mode 100644 index 000000000..9c876a66f --- /dev/null +++ b/src/Discord.Net.Rpc/API/Rpc/UserVoiceSettings.cs @@ -0,0 +1,18 @@ +#pragma warning disable CS1591 + +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class UserVoiceSettings + { + [JsonProperty("userId")] + internal ulong UserId { get; set; } + [JsonProperty("pan")] + public Optional Pan { get; set; } + [JsonProperty("volume")] + public Optional Volume { get; set; } + [JsonProperty("mute")] + public Optional Mute { get; set; } + } +} diff --git a/src/Discord.Net.Rpc/API/Rpc/VoiceDevice.cs b/src/Discord.Net.Rpc/API/Rpc/VoiceDevice.cs new file mode 100644 index 000000000..4dc99d4cd --- /dev/null +++ b/src/Discord.Net.Rpc/API/Rpc/VoiceDevice.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class VoiceDevice + { + [JsonProperty("id")] + public string Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + } +} diff --git a/src/Discord.Net.Rpc/API/Rpc/VoiceDeviceSettings.cs b/src/Discord.Net.Rpc/API/Rpc/VoiceDeviceSettings.cs new file mode 100644 index 000000000..38473c803 --- /dev/null +++ b/src/Discord.Net.Rpc/API/Rpc/VoiceDeviceSettings.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class VoiceDeviceSettings + { + [JsonProperty("device_id")] + public Optional DeviceId { get; set; } + [JsonProperty("volume")] + public Optional Volume { get; set; } + [JsonProperty("available_devices")] + public Optional AvailableDevices { get; set; } + } +} diff --git a/src/Discord.Net.Rpc/API/Rpc/VoiceMode.cs b/src/Discord.Net.Rpc/API/Rpc/VoiceMode.cs new file mode 100644 index 000000000..a502cc960 --- /dev/null +++ b/src/Discord.Net.Rpc/API/Rpc/VoiceMode.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class VoiceMode + { + [JsonProperty("type")] + public Optional Type { get; set; } + [JsonProperty("auto_threshold")] + public Optional AutoThreshold { get; set; } + [JsonProperty("threshold")] + public Optional Threshold { get; set; } + [JsonProperty("shortcut")] + public Optional Shortcut { get; set; } + [JsonProperty("delay")] + public Optional Delay { get; set; } + } +} diff --git a/src/Discord.Net.Rpc/API/Rpc/VoiceSettings.cs b/src/Discord.Net.Rpc/API/Rpc/VoiceSettings.cs new file mode 100644 index 000000000..c3268a719 --- /dev/null +++ b/src/Discord.Net.Rpc/API/Rpc/VoiceSettings.cs @@ -0,0 +1,26 @@ +#pragma warning disable CS1591 + +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class VoiceSettings + { + [JsonProperty("input")] + public VoiceDeviceSettings Input { get; set; } + [JsonProperty("output")] + public VoiceDeviceSettings Output { get; set; } + [JsonProperty("mode")] + public VoiceMode Mode { get; set; } + [JsonProperty("automatic_gain_control")] + public Optional AutomaticGainControl { get; set; } + [JsonProperty("echo_cancellation")] + public Optional EchoCancellation { get; set; } + [JsonProperty("noise_suppression")] + public Optional NoiseSuppression { get; set; } + [JsonProperty("qos")] + public Optional QualityOfService { get; set; } + [JsonProperty("silence_warning")] + public Optional SilenceWarning { get; set; } + } +} diff --git a/src/Discord.Net.Rpc/API/Rpc/VoiceShortcut.cs b/src/Discord.Net.Rpc/API/Rpc/VoiceShortcut.cs new file mode 100644 index 000000000..5b0939d79 --- /dev/null +++ b/src/Discord.Net.Rpc/API/Rpc/VoiceShortcut.cs @@ -0,0 +1,15 @@ +using Discord.Rpc; +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class VoiceShortcut + { + [JsonProperty("type")] + public Optional Type { get; set; } + [JsonProperty("code")] + public Optional Code { get; set; } + [JsonProperty("name")] + public Optional Name { get; set; } + } +} diff --git a/src/Discord.Net/API/Rpc/RpcMessage.cs b/src/Discord.Net.Rpc/API/RpcFrame.cs similarity index 94% rename from src/Discord.Net/API/Rpc/RpcMessage.cs rename to src/Discord.Net.Rpc/API/RpcFrame.cs index 41616222c..cac150e3b 100644 --- a/src/Discord.Net/API/Rpc/RpcMessage.cs +++ b/src/Discord.Net.Rpc/API/RpcFrame.cs @@ -4,7 +4,7 @@ using System; namespace Discord.API.Rpc { - public class RpcMessage + public class RpcFrame { [JsonProperty("cmd")] public string Cmd { get; set; } diff --git a/src/Discord.Net.Rpc/AssemblyInfo.cs b/src/Discord.Net.Rpc/AssemblyInfo.cs new file mode 100644 index 000000000..c6b5997b4 --- /dev/null +++ b/src/Discord.Net.Rpc/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Discord.Net.Tests")] \ No newline at end of file diff --git a/src/Discord.Net.Rpc/Discord.Net.Rpc.xproj b/src/Discord.Net.Rpc/Discord.Net.Rpc.xproj new file mode 100644 index 000000000..c5d036842 --- /dev/null +++ b/src/Discord.Net.Rpc/Discord.Net.Rpc.xproj @@ -0,0 +1,19 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 5688a353-121e-40a1-8bfa-b17b91fb48fb + Discord.Rpc + .\obj + .\bin\ + v4.6.1 + + + 2.0 + + + \ No newline at end of file diff --git a/src/Discord.Net.Rpc/DiscordRpcClient.Events.cs b/src/Discord.Net.Rpc/DiscordRpcClient.Events.cs new file mode 100644 index 000000000..2a9ae21bf --- /dev/null +++ b/src/Discord.Net.Rpc/DiscordRpcClient.Events.cs @@ -0,0 +1,112 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Rpc +{ + public partial class DiscordRpcClient + { + //General + public event Func Connected + { + add { _connectedEvent.Add(value); } + remove { _connectedEvent.Remove(value); } + } + private readonly AsyncEvent> _connectedEvent = new AsyncEvent>(); + public event Func Disconnected + { + add { _disconnectedEvent.Add(value); } + remove { _disconnectedEvent.Remove(value); } + } + private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); + public event Func Ready + { + add { _readyEvent.Add(value); } + remove { _readyEvent.Remove(value); } + } + private readonly AsyncEvent> _readyEvent = new AsyncEvent>(); + + //Channel + public event Func ChannelCreated + { + add { _channelCreatedEvent.Add(value); } + remove { _channelCreatedEvent.Remove(value); } + } + private readonly AsyncEvent> _channelCreatedEvent = new AsyncEvent>(); + + //Guild + public event Func GuildCreated + { + add { _guildCreatedEvent.Add(value); } + remove { _guildCreatedEvent.Remove(value); } + } + private readonly AsyncEvent> _guildCreatedEvent = new AsyncEvent>(); + public event Func GuildStatusUpdated + { + add { _guildStatusUpdatedEvent.Add(value); } + remove { _guildStatusUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _guildStatusUpdatedEvent = new AsyncEvent>(); + + //Voice + public event Func VoiceStateCreated + { + add { _voiceStateCreatedEvent.Add(value); } + remove { _voiceStateCreatedEvent.Remove(value); } + } + private readonly AsyncEvent> _voiceStateCreatedEvent = new AsyncEvent>(); + + public event Func VoiceStateUpdated + { + add { _voiceStateUpdatedEvent.Add(value); } + remove { _voiceStateUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _voiceStateUpdatedEvent = new AsyncEvent>(); + + public event Func VoiceStateDeleted + { + add { _voiceStateDeletedEvent.Add(value); } + remove { _voiceStateDeletedEvent.Remove(value); } + } + private readonly AsyncEvent> _voiceStateDeletedEvent = new AsyncEvent>(); + + public event Func SpeakingStarted + { + add { _speakingStartedEvent.Add(value); } + remove { _speakingStartedEvent.Remove(value); } + } + private readonly AsyncEvent> _speakingStartedEvent = new AsyncEvent>(); + public event Func SpeakingStopped + { + add { _speakingStoppedEvent.Add(value); } + remove { _speakingStoppedEvent.Remove(value); } + } + private readonly AsyncEvent> _speakingStoppedEvent = new AsyncEvent>(); + + public event Func VoiceSettingsUpdated + { + add { _voiceSettingsUpdated.Add(value); } + remove { _voiceSettingsUpdated.Remove(value); } + } + private readonly AsyncEvent> _voiceSettingsUpdated = new AsyncEvent>(); + + //Messages + public event Func MessageReceived + { + add { _messageReceivedEvent.Add(value); } + remove { _messageReceivedEvent.Remove(value); } + } + private readonly AsyncEvent> _messageReceivedEvent = new AsyncEvent>(); + public event Func MessageUpdated + { + add { _messageUpdatedEvent.Add(value); } + remove { _messageUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _messageUpdatedEvent = new AsyncEvent>(); + public event Func MessageDeleted + { + add { _messageDeletedEvent.Add(value); } + remove { _messageDeletedEvent.Remove(value); } + } + private readonly AsyncEvent> _messageDeletedEvent = new AsyncEvent>(); + } +} diff --git a/src/Discord.Net/Rpc/DiscordRpcClient.cs b/src/Discord.Net.Rpc/DiscordRpcClient.cs similarity index 53% rename from src/Discord.Net/Rpc/DiscordRpcClient.cs rename to src/Discord.Net.Rpc/DiscordRpcClient.cs index f916f42bf..52fe6172f 100644 --- a/src/Discord.Net/Rpc/DiscordRpcClient.cs +++ b/src/Discord.Net.Rpc/DiscordRpcClient.cs @@ -6,14 +6,17 @@ using Discord.Rest; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; using System.Threading; using System.Threading.Tasks; namespace Discord.Rpc { - public partial class DiscordRpcClient : DiscordRestClient + public partial class DiscordRpcClient : BaseDiscordClient { - private readonly ILogger _rpcLogger; + private readonly Logger _rpcLogger; private readonly JsonSerializer _serializer; private TaskCompletionSource _connectTask; @@ -22,17 +25,22 @@ namespace Discord.Rpc private bool _canReconnect; public ConnectionState ConnectionState { get; private set; } + public IReadOnlyCollection Scopes { get; private set; } + public DateTimeOffset TokenExpiresAt { get; private set; } //From DiscordRpcConfig internal int ConnectionTimeout { get; private set; } public new API.DiscordRpcApiClient ApiClient => base.ApiClient as API.DiscordRpcApiClient; + public new RestSelfUser CurrentUser { get { return base.CurrentUser as RestSelfUser; } private set { base.CurrentUser = value; } } + public RestApplication CurrentApplication { get; private set; } /// Creates a new RPC discord client. - public DiscordRpcClient(string clientId, string origin) : this(new DiscordRpcConfig(clientId, origin)) { } + public DiscordRpcClient(string clientId, string origin) + : this(clientId, origin, new DiscordRpcConfig()) { } /// Creates a new RPC discord client. - public DiscordRpcClient(DiscordRpcConfig config) - : base(config, CreateApiClient(config)) + public DiscordRpcClient(string clientId, string origin, DiscordRpcConfig config) + : base(config, CreateApiClient(clientId, origin, config)) { ConnectionTimeout = config.ConnectionTimeout; _rpcLogger = LogManager.CreateLogger("RPC"); @@ -57,19 +65,9 @@ namespace Discord.Rpc await _rpcLogger.WarningAsync($"Connection Closed").ConfigureAwait(false); }; } - 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 - } + private static API.DiscordRpcApiClient CreateApiClient(string clientId, string origin, DiscordRpcConfig config) + => new API.DiscordRpcApiClient(clientId, DiscordRestConfig.UserAgent, origin, config.RestClientProvider, config.WebSocketProvider, requestQueue: new RequestQueue()); /// public Task ConnectAsync() => ConnectAsync(false); @@ -105,7 +103,7 @@ namespace Discord.Rpc //Abort connection on timeout var _ = Task.Run(async () => { - await Task.Delay(ConnectionTimeout); + await Task.Delay(ConnectionTimeout).ConfigureAwait(false); connectTask.TrySetException(new TimeoutException()); }); @@ -223,39 +221,124 @@ namespace Discord.Rpc } } - public async Task AuthorizeAsync(string[] scopes, string rpcToken = null) + public async Task AuthorizeAsync(string[] scopes, string rpcToken = null, RequestOptions options = null) { await ConnectAsync(true).ConfigureAwait(false); - var result = await ApiClient.SendAuthorizeAsync(scopes, rpcToken).ConfigureAwait(false); + var result = await ApiClient.SendAuthorizeAsync(scopes, rpcToken, options).ConfigureAwait(false); await DisconnectAsync().ConfigureAwait(false); return result.Code; } - public async Task SubscribeGuild(ulong guildId, params RpcChannelEvent[] events) + public async Task SubscribeGlobal(RpcGlobalEvent evnt, RequestOptions options = null) + { + await ApiClient.SendGlobalSubscribeAsync(GetEventName(evnt), options).ConfigureAwait(false); + } + public async Task UnsubscribeGlobal(RpcGlobalEvent evnt, RequestOptions options = null) + { + await ApiClient.SendGlobalUnsubscribeAsync(GetEventName(evnt), options).ConfigureAwait(false); + } + public async Task SubscribeGuild(ulong guildId, RpcChannelEvent evnt, RequestOptions options = null) + { + await ApiClient.SendGuildSubscribeAsync(GetEventName(evnt), guildId, options).ConfigureAwait(false); + } + public async Task UnsubscribeGuild(ulong guildId, RpcChannelEvent evnt, RequestOptions options = null) + { + await ApiClient.SendGuildUnsubscribeAsync(GetEventName(evnt), guildId, options).ConfigureAwait(false); + } + public async Task SubscribeChannel(ulong channelId, RpcChannelEvent evnt, RequestOptions options = null) + { + await ApiClient.SendChannelSubscribeAsync(GetEventName(evnt), channelId).ConfigureAwait(false); + } + public async Task UnsubscribeChannel(ulong channelId, RpcChannelEvent evnt, RequestOptions options = null) + { + await ApiClient.SendChannelUnsubscribeAsync(GetEventName(evnt), channelId).ConfigureAwait(false); + } + + public async Task GetRpcGuildAsync(ulong id, RequestOptions options = null) + { + var model = await ApiClient.SendGetGuildAsync(id, options).ConfigureAwait(false); + return RpcGuild.Create(this, model); + } + public async Task> GetRpcGuildsAsync(RequestOptions options = null) + { + var models = await ApiClient.SendGetGuildsAsync(options).ConfigureAwait(false); + return models.Guilds.Select(x => RpcGuildSummary.Create(x)).ToImmutableArray(); + } + public async Task GetRpcChannelAsync(ulong id, RequestOptions options = null) + { + var model = await ApiClient.SendGetChannelAsync(id, options).ConfigureAwait(false); + return RpcChannel.Create(this, model); + } + public async Task> GetRpcChannelsAsync(ulong guildId, RequestOptions options = null) + { + var models = await ApiClient.SendGetChannelsAsync(guildId, options).ConfigureAwait(false); + return models.Channels.Select(x => RpcChannelSummary.Create(x)).ToImmutableArray(); + } + + public async Task SelectTextChannelAsync(IChannel channel, RequestOptions options = null) + { + var model = await ApiClient.SendSelectTextChannelAsync(channel.Id, options).ConfigureAwait(false); + return RpcChannel.Create(this, model) as IMessageChannel; + } + public async Task SelectTextChannelAsync(RpcChannelSummary channel, RequestOptions options = null) + { + var model = await ApiClient.SendSelectTextChannelAsync(channel.Id, options).ConfigureAwait(false); + return RpcChannel.Create(this, model) as IMessageChannel; + } + public async Task SelectTextChannelAsync(ulong channelId, RequestOptions options = null) + { + var model = await ApiClient.SendSelectTextChannelAsync(channelId, options).ConfigureAwait(false); + return RpcChannel.Create(this, model) as IMessageChannel; + } + + public async Task SelectVoiceChannelAsync(IChannel channel, bool force = false, RequestOptions options = null) + { + var model = await ApiClient.SendSelectVoiceChannelAsync(channel.Id, force, options).ConfigureAwait(false); + return RpcChannel.Create(this, model) as IRpcAudioChannel; + } + public async Task SelectVoiceChannelAsync(RpcChannelSummary channel, bool force = false, RequestOptions options = null) { - Preconditions.AtLeast(events?.Length ?? 0, 1, nameof(events)); - for (int i = 0; i < events.Length; i++) - await ApiClient.SendGuildSubscribeAsync(GetEventName(events[i]), guildId); + var model = await ApiClient.SendSelectVoiceChannelAsync(channel.Id, force, options).ConfigureAwait(false); + return RpcChannel.Create(this, model) as IRpcAudioChannel; } - public async Task UnsubscribeGuild(ulong guildId, params RpcChannelEvent[] events) + public async Task SelectVoiceChannelAsync(ulong channelId, bool force = false, RequestOptions options = null) { - Preconditions.AtLeast(events?.Length ?? 0, 1, nameof(events)); - for (int i = 0; i < events.Length; i++) - await ApiClient.SendGuildUnsubscribeAsync(GetEventName(events[i]), guildId); + var model = await ApiClient.SendSelectVoiceChannelAsync(channelId, force, options).ConfigureAwait(false); + return RpcChannel.Create(this, model) as IRpcAudioChannel; } - public async Task SubscribeChannel(ulong channelId, params RpcChannelEvent[] events) + + public async Task GetVoiceSettingsAsync(RequestOptions options = null) + { + var model = await ApiClient.GetVoiceSettingsAsync(options).ConfigureAwait(false); + return VoiceSettings.Create(model); + } + public async Task SetVoiceSettingsAsync(Action func, RequestOptions options = null) { - Preconditions.AtLeast(events?.Length ?? 0, 1, nameof(events)); - for (int i = 0; i < events.Length; i++) - await ApiClient.SendChannelSubscribeAsync(GetEventName(events[i]), channelId); + var settings = new API.Rpc.VoiceSettings(); + settings.Input = new VoiceDeviceSettings(); + settings.Output = new VoiceDeviceSettings(); + settings.Mode = new VoiceMode(); + func(settings); + await ApiClient.SetVoiceSettingsAsync(settings, options).ConfigureAwait(false); } - public async Task UnsubscribeChannel(ulong channelId, params RpcChannelEvent[] events) + public async Task SetUserVoiceSettingsAsync(ulong userId, Action func, RequestOptions options = null) { - Preconditions.AtLeast(events?.Length ?? 0, 1, nameof(events)); - for (int i = 0; i < events.Length; i++) - await ApiClient.SendChannelUnsubscribeAsync(GetEventName(events[i]), channelId); + var settings = new API.Rpc.UserVoiceSettings(); + func(settings); + await ApiClient.SetUserVoiceSettingsAsync(userId, settings, options).ConfigureAwait(false); } + private static string GetEventName(RpcGlobalEvent rpcEvent) + { + switch (rpcEvent) + { + case RpcGlobalEvent.ChannelCreated: return "CHANNEL_CREATE"; + case RpcGlobalEvent.GuildCreated: return "GUILD_CREATE"; + case RpcGlobalEvent.VoiceSettingsUpdated: return "VOICE_SETTINGS_UPDATE"; + default: + throw new InvalidOperationException($"Unknown RPC Global Event: {rpcEvent}"); + } + } private static string GetEventName(RpcGuildEvent rpcEvent) { switch (rpcEvent) @@ -269,14 +352,14 @@ namespace Discord.Rpc { switch (rpcEvent) { - case RpcChannelEvent.VoiceStateCreate: return "VOICE_STATE_CREATE"; - case RpcChannelEvent.VoiceStateUpdate: return "VOICE_STATE_UPDATE"; - case RpcChannelEvent.VoiceStateDelete: return "VOICE_STATE_DELETE"; - case RpcChannelEvent.SpeakingStart: return "SPEAKING_START"; - case RpcChannelEvent.SpeakingStop: return "SPEAKING_STOP"; case RpcChannelEvent.MessageCreate: return "MESSAGE_CREATE"; case RpcChannelEvent.MessageUpdate: return "MESSAGE_UPDATE"; case RpcChannelEvent.MessageDelete: return "MESSAGE_DELETE"; + case RpcChannelEvent.SpeakingStart: return "SPEAKING_START"; + case RpcChannelEvent.SpeakingStop: return "SPEAKING_STOP"; + case RpcChannelEvent.VoiceStateCreate: return "VOICE_STATE_CREATE"; + case RpcChannelEvent.VoiceStateUpdate: return "VOICE_STATE_UPDATE"; + case RpcChannelEvent.VoiceStateDelete: return "VOICE_STATE_DELETE"; default: throw new InvalidOperationException($"Unknown RPC Channel Event: {rpcEvent}"); } @@ -296,38 +379,70 @@ namespace Discord.Rpc { await _rpcLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false); var data = (payload.Value as JToken).ToObject(_serializer); - var cancelToken = _cancelToken; - var _ = Task.Run(async () => + RequestOptions options = new RequestOptions + { + //CancellationToken = _cancelToken //TODO: Implement + }; + + if (ApiClient.LoginState == LoginState.LoggedIn) { - try + var _ = Task.Run(async () => { - RequestOptions options = new RequestOptions + try { - //CancellationToken = cancelToken //TODO: Implement - }; + var response = await ApiClient.SendAuthenticateAsync(options).ConfigureAwait(false); + CurrentUser = RestSelfUser.Create(this, response.User); + CurrentApplication = RestApplication.Create(this, response.Application); + Scopes = response.Scopes; + TokenExpiresAt = response.Expires; + + var __ = _connectTask.TrySetResultAsync(true); //Signal the .Connect() call to complete + await _rpcLogger.InfoAsync("Ready").ConfigureAwait(false); + } + catch (Exception ex) + { + await _rpcLogger.ErrorAsync($"Error handling {cmd}{(evnt.IsSpecified ? $" ({evnt})" : "")}", ex).ConfigureAwait(false); + return; + } + }); + } + else + { + var _ = _connectTask.TrySetResultAsync(true); //Signal the .Connect() call to complete + await _rpcLogger.InfoAsync("Ready").ConfigureAwait(false); + } + } + break; - if (LoginState != LoginState.LoggedOut) - await ApiClient.SendAuthenticateAsync(options).ConfigureAwait(false); //Has bearer + //Channels + case "CHANNEL_CREATE": + { + await _rpcLogger.DebugAsync("Received Dispatch (CHANNEL_CREATE)").ConfigureAwait(false); + var data = (payload.Value as JToken).ToObject(_serializer); + var channel = RpcChannelSummary.Create(data); - var __ = _connectTask.TrySetResultAsync(true); //Signal the .Connect() call to complete - await _rpcLogger.InfoAsync("Ready").ConfigureAwait(false); - } - catch (Exception ex) - { - await _rpcLogger.ErrorAsync($"Error handling {cmd}{(evnt.IsSpecified ? $" ({evnt})" : "")}", ex).ConfigureAwait(false); - return; - } - }); + await _channelCreatedEvent.InvokeAsync(channel).ConfigureAwait(false); } break; //Guilds + case "GUILD_CREATE": + { + await _rpcLogger.DebugAsync("Received Dispatch (GUILD_CREATE)").ConfigureAwait(false); + var data = (payload.Value as JToken).ToObject(_serializer); + var guild = RpcGuildSummary.Create(data); + + await _guildCreatedEvent.InvokeAsync(guild).ConfigureAwait(false); + } + break; case "GUILD_STATUS": { await _rpcLogger.DebugAsync("Received Dispatch (GUILD_STATUS)").ConfigureAwait(false); + var data = (payload.Value as JToken).ToObject(_serializer); + var guildStatus = RpcGuildStatus.Create(data); - await _guildUpdatedEvent.InvokeAsync().ConfigureAwait(false); + await _guildStatusUpdatedEvent.InvokeAsync(guildStatus).ConfigureAwait(false); } break; @@ -335,36 +450,54 @@ namespace Discord.Rpc case "VOICE_STATE_CREATE": { await _rpcLogger.DebugAsync("Received Dispatch (VOICE_STATE_CREATE)").ConfigureAwait(false); + var data = (payload.Value as JToken).ToObject(_serializer); + var voiceState = RpcVoiceState.Create(this, data); - await _voiceStateUpdatedEvent.InvokeAsync().ConfigureAwait(false); + await _voiceStateCreatedEvent.InvokeAsync(voiceState).ConfigureAwait(false); } break; case "VOICE_STATE_UPDATE": { await _rpcLogger.DebugAsync("Received Dispatch (VOICE_STATE_UPDATE)").ConfigureAwait(false); + var data = (payload.Value as JToken).ToObject(_serializer); + var voiceState = RpcVoiceState.Create(this, data); - await _voiceStateUpdatedEvent.InvokeAsync().ConfigureAwait(false); + await _voiceStateUpdatedEvent.InvokeAsync(voiceState).ConfigureAwait(false); } break; case "VOICE_STATE_DELETE": { await _rpcLogger.DebugAsync("Received Dispatch (VOICE_STATE_DELETE)").ConfigureAwait(false); + var data = (payload.Value as JToken).ToObject(_serializer); + var voiceState = RpcVoiceState.Create(this, data); - await _voiceStateUpdatedEvent.InvokeAsync().ConfigureAwait(false); + await _voiceStateDeletedEvent.InvokeAsync(voiceState).ConfigureAwait(false); } break; case "SPEAKING_START": { await _rpcLogger.DebugAsync("Received Dispatch (SPEAKING_START)").ConfigureAwait(false); - await _voiceStateUpdatedEvent.InvokeAsync().ConfigureAwait(false); + var data = (payload.Value as JToken).ToObject(_serializer); + + await _speakingStartedEvent.InvokeAsync(data.UserId).ConfigureAwait(false); } break; case "SPEAKING_STOP": { await _rpcLogger.DebugAsync("Received Dispatch (SPEAKING_STOP)").ConfigureAwait(false); + var data = (payload.Value as JToken).ToObject(_serializer); + + await _speakingStoppedEvent.InvokeAsync(data.UserId).ConfigureAwait(false); + } + break; + case "VOICE_SETTINGS_UPDATE": + { + await _rpcLogger.DebugAsync("Received Dispatch (VOICE_SETTINGS_UPDATE)").ConfigureAwait(false); + var data = (payload.Value as JToken).ToObject(_serializer); + var settings = VoiceSettings.Create(data); - await _voiceStateUpdatedEvent.InvokeAsync().ConfigureAwait(false); + await _voiceSettingsUpdated.InvokeAsync(settings).ConfigureAwait(false); } break; @@ -373,18 +506,18 @@ namespace Discord.Rpc { await _rpcLogger.DebugAsync("Received Dispatch (MESSAGE_CREATE)").ConfigureAwait(false); var data = (payload.Value as JToken).ToObject(_serializer); - var msg = new RpcMessage(this, data.Message); + var msg = RpcMessage.Create(this, data.ChannelId, data.Message); - await _messageReceivedEvent.InvokeAsync(data.ChannelId, msg).ConfigureAwait(false); + await _messageReceivedEvent.InvokeAsync(msg).ConfigureAwait(false); } break; case "MESSAGE_UPDATE": { await _rpcLogger.DebugAsync("Received Dispatch (MESSAGE_UPDATE)").ConfigureAwait(false); var data = (payload.Value as JToken).ToObject(_serializer); - var msg = new RpcMessage(this, data.Message); + var msg = RpcMessage.Create(this, data.ChannelId, data.Message); - await _messageUpdatedEvent.InvokeAsync(data.ChannelId, msg).ConfigureAwait(false); + await _messageUpdatedEvent.InvokeAsync(msg).ConfigureAwait(false); } break; case "MESSAGE_DELETE": diff --git a/src/Discord.Net/Rpc/DiscordRpcConfig.cs b/src/Discord.Net.Rpc/DiscordRpcConfig.cs similarity index 61% rename from src/Discord.Net/Rpc/DiscordRpcConfig.cs rename to src/Discord.Net.Rpc/DiscordRpcConfig.cs index ac54551ed..d1e69376c 100644 --- a/src/Discord.Net/Rpc/DiscordRpcConfig.cs +++ b/src/Discord.Net.Rpc/DiscordRpcConfig.cs @@ -10,17 +10,6 @@ namespace Discord.Rpc public const int PortRangeStart = 6463; public const int PortRangeEnd = 6472; - public DiscordRpcConfig(string clientId, string origin) - { - ClientId = clientId; - Origin = origin; - } - - /// Gets or sets the Discord client/application id used for this RPC connection. - public string ClientId { get; } - /// Gets or sets the origin used for this RPC connection. - public string Origin { get; } - /// Gets or sets the time, in milliseconds, to wait for a connection to complete before aborting. public int ConnectionTimeout { get; set; } = 30000; diff --git a/src/Discord.Net.Rpc/Entities/Channels/IRpcAudioChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/IRpcAudioChannel.cs new file mode 100644 index 000000000..4fa01104a --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/Channels/IRpcAudioChannel.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Discord.Rpc +{ + public interface IRpcAudioChannel : IAudioChannel + { + IReadOnlyCollection VoiceStates { get; } + } +} diff --git a/src/Discord.Net.Rpc/Entities/Channels/IRpcMessageChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/IRpcMessageChannel.cs new file mode 100644 index 000000000..8e69c1b30 --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/Channels/IRpcMessageChannel.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Discord.Rpc +{ + public interface IRpcMessageChannel : IMessageChannel + { + IReadOnlyCollection CachedMessages { get; } + } +} diff --git a/src/Discord.Net.Rpc/Entities/Channels/IRpcPrivateChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/IRpcPrivateChannel.cs new file mode 100644 index 000000000..ae43c8675 --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/Channels/IRpcPrivateChannel.cs @@ -0,0 +1,6 @@ +namespace Discord.Rpc +{ + public interface IRpcPrivateChannel + { + } +} diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs new file mode 100644 index 000000000..934dae94b --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs @@ -0,0 +1,42 @@ +using System; + +using Model = Discord.API.Rpc.Channel; + +namespace Discord.Rpc +{ + public class RpcChannel : RpcEntity + { + public string Name { get; private set; } + + public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + + internal RpcChannel(DiscordRpcClient discord, ulong id) + : base(discord, id) + { + } + internal static RpcChannel Create(DiscordRpcClient discord, Model model) + { + if (model.GuildId.IsSpecified) + return RpcGuildChannel.Create(discord, model); + else + return CreatePrivate(discord, model); + } + internal static RpcChannel CreatePrivate(DiscordRpcClient discord, Model model) + { + switch (model.Type) + { + case ChannelType.DM: + return RpcDMChannel.Create(discord, model); + case ChannelType.Group: + return RpcGroupChannel.Create(discord, model); + default: + throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); + } + } + internal virtual void Update(Model model) + { + if (model.Name.IsSpecified) + Name = model.Name.Value; + } + } +} diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcChannelSummary.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcChannelSummary.cs new file mode 100644 index 000000000..72679ac58 --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcChannelSummary.cs @@ -0,0 +1,32 @@ +using System.Diagnostics; +using Model = Discord.API.Rpc.ChannelSummary; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RpcChannelSummary + { + public ulong Id { get; } + public string Name { get; set; } + public ChannelType Type { get; set; } + + internal RpcChannelSummary(ulong id) + { + Id = id; + } + internal static RpcChannelSummary Create(Model model) + { + var entity = new RpcChannelSummary(model.Id); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { + Name = model.Name; + Type = model.Type; + } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id}, {Type})"; + } +} diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs new file mode 100644 index 000000000..fc67e3084 --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs @@ -0,0 +1,124 @@ +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Rpc.Channel; + +namespace Discord.Rpc +{ + public class RpcDMChannel : RpcChannel, IRpcMessageChannel, IRpcPrivateChannel, IDMChannel + { + public IReadOnlyCollection CachedMessages { get; private set; } + + internal RpcDMChannel(DiscordRpcClient discord, ulong id) + : base(discord, id) + { + } + internal static new RpcDMChannel Create(DiscordRpcClient discord, Model model) + { + var entity = new RpcDMChannel(discord, model.Id); + entity.Update(model); + return entity; + } + internal override void Update(Model model) + { + base.Update(model); + CachedMessages = model.Messages.Select(x => RpcMessage.Create(Discord, Id, x)).ToImmutableArray(); + } + + public Task CloseAsync(RequestOptions options = null) + => ChannelHelper.DeleteAsync(this, Discord, options); + + //TODO: Use RPC cache + public Task GetMessageAsync(ulong id, RequestOptions options = null) + => ChannelHelper.GetMessageAsync(this, Discord, id, null, options); + public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, null, options); + public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, null, options); + public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, null, options); + public Task> GetPinnedMessagesAsync(RequestOptions options = null) + => ChannelHelper.GetPinnedMessagesAsync(this, Discord, null, options); + + public Task SendMessageAsync(string text, bool isTTS, RequestOptions options = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, null, options); + public Task SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, null, options); + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, null, options); + + public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + + public Task TriggerTypingAsync(RequestOptions options = null) + => ChannelHelper.TriggerTypingAsync(this, Discord, options); + public IDisposable EnterTypingState(RequestOptions options = null) + => ChannelHelper.EnterTypingState(this, Discord, options); + + public override string ToString() => Id.ToString(); + private string DebuggerDisplay => $"({Id}, DM)"; + + //IDMChannel + IUser IDMChannel.Recipient { get { throw new NotSupportedException(); } } + + //IPrivateChannel + IReadOnlyCollection IPrivateChannel.Recipients { get { throw new NotSupportedException(); } } + + //IMessageChannel + async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetMessageAsync(id, options).ConfigureAwait(false); + else + return null; + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(limit, options); + else + return AsyncEnumerable.Empty>(); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(fromMessageId, dir, limit, options); + else + return AsyncEnumerable.Empty>(); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(fromMessage, dir, limit, options); + else + return AsyncEnumerable.Empty>(); + } + async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) + => await GetPinnedMessagesAsync(options).ConfigureAwait(false); + + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) + => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) + => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, RequestOptions options) + => await SendMessageAsync(text, isTTS, options).ConfigureAwait(false); + IDisposable IMessageChannel.EnterTypingState(RequestOptions options) + => EnterTypingState(options); + + //IChannel + string IChannel.Name { get { throw new NotSupportedException(); } } + + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + throw new NotSupportedException(); + } + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs new file mode 100644 index 000000000..deffc7e14 --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs @@ -0,0 +1,123 @@ +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Rpc.Channel; + +namespace Discord.Rpc +{ + public class RpcGroupChannel : RpcChannel, IRpcMessageChannel, IRpcAudioChannel, IRpcPrivateChannel, IGroupChannel + { + public IReadOnlyCollection CachedMessages { get; private set; } + public IReadOnlyCollection VoiceStates { get; private set; } + + internal RpcGroupChannel(DiscordRpcClient discord, ulong id) + : base(discord, id) + { + } + internal new static RpcGroupChannel Create(DiscordRpcClient discord, Model model) + { + var entity = new RpcGroupChannel(discord, model.Id); + entity.Update(model); + return entity; + } + internal override void Update(Model model) + { + base.Update(model); + CachedMessages = model.Messages.Select(x => RpcMessage.Create(Discord, Id, x)).ToImmutableArray(); + VoiceStates = model.VoiceStates.Select(x => RpcVoiceState.Create(Discord, x)).ToImmutableArray(); + } + + public Task LeaveAsync(RequestOptions options = null) + => ChannelHelper.DeleteAsync(this, Discord, options); + + //TODO: Use RPC cache + public Task GetMessageAsync(ulong id, RequestOptions options = null) + => ChannelHelper.GetMessageAsync(this, Discord, id, null, options); + public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, null, options); + public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, null, options); + public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, null, options); + public Task> GetPinnedMessagesAsync(RequestOptions options = null) + => ChannelHelper.GetPinnedMessagesAsync(this, Discord, null, options); + + public Task SendMessageAsync(string text, bool isTTS, RequestOptions options = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, null, options); + public Task SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, null, options); + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, null, options); + + public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + + public Task TriggerTypingAsync(RequestOptions options = null) + => ChannelHelper.TriggerTypingAsync(this, Discord, options); + public IDisposable EnterTypingState(RequestOptions options = null) + => ChannelHelper.EnterTypingState(this, Discord, options); + + public override string ToString() => Id.ToString(); + private string DebuggerDisplay => $"({Id}, Group)"; + + //IPrivateChannel + IReadOnlyCollection IPrivateChannel.Recipients { get { throw new NotSupportedException(); } } + + //IMessageChannel + async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetMessageAsync(id, options).ConfigureAwait(false); + else + return null; + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(limit, options); + else + return AsyncEnumerable.Empty>(); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(fromMessageId, dir, limit, options); + else + return AsyncEnumerable.Empty>(); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(fromMessage, dir, limit, options); + else + return AsyncEnumerable.Empty>(); + } + async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) + => await GetPinnedMessagesAsync(options).ConfigureAwait(false); + + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) + => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) + => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, RequestOptions options) + => await SendMessageAsync(text, isTTS, options).ConfigureAwait(false); + IDisposable IMessageChannel.EnterTypingState(RequestOptions options) + => EnterTypingState(options); + + //IChannel + string IChannel.Name { get { throw new NotSupportedException(); } } + + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + throw new NotSupportedException(); + } + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs new file mode 100644 index 000000000..9b010b903 --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Discord.API.Rest; +using Model = Discord.API.Rpc.Channel; +using Discord.Rest; + +namespace Discord.Rpc +{ + public class RpcGuildChannel : RpcChannel, IGuildChannel + { + public ulong GuildId { get; } + public int Position { get; private set; } + + internal RpcGuildChannel(DiscordRpcClient discord, ulong id, ulong guildId) + : base(discord, id) + { + GuildId = guildId; + } + internal new static RpcGuildChannel Create(DiscordRpcClient discord, Model model) + { + switch (model.Type) + { + case ChannelType.Text: + return RpcTextChannel.Create(discord, model); + case ChannelType.Voice: + return RpcVoiceChannel.Create(discord, model); + default: + throw new InvalidOperationException("Unknown guild channel type"); + } + } + internal override void Update(Model model) + { + base.Update(model); + if (model.Position.IsSpecified) + Position = model.Position.Value; + } + + public Task ModifyAsync(Action func, RequestOptions options = null) + => ChannelHelper.ModifyAsync(this, Discord, func, options); + public Task DeleteAsync(RequestOptions options = null) + => ChannelHelper.DeleteAsync(this, Discord, options); + + public Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions perms, RequestOptions options = null) + => ChannelHelper.AddPermissionOverwriteAsync(this, Discord, user, perms, options); + public Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions perms, RequestOptions options = null) + => ChannelHelper.AddPermissionOverwriteAsync(this, Discord, role, perms, options); + public Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null) + => ChannelHelper.RemovePermissionOverwriteAsync(this, Discord, user, options); + public Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) + => ChannelHelper.RemovePermissionOverwriteAsync(this, Discord, role, options); + + public async Task> GetInvitesAsync(RequestOptions options = null) + => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); + public async Task CreateInviteAsync(int? maxAge = 3600, int? maxUses = null, bool isTemporary = true, RequestOptions options = null) + => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, options).ConfigureAwait(false); + + public override string ToString() => Name; + + //IGuildChannel + async Task> IGuildChannel.GetInvitesAsync(RequestOptions options) + => await GetInvitesAsync(options).ConfigureAwait(false); + async Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, RequestOptions options) + => await CreateInviteAsync(maxAge, maxUses, isTemporary, options).ConfigureAwait(false); + + IReadOnlyCollection IGuildChannel.PermissionOverwrites { get { throw new NotSupportedException(); } } + OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IUser user) + { + throw new NotSupportedException(); + } + OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IRole role) + { + throw new NotSupportedException(); + } + IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + { + throw new NotSupportedException(); + } + Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + throw new NotSupportedException(); + } + + //IChannel + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + { + throw new NotSupportedException(); + } + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs new file mode 100644 index 000000000..b29d392f5 --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs @@ -0,0 +1,113 @@ +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 Model = Discord.API.Rpc.Channel; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RpcTextChannel : RpcGuildChannel, IRpcMessageChannel, ITextChannel + { + public IReadOnlyCollection CachedMessages { get; private set; } + + public string Mention => MentionUtils.MentionChannel(Id); + + internal RpcTextChannel(DiscordRpcClient discord, ulong id, ulong guildId) + : base(discord, id, guildId) + { + } + internal new static RpcVoiceChannel Create(DiscordRpcClient discord, Model model) + { + var entity = new RpcVoiceChannel(discord, model.Id, model.GuildId.Value); + entity.Update(model); + return entity; + } + internal override void Update(Model model) + { + base.Update(model); + CachedMessages = model.Messages.Select(x => RpcMessage.Create(Discord, Id, x)).ToImmutableArray(); + } + + public Task ModifyAsync(Action func, RequestOptions options = null) + => ChannelHelper.ModifyAsync(this, Discord, func, options); + + //TODO: Use RPC cache + public Task GetMessageAsync(ulong id, RequestOptions options = null) + => ChannelHelper.GetMessageAsync(this, Discord, id, null, options); + public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, null, options); + public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, null, options); + public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, null, options); + public Task> GetPinnedMessagesAsync(RequestOptions options = null) + => ChannelHelper.GetPinnedMessagesAsync(this, Discord, null, options); + + public Task SendMessageAsync(string text, bool isTTS, RequestOptions options = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, null, options); + public Task SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, null, options); + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, null, options); + + public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + + public Task TriggerTypingAsync(RequestOptions options = null) + => ChannelHelper.TriggerTypingAsync(this, Discord, options); + public IDisposable EnterTypingState(RequestOptions options = null) + => ChannelHelper.EnterTypingState(this, Discord, options); + + private string DebuggerDisplay => $"{Name} ({Id}, Text)"; + + //ITextChannel + string ITextChannel.Topic { get { throw new NotSupportedException(); } } + + //IMessageChannel + async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetMessageAsync(id, options).ConfigureAwait(false); + else + return null; + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(limit, options); + else + return AsyncEnumerable.Empty>(); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(fromMessageId, dir, limit, options); + else + return AsyncEnumerable.Empty>(); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return GetMessagesAsync(fromMessage, dir, limit, options); + else + return AsyncEnumerable.Empty>(); + } + async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) + => await GetPinnedMessagesAsync(options).ConfigureAwait(false); + + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) + => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) + => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, RequestOptions options) + => await SendMessageAsync(text, isTTS, options).ConfigureAwait(false); + IDisposable IMessageChannel.EnterTypingState(RequestOptions options) + => EnterTypingState(options); + } +} diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs new file mode 100644 index 000000000..1e6510a38 --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs @@ -0,0 +1,49 @@ +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.Rpc.Channel; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RpcVoiceChannel : RpcGuildChannel, IRpcAudioChannel, IVoiceChannel + { + public int UserLimit { get; private set; } + public int Bitrate { get; private set; } + public IReadOnlyCollection VoiceStates { get; private set; } + + internal RpcVoiceChannel(DiscordRpcClient discord, ulong id, ulong guildId) + : base(discord, id, guildId) + { + } + internal new static RpcVoiceChannel Create(DiscordRpcClient discord, Model model) + { + var entity = new RpcVoiceChannel(discord, model.Id, model.GuildId.Value); + entity.Update(model); + return entity; + } + internal override void Update(Model model) + { + base.Update(model); + if (model.UserLimit.IsSpecified) + UserLimit = model.UserLimit.Value; + if (model.Bitrate.IsSpecified) + Bitrate = model.Bitrate.Value; + VoiceStates = model.VoiceStates.Select(x => RpcVoiceState.Create(Discord, x)).ToImmutableArray(); + } + + public Task ModifyAsync(Action func, RequestOptions options = null) + => ChannelHelper.ModifyAsync(this, Discord, func, options); + + private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; + + //IVoiceChannel + Task IVoiceChannel.ConnectAsync() { throw new NotSupportedException(); } + } +} diff --git a/src/Discord.Net.Rpc/Entities/Guilds/RpcGuild.cs b/src/Discord.Net.Rpc/Entities/Guilds/RpcGuild.cs new file mode 100644 index 000000000..7352d9e92 --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/Guilds/RpcGuild.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using Model = Discord.API.Rpc.Guild; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RpcGuild : RpcEntity + { + public string Name { get; private set; } + public string IconUrl { get; private set; } + public IReadOnlyCollection Users { get; private set; } + + internal RpcGuild(DiscordRpcClient discord, ulong id) + : base(discord, id) + { + } + internal static RpcGuild Create(DiscordRpcClient discord, Model model) + { + var entity = new RpcGuild(discord, model.Id); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { + Name = model.Name; + IconUrl = model.IconUrl; + Users = model.Members.Select(x => RpcGuildUser.Create(Discord, x)).ToImmutableArray(); + } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id})"; + } +} diff --git a/src/Discord.Net.Rpc/Entities/Guilds/RpcGuildStatus.cs b/src/Discord.Net.Rpc/Entities/Guilds/RpcGuildStatus.cs new file mode 100644 index 000000000..f443d7aa3 --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/Guilds/RpcGuildStatus.cs @@ -0,0 +1,30 @@ +using System.Diagnostics; +using Model = Discord.API.Rpc.GuildStatusEvent; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RpcGuildStatus + { + public RpcGuildSummary Guild { get; } + public int Online { get; private set; } + + internal RpcGuildStatus(ulong guildId) + { + Guild = new RpcGuildSummary(guildId); + } + internal static RpcGuildStatus Create(Model model) + { + var entity = new RpcGuildStatus(model.Guild.Id); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { + Online = model.Online; + } + + public override string ToString() => Guild.Name; + private string DebuggerDisplay => $"{Guild.Name} ({Guild.Id}, {Online} Online)"; + } +} diff --git a/src/Discord.Net.Rpc/Entities/Guilds/RpcGuildSummary.cs b/src/Discord.Net.Rpc/Entities/Guilds/RpcGuildSummary.cs new file mode 100644 index 000000000..4f9bff2c9 --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/Guilds/RpcGuildSummary.cs @@ -0,0 +1,30 @@ +using System.Diagnostics; +using Model = Discord.API.Rpc.GuildSummary; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RpcGuildSummary + { + public ulong Id { get; } + public string Name { get; private set; } + + internal RpcGuildSummary(ulong id) + { + Id = id; + } + internal static RpcGuildSummary Create(Model model) + { + var entity = new RpcGuildSummary(model.Id); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { + Name = model.Name; + } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id})"; + } +} diff --git a/src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs b/src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs new file mode 100644 index 000000000..3f4d102bf --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using Model = Discord.API.Rpc.Message; + +namespace Discord.Rpc +{ + public abstract class RpcMessage : RpcEntity, IMessage + { + private long _timestampTicks; + + public IMessageChannel Channel { get; } + public RpcUser Author { get; } + + public string Content { get; private set; } + public Color AuthorColor { get; private set; } + + public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public virtual bool IsTTS => false; + public virtual bool IsPinned => false; + public virtual bool IsBlocked => false; + public virtual DateTimeOffset? EditedTimestamp => null; + public virtual IReadOnlyCollection Attachments => ImmutableArray.Create(); + public virtual IReadOnlyCollection Embeds => ImmutableArray.Create(); + public virtual IReadOnlyCollection MentionedChannelIds => ImmutableArray.Create(); + public virtual IReadOnlyCollection MentionedRoleIds => ImmutableArray.Create(); + public virtual IReadOnlyCollection MentionedUserIds => ImmutableArray.Create(); + public virtual IReadOnlyCollection Tags => ImmutableArray.Create(); + public virtual ulong? WebhookId => null; + public bool IsWebhook => WebhookId != null; + + public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); + + internal RpcMessage(DiscordRpcClient discord, ulong id, IMessageChannel channel, RpcUser author) + : base(discord, id) + { + Channel = channel; + Author = author; + } + internal static RpcMessage Create(DiscordRpcClient discord, ulong channelId, Model model) + { + //model.ChannelId is always 0, needs to be passed from the event + if (model.Type == MessageType.Default) + return RpcUserMessage.Create(discord, channelId, model); + else + return RpcSystemMessage.Create(discord, channelId, model); + } + internal virtual void Update(Model model) + { + if (model.Timestamp.IsSpecified) + _timestampTicks = model.Timestamp.Value.UtcTicks; + + if (model.Content.IsSpecified) + Content = model.Content.Value; + if (model.AuthorColor.IsSpecified) + AuthorColor = new Color(Convert.ToUInt32(model.AuthorColor.Value.Substring(1), 16)); + } + + public override string ToString() => Content; + + MessageType IMessage.Type => MessageType.Default; + IUser IMessage.Author => Author; + IReadOnlyCollection IMessage.Attachments => Attachments; + IReadOnlyCollection IMessage.Embeds => Embeds; + } +} diff --git a/src/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs b/src/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs new file mode 100644 index 000000000..734ef38bc --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs @@ -0,0 +1,33 @@ +using Discord.Rest; +using System.Diagnostics; +using Model = Discord.API.Rpc.Message; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RpcSystemMessage : RpcMessage, ISystemMessage + { + public MessageType Type { get; private set; } + + internal RpcSystemMessage(DiscordRpcClient discord, ulong id, IMessageChannel channel, RpcUser author) + : base(discord, id, channel, author) + { + } + internal new static RpcSystemMessage Create(DiscordRpcClient discord, ulong channelId, Model model) + { + var entity = new RpcSystemMessage(discord, model.Id, + RestVirtualMessageChannel.Create(discord, channelId), + RpcUser.Create(discord, model.Author.Value)); + entity.Update(model); + return entity; + } + internal override void Update(Model model) + { + base.Update(model); + + Type = model.Type; + } + + private string DebuggerDisplay => $"{Author}: {Content} ({Id}, {Type})"; + } +} diff --git a/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs b/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs new file mode 100644 index 000000000..aa1d21973 --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs @@ -0,0 +1,117 @@ +using Discord.API.Rest; +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.Rpc.Message; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RpcUserMessage : RpcMessage, IUserMessage + { + private bool _isMentioningEveryone, _isTTS, _isPinned, _isBlocked; + private long? _editedTimestampTicks; + private ulong? _webhookId; + private ImmutableArray _attachments; + private ImmutableArray _embeds; + private ImmutableArray _tags; + + public override bool IsTTS => _isTTS; + public override bool IsPinned => _isPinned; + public override bool IsBlocked => _isBlocked; + public override ulong? WebhookId => _webhookId; + public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks); + public override IReadOnlyCollection Attachments => _attachments; + public override IReadOnlyCollection Embeds => _embeds; + public override IReadOnlyCollection MentionedChannelIds => MessageHelper.FilterTagsByKey(TagType.ChannelMention, _tags); + public override IReadOnlyCollection MentionedRoleIds => MessageHelper.FilterTagsByKey(TagType.RoleMention, _tags); + public override IReadOnlyCollection MentionedUserIds => MessageHelper.FilterTagsByKey(TagType.UserMention, _tags); + public override IReadOnlyCollection Tags => _tags; + + internal RpcUserMessage(DiscordRpcClient discord, ulong id, IMessageChannel channel, RpcUser author) + : base(discord, id, channel, author) + { + } + internal new static RpcUserMessage Create(DiscordRpcClient discord, ulong channelId, Model model) + { + var entity = new RpcUserMessage(discord, model.Id, + RestVirtualMessageChannel.Create(discord, channelId), + RpcUser.Create(discord, model.Author.Value)); + 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.WebhookId.IsSpecified) + _webhookId = model.WebhookId.Value; + + if (model.IsBlocked.IsSpecified) + _isBlocked = model.IsBlocked.Value; + + if (model.Attachments.IsSpecified) + { + var value = model.Attachments.Value; + if (value.Length > 0) + { + var attachments = ImmutableArray.CreateBuilder(value.Length); + for (int i = 0; i < value.Length; i++) + attachments.Add(Attachment.Create(value[i])); + _attachments = attachments.ToImmutable(); + } + else + _attachments = ImmutableArray.Create(); + } + + if (model.Embeds.IsSpecified) + { + var value = model.Embeds.Value; + if (value.Length > 0) + { + var embeds = ImmutableArray.CreateBuilder(value.Length); + for (int i = 0; i < value.Length; i++) + embeds.Add(Embed.Create(value[i])); + _embeds = embeds.ToImmutable(); + } + else + _embeds = ImmutableArray.Create(); + } + + if (model.Content.IsSpecified) + { + var text = model.Content.Value; + _tags = MessageHelper.ParseTags(text, null, null, ImmutableArray.Create()); + model.Content = text; + } + } + + public Task ModifyAsync(Action func, RequestOptions options) + => MessageHelper.ModifyAsync(this, Discord, func, options); + public Task DeleteAsync(RequestOptions options) + => MessageHelper.DeleteAsync(this, Discord, options); + + public Task PinAsync(RequestOptions options) + => MessageHelper.PinAsync(this, Discord, options); + public Task UnpinAsync(RequestOptions options) + => MessageHelper.UnpinAsync(this, Discord, options); + + public string Resolve(TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, + TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) + => MentionUtils.Resolve(this, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); + + private string DebuggerDisplay => $"{Author}: {Content} ({Id}{(Attachments.Count > 0 ? $", {Attachments.Count} Attachments" : "")})"; + } +} diff --git a/src/Discord.Net.Rpc/Entities/RpcEntity.cs b/src/Discord.Net.Rpc/Entities/RpcEntity.cs new file mode 100644 index 000000000..9fecd77ff --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/RpcEntity.cs @@ -0,0 +1,19 @@ +using System; + +namespace Discord.Rpc +{ + public abstract class RpcEntity : IEntity + where T : IEquatable + { + public DiscordRpcClient Discord { get; } + public T Id { get; } + + internal RpcEntity(DiscordRpcClient discord, T id) + { + Discord = discord; + Id = id; + } + + IDiscordClient IEntity.Discord => Discord; + } +} diff --git a/src/Discord.Net.Rpc/Entities/Users/Pan.cs b/src/Discord.Net.Rpc/Entities/Users/Pan.cs new file mode 100644 index 000000000..2db6cdb1e --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/Users/Pan.cs @@ -0,0 +1,25 @@ +using System.Diagnostics; +using Model = Discord.API.Rpc.Pan; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public struct Pan + { + public float Left { get; } + public float Right { get; } + + public Pan(float left, float right) + { + Left = left; + Right = right; + } + internal static Pan Create(Model model) + { + return new Pan(model.Left, model.Right); + } + + public override string ToString() => $"Left = {Left}, Right = {Right}"; + private string DebuggerDisplay => $"Left = {Left}, Right = {Right}"; + } +} diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcGuildUser.cs b/src/Discord.Net.Rpc/Entities/Users/RpcGuildUser.cs new file mode 100644 index 000000000..f4ca63750 --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/Users/RpcGuildUser.cs @@ -0,0 +1,29 @@ +using Model = Discord.API.Rpc.GuildMember; + +namespace Discord.Rpc +{ + public class RpcGuildUser : RpcUser + { + private UserStatus _status; + + public override UserStatus Status => _status; + //public object Acitivity { get; private set; } + + internal RpcGuildUser(DiscordRpcClient discord, ulong id) + : base(discord, id) + { + } + internal static RpcGuildUser Create(DiscordRpcClient discord, Model model) + { + var entity = new RpcGuildUser(discord, model.User.Id); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { + base.Update(model.User); + _status = model.Status; + //Activity = model.Activity; + } + } +} diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs b/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs new file mode 100644 index 000000000..360039beb --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs @@ -0,0 +1,58 @@ +using Discord.Rest; +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.User; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RpcUser : RpcEntity, IUser + { + public bool IsBot { get; private set; } + public string Username { get; private set; } + public ushort DiscriminatorValue { get; private set; } + public string AvatarId { get; private set; } + + public string AvatarUrl => API.CDN.GetUserAvatarUrl(Id, AvatarId); + public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public string Discriminator => DiscriminatorValue.ToString("D4"); + public string Mention => MentionUtils.MentionUser(Id); + public virtual Game? Game => null; + public virtual UserStatus Status => UserStatus.Unknown; + + internal RpcUser(DiscordRpcClient discord, ulong id) + : base(discord, id) + { + } + internal static RpcUser Create(DiscordRpcClient discord, Model model) + { + var entity = new RpcUser(discord, model.Id); + entity.Update(model); + return entity; + } + internal virtual void Update(Model model) + { + if (model.Avatar.IsSpecified) + AvatarId = model.Avatar.Value; + if (model.Discriminator.IsSpecified) + DiscriminatorValue = ushort.Parse(model.Discriminator.Value); + if (model.Bot.IsSpecified) + IsBot = model.Bot.Value; + if (model.Username.IsSpecified) + Username = model.Username.Value; + } + + public Task CreateDMChannelAsync(RequestOptions options = null) + => UserHelper.CreateDMChannelAsync(this, Discord, options); + + public override string ToString() => $"{Username}#{Discriminator}"; + internal string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; + + //IUser + Task IUser.GetDMChannelAsync(CacheMode mode, RequestOptions options) + => Task.FromResult(null); + async Task IUser.CreateDMChannelAsync(RequestOptions options) + => await CreateDMChannelAsync(options).ConfigureAwait(false); + } +} diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcVoiceState.cs b/src/Discord.Net.Rpc/Entities/Users/RpcVoiceState.cs new file mode 100644 index 000000000..f18a51434 --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/Users/RpcVoiceState.cs @@ -0,0 +1,79 @@ +using System; +using System.Diagnostics; +using Model = Discord.API.Rpc.ExtendedVoiceState; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RpcVoiceState : IVoiceState + { + [Flags] + private enum Flags : byte + { + Normal = 0x00, + Suppressed = 0x01, + Muted = 0x02, + Deafened = 0x04, + SelfMuted = 0x08, + SelfDeafened = 0x10, + } + + private Flags _voiceStates; + + public RpcUser User { get; } + public string Nickname { get; private set; } + public int Volume { get; private set; } + public bool IsMuted2 { get; private set; } + public Pan Pan { get; private set; } + + public bool IsMuted => (_voiceStates & Flags.Muted) != 0; + public bool IsDeafened => (_voiceStates & Flags.Deafened) != 0; + public bool IsSuppressed => (_voiceStates & Flags.Suppressed) != 0; + public bool IsSelfMuted => (_voiceStates & Flags.SelfMuted) != 0; + public bool IsSelfDeafened => (_voiceStates & Flags.SelfDeafened) != 0; + + internal RpcVoiceState(DiscordRpcClient discord, ulong userId) + { + User = new RpcUser(discord, userId); + } + internal static RpcVoiceState Create(DiscordRpcClient discord, Model model) + { + var entity = new RpcVoiceState(discord, model.User.Id); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { + if (model.VoiceState.IsSpecified) + { + Flags voiceStates = Flags.Normal; + if (model.VoiceState.Value.Mute) + voiceStates |= Flags.Muted; + if (model.VoiceState.Value.Deaf) + voiceStates |= Flags.Deafened; + if (model.VoiceState.Value.SelfMute) + voiceStates |= Flags.SelfMuted; + if (model.VoiceState.Value.SelfDeaf) + voiceStates |= Flags.SelfDeafened; + if (model.VoiceState.Value.Suppress) + voiceStates |= Flags.Suppressed; + _voiceStates = voiceStates; + } + User.Update(model.User); + if (model.Nickname.IsSpecified) + Nickname = model.Nickname.Value; + if (model.Volume.IsSpecified) + Volume = model.Volume.Value; + if (model.Mute.IsSpecified) + IsMuted2 = model.Mute.Value; + if (model.Pan.IsSpecified) + Pan = Pan.Create(model.Pan.Value); + } + + public override string ToString() => User.ToString(); + internal string DebuggerDisplay => $"{User} ({_voiceStates})"; + + string IVoiceState.VoiceSessionId { get { throw new NotSupportedException(); } } + IVoiceChannel IVoiceState.VoiceChannel { get { throw new NotSupportedException(); } } + } +} diff --git a/src/Discord.Net.Rpc/Entities/VoiceDevice.cs b/src/Discord.Net.Rpc/Entities/VoiceDevice.cs new file mode 100644 index 000000000..34a718adc --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/VoiceDevice.cs @@ -0,0 +1,25 @@ +using System.Diagnostics; +using Model = Discord.API.Rpc.VoiceDevice; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public struct VoiceDevice + { + public string Id { get; } + public string Name { get; } + + internal VoiceDevice(string id, string name) + { + Id = id; + Name = name; + } + internal static VoiceDevice Create(Model model) + { + return new VoiceDevice(model.Id, model.Name); + } + + public override string ToString() => $"{Name}"; + internal string DebuggerDisplay => $"{Name} ({Id})"; + } +} diff --git a/src/Discord.Net.Rpc/Entities/VoiceSettings.cs b/src/Discord.Net.Rpc/Entities/VoiceSettings.cs new file mode 100644 index 000000000..2e8d6e74c --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/VoiceSettings.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Model = Discord.API.Rpc.VoiceSettings; + +namespace Discord.Rpc +{ + public class VoiceSettings + { + public string InputDeviceId { get; private set; } + public float InputVolume { get; private set; } + public IReadOnlyCollection AvailableInputDevices { get; private set; } + + public string OutputDeviceId { get; private set; } + public float OutputVolume { get; private set; } + public IReadOnlyCollection AvailableOutputDevices { get; private set; } + + public bool AutomaticGainControl { get; private set; } + public bool EchoCancellation { get; private set; } + public bool NoiseSuppression { get; private set; } + public bool QualityOfService { get; private set; } + public bool SilenceWarning { get; private set; } + + public string ActivationMode { get; private set; } + public bool AutoThreshold { get; private set; } + public float Threshold { get; private set; } + public IReadOnlyCollection Shortcuts { get; private set; } + public float Delay { get; private set; } + + internal VoiceSettings() { } + internal static VoiceSettings Create(Model model) + { + var entity = new VoiceSettings(); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { + if (model.AutomaticGainControl.IsSpecified) + AutomaticGainControl = model.AutomaticGainControl.Value; + if (model.EchoCancellation.IsSpecified) + EchoCancellation = model.EchoCancellation.Value; + if (model.NoiseSuppression.IsSpecified) + NoiseSuppression = model.NoiseSuppression.Value; + if (model.QualityOfService.IsSpecified) + QualityOfService = model.QualityOfService.Value; + if (model.SilenceWarning.IsSpecified) + SilenceWarning = model.SilenceWarning.Value; + + if (model.Input.DeviceId.IsSpecified) + InputDeviceId = model.Input.DeviceId.Value; + if (model.Input.Volume.IsSpecified) + InputVolume = model.Input.Volume.Value; + if (model.Input.AvailableDevices.IsSpecified) + AvailableInputDevices = model.Input.AvailableDevices.Value.Select(x => VoiceDevice.Create(x)).ToImmutableArray(); + + if (model.Output.DeviceId.IsSpecified) + OutputDeviceId = model.Output.DeviceId.Value; + if (model.Output.Volume.IsSpecified) + OutputVolume = model.Output.Volume.Value; + if (model.Output.AvailableDevices.IsSpecified) + AvailableInputDevices = model.Output.AvailableDevices.Value.Select(x => VoiceDevice.Create(x)).ToImmutableArray(); + + if (model.Mode.Type.IsSpecified) + ActivationMode = model.Mode.Type.Value; + if (model.Mode.AutoThreshold.IsSpecified) + AutoThreshold = model.Mode.AutoThreshold.Value; + if (model.Mode.Threshold.IsSpecified) + Threshold = model.Mode.Threshold.Value; + if (model.Mode.Shortcut.IsSpecified) + Shortcuts = model.Mode.Shortcut.Value.Select(x => VoiceShortcut.Create(x)).ToImmutableArray(); + if (model.Mode.Delay.IsSpecified) + Delay = model.Mode.Delay.Value; + } + } +} diff --git a/src/Discord.Net.Rpc/Entities/VoiceShortcut.cs b/src/Discord.Net.Rpc/Entities/VoiceShortcut.cs new file mode 100644 index 000000000..93ef21804 --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/VoiceShortcut.cs @@ -0,0 +1,27 @@ +using System.Diagnostics; +using Model = Discord.API.Rpc.VoiceShortcut; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public struct VoiceShortcut + { + public VoiceShortcutType Type { get; } + public int Code { get; } + public string Name { get; } + + internal VoiceShortcut(VoiceShortcutType type, int code, string name) + { + Type = type; + Code = code; + Name = name; + } + internal static VoiceShortcut Create(Model model) + { + return new VoiceShortcut(model.Type.Value, model.Code.Value, model.Name.Value); + } + + public override string ToString() => $"{Name}"; + internal string DebuggerDisplay => $"{Name} ({Code}, {Type})"; + } +} diff --git a/src/Discord.Net.Rpc/Entities/VoiceShortcutType.cs b/src/Discord.Net.Rpc/Entities/VoiceShortcutType.cs new file mode 100644 index 000000000..f3b82804e --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/VoiceShortcutType.cs @@ -0,0 +1,10 @@ +namespace Discord.Rpc +{ + public enum VoiceShortcutType + { + KeyboardKey = 0, + MouseButton = 1, + KeyboardModifierKey = 2, + GamepadButton = 3 + } +} diff --git a/src/Discord.Net/Rpc/RpcChannelEvent.cs b/src/Discord.Net.Rpc/RpcChannelEvent.cs similarity index 100% rename from src/Discord.Net/Rpc/RpcChannelEvent.cs rename to src/Discord.Net.Rpc/RpcChannelEvent.cs diff --git a/src/Discord.Net.Rpc/RpcGlobalEvent.cs b/src/Discord.Net.Rpc/RpcGlobalEvent.cs new file mode 100644 index 000000000..673eaed5d --- /dev/null +++ b/src/Discord.Net.Rpc/RpcGlobalEvent.cs @@ -0,0 +1,9 @@ +namespace Discord.Rpc +{ + public enum RpcGlobalEvent + { + ChannelCreated, + GuildCreated, + VoiceSettingsUpdated + } +} diff --git a/src/Discord.Net/Rpc/RpcGuildEvent.cs b/src/Discord.Net.Rpc/RpcGuildEvent.cs similarity index 100% rename from src/Discord.Net/Rpc/RpcGuildEvent.cs rename to src/Discord.Net.Rpc/RpcGuildEvent.cs diff --git a/src/Discord.Net.Rpc/project.json b/src/Discord.Net.Rpc/project.json new file mode 100644 index 000000000..549bcc347 --- /dev/null +++ b/src/Discord.Net.Rpc/project.json @@ -0,0 +1,36 @@ +{ + "version": "1.0.0-beta2-*", + + "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" + }, + "System.IO.Compression": "4.1.0", + "System.Net.WebSockets.Client": "4.0.0" + }, + + "frameworks": { + "netstandard1.3": { + "imports": [ + "dotnet5.4", + "dnxcore50", + "portable-net45+win8" + ] + } + } +} diff --git a/src/Discord.Net/API/DiscordSocketApiClient.cs b/src/Discord.Net.WebSocket/API/DiscordSocketApiClient.cs similarity index 86% rename from src/Discord.Net/API/DiscordSocketApiClient.cs rename to src/Discord.Net.WebSocket/API/DiscordSocketApiClient.cs index 2d2579273..f0dd5f852 100644 --- a/src/Discord.Net/API/DiscordSocketApiClient.cs +++ b/src/Discord.Net.WebSocket/API/DiscordSocketApiClient.cs @@ -32,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+) @@ -48,7 +48,7 @@ namespace Discord.API using (var reader = new StreamReader(decompressed)) using (var jsonReader = new JsonTextReader(reader)) { - var msg = _serializer.Deserialize(jsonReader); + var msg = _serializer.Deserialize(jsonReader); await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); } } @@ -58,7 +58,7 @@ namespace Discord.API using (var reader = new StringReader(text)) using (var jsonReader = new JsonTextReader(reader)) { - var msg = _serializer.Deserialize(jsonReader); + var msg = _serializer.Deserialize(jsonReader); await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); } }; @@ -156,35 +156,30 @@ namespace Discord.API } //Core - private async Task SendGatewayInternalAsync(GatewayOpCode opCode, object payload, - BucketGroup group, int bucketId, ulong guildId, RequestOptions options) + public Task SendGatewayAsync(GatewayOpCode opCode, object payload, RequestOptions options = null) + => SendGatewayInternalAsync(opCode, payload, options); + private async Task SendGatewayInternalAsync(GatewayOpCode opCode, object payload, RequestOptions options) { CheckState(); //TODO: Add ETF byte[] bytes = null; - payload = new WebSocketMessage { Operation = (int)opCode, Payload = payload }; + payload = new SocketFrame { Operation = (int)opCode, Payload = payload }; if (payload != null) bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); - await RequestQueue.SendAsync(new WebSocketRequest(_gatewayClient, bytes, true, options), group, bucketId, guildId).ConfigureAwait(false); + await RequestQueue.SendAsync(new WebSocketRequest(_gatewayClient, null, bytes, true, options)).ConfigureAwait(false); await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false); } //Gateway - public Task SendGatewayAsync(GatewayOpCode opCode, object payload, - GlobalBucket bucket = GlobalBucket.GeneralGateway, RequestOptions options = null) - => SendGatewayInternalAsync(opCode, payload, BucketGroup.Global, (int)bucket, 0, options); - - public Task SendGatewayAsync(GatewayOpCode opCode, object payload, - GuildBucket bucket, ulong guildId, RequestOptions options = null) - => SendGatewayInternalAsync(opCode, payload, BucketGroup.Guild, (int)bucket, guildId, options); - public async Task GetGatewayAsync(RequestOptions options = null) { - return await SendAsync("GET", "gateway", options: options).ConfigureAwait(false); + options = RequestOptions.CreateOrClone(options); + return await SendAsync("GET", () => "gateway", new BucketIds(), options: options).ConfigureAwait(false); } public async Task SendIdentifyAsync(int largeThreshold = 100, bool useCompression = true, int shardID = 0, int totalShards = 1, RequestOptions options = null) { + options = RequestOptions.CreateOrClone(options); var props = new Dictionary { ["$device"] = "Discord.Net" @@ -203,6 +198,7 @@ namespace Discord.API } public async Task SendResumeAsync(string sessionId, int lastSeq, RequestOptions options = null) { + options = RequestOptions.CreateOrClone(options); var msg = new ResumeParams() { Token = _authToken, @@ -213,23 +209,29 @@ namespace Discord.API } public async Task SendHeartbeatAsync(int lastSeq, RequestOptions options = null) { + options = RequestOptions.CreateOrClone(options); await SendGatewayAsync(GatewayOpCode.Heartbeat, lastSeq, options: options).ConfigureAwait(false); } - public async Task SendStatusUpdateAsync(long? idleSince, Game game, RequestOptions options = null) + public async Task SendStatusUpdateAsync(UserStatus status, bool isAFK, long? since, Game game, RequestOptions options = null) { + options = RequestOptions.CreateOrClone(options); var args = new StatusUpdateParams { - IdleSince = idleSince, + Status = status, + IdleSince = since, + IsAFK = isAFK, Game = game }; await SendGatewayAsync(GatewayOpCode.StatusUpdate, args, options: options).ConfigureAwait(false); } public async Task SendRequestMembersAsync(IEnumerable guildIds, RequestOptions options = null) { + options = RequestOptions.CreateOrClone(options); await SendGatewayAsync(GatewayOpCode.RequestGuildMembers, new RequestMembersParams { GuildIds = guildIds, Query = "", Limit = 0 }, options: options).ConfigureAwait(false); } public async Task SendVoiceStateUpdateAsync(ulong guildId, ulong? channelId, bool selfDeaf, bool selfMute, RequestOptions options = null) { + options = RequestOptions.CreateOrClone(options); var payload = new VoiceStateUpdateParams { GuildId = guildId, @@ -241,6 +243,7 @@ namespace Discord.API } public async Task SendGuildSyncAsync(IEnumerable guildIds, RequestOptions options = null) { + options = RequestOptions.CreateOrClone(options); await SendGatewayAsync(GatewayOpCode.GuildSync, guildIds, options: options).ConfigureAwait(false); } } diff --git a/src/Discord.Net/API/DiscordVoiceAPIClient.cs b/src/Discord.Net.WebSocket/API/DiscordVoiceApiClient.cs similarity index 96% rename from src/Discord.Net/API/DiscordVoiceAPIClient.cs rename to src/Discord.Net.WebSocket/API/DiscordVoiceApiClient.cs index 8b9209c61..378acd22e 100644 --- a/src/Discord.Net/API/DiscordVoiceAPIClient.cs +++ b/src/Discord.Net.WebSocket/API/DiscordVoiceApiClient.cs @@ -9,11 +9,11 @@ using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Compression; +using System.Net; +using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; -using System.Net.Sockets; -using System.Net; namespace Discord.Audio { @@ -68,14 +68,14 @@ namespace Discord.Audio decompressed.Position = 0; using (var reader = new StreamReader(decompressed)) { - var msg = JsonConvert.DeserializeObject(reader.ReadToEnd()); + var msg = JsonConvert.DeserializeObject(reader.ReadToEnd()); await _receivedEvent.InvokeAsync((VoiceOpCode)msg.Operation, msg.Payload).ConfigureAwait(false); } } }; _webSocketClient.TextMessage += async text => { - var msg = JsonConvert.DeserializeObject(text); + var msg = JsonConvert.DeserializeObject(text); await _receivedEvent.InvokeAsync((VoiceOpCode)msg.Operation, msg.Payload).ConfigureAwait(false); }; _webSocketClient.Closed += async ex => @@ -103,11 +103,11 @@ namespace Discord.Audio public async Task SendAsync(VoiceOpCode opCode, object payload, RequestOptions options = null) { byte[] bytes = null; - payload = new WebSocketMessage { Operation = (int)opCode, Payload = payload }; + payload = new SocketFrame { Operation = (int)opCode, Payload = payload }; if (payload != null) bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); await _webSocketClient.SendAsync(bytes, 0, bytes.Length, true).ConfigureAwait(false); - await _sentGatewayMessageEvent.InvokeAsync(opCode); + await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false); } public async Task SendAsync(byte[] data, int bytes) { @@ -132,7 +132,7 @@ namespace Discord.Audio UserId = userId, SessionId = sessionId, Token = token - }); + }).ConfigureAwait(false); } public async Task SendSelectProtocol(string externalIp, int externalPort) { @@ -145,7 +145,7 @@ namespace Discord.Audio Port = externalPort, Mode = Mode } - }); + }).ConfigureAwait(false); } public async Task SendSetSpeaking(bool value) { @@ -153,7 +153,7 @@ namespace Discord.Audio { IsSpeaking = value, Delay = 0 - }); + }).ConfigureAwait(false); } public async Task ConnectAsync(string url) diff --git a/src/Discord.Net/API/Gateway/ExtendedGuild.cs b/src/Discord.Net.WebSocket/API/Gateway/ExtendedGuild.cs similarity index 100% rename from src/Discord.Net/API/Gateway/ExtendedGuild.cs rename to src/Discord.Net.WebSocket/API/Gateway/ExtendedGuild.cs diff --git a/src/Discord.Net/API/Gateway/GatewayOpCode.cs b/src/Discord.Net.WebSocket/API/Gateway/GatewayOpCode.cs similarity index 100% rename from src/Discord.Net/API/Gateway/GatewayOpCode.cs rename to src/Discord.Net.WebSocket/API/Gateway/GatewayOpCode.cs diff --git a/src/Discord.Net/API/Gateway/GuildBanEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildBanEvent.cs similarity index 100% rename from src/Discord.Net/API/Gateway/GuildBanEvent.cs rename to src/Discord.Net.WebSocket/API/Gateway/GuildBanEvent.cs diff --git a/src/Discord.Net/API/Gateway/GuildEmojiUpdateEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildEmojiUpdateEvent.cs similarity index 100% rename from src/Discord.Net/API/Gateway/GuildEmojiUpdateEvent.cs rename to src/Discord.Net.WebSocket/API/Gateway/GuildEmojiUpdateEvent.cs diff --git a/src/Discord.Net/API/Gateway/GuildMemberAddEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildMemberAddEvent.cs similarity index 100% rename from src/Discord.Net/API/Gateway/GuildMemberAddEvent.cs rename to src/Discord.Net.WebSocket/API/Gateway/GuildMemberAddEvent.cs diff --git a/src/Discord.Net/API/Gateway/GuildMemberRemoveEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildMemberRemoveEvent.cs similarity index 100% rename from src/Discord.Net/API/Gateway/GuildMemberRemoveEvent.cs rename to src/Discord.Net.WebSocket/API/Gateway/GuildMemberRemoveEvent.cs diff --git a/src/Discord.Net/API/Gateway/GuildMemberUpdateEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildMemberUpdateEvent.cs similarity index 100% rename from src/Discord.Net/API/Gateway/GuildMemberUpdateEvent.cs rename to src/Discord.Net.WebSocket/API/Gateway/GuildMemberUpdateEvent.cs diff --git a/src/Discord.Net/API/Gateway/GuildMembersChunkEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildMembersChunkEvent.cs similarity index 100% rename from src/Discord.Net/API/Gateway/GuildMembersChunkEvent.cs rename to src/Discord.Net.WebSocket/API/Gateway/GuildMembersChunkEvent.cs diff --git a/src/Discord.Net/API/Gateway/GuildRoleCreateEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildRoleCreateEvent.cs similarity index 100% rename from src/Discord.Net/API/Gateway/GuildRoleCreateEvent.cs rename to src/Discord.Net.WebSocket/API/Gateway/GuildRoleCreateEvent.cs diff --git a/src/Discord.Net/API/Gateway/GuildRoleDeleteEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildRoleDeleteEvent.cs similarity index 100% rename from src/Discord.Net/API/Gateway/GuildRoleDeleteEvent.cs rename to src/Discord.Net.WebSocket/API/Gateway/GuildRoleDeleteEvent.cs diff --git a/src/Discord.Net/API/Gateway/GuildRoleUpdateEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildRoleUpdateEvent.cs similarity index 100% rename from src/Discord.Net/API/Gateway/GuildRoleUpdateEvent.cs rename to src/Discord.Net.WebSocket/API/Gateway/GuildRoleUpdateEvent.cs diff --git a/src/Discord.Net/API/Gateway/GuildSyncEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildSyncEvent.cs similarity index 100% rename from src/Discord.Net/API/Gateway/GuildSyncEvent.cs rename to src/Discord.Net.WebSocket/API/Gateway/GuildSyncEvent.cs diff --git a/src/Discord.Net/API/Gateway/HelloEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/HelloEvent.cs similarity index 100% rename from src/Discord.Net/API/Gateway/HelloEvent.cs rename to src/Discord.Net.WebSocket/API/Gateway/HelloEvent.cs diff --git a/src/Discord.Net/API/Gateway/IdentifyParams.cs b/src/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs similarity index 100% rename from src/Discord.Net/API/Gateway/IdentifyParams.cs rename to src/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs diff --git a/src/Discord.Net/API/Gateway/MessageDeleteBulkEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/MessageDeleteBulkEvent.cs similarity index 100% rename from src/Discord.Net/API/Gateway/MessageDeleteBulkEvent.cs rename to src/Discord.Net.WebSocket/API/Gateway/MessageDeleteBulkEvent.cs diff --git a/src/Discord.Net/API/Gateway/ReadyEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/ReadyEvent.cs similarity index 100% rename from src/Discord.Net/API/Gateway/ReadyEvent.cs rename to src/Discord.Net.WebSocket/API/Gateway/ReadyEvent.cs diff --git a/src/Discord.Net/API/Gateway/RecipientEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/RecipientEvent.cs similarity index 100% rename from src/Discord.Net/API/Gateway/RecipientEvent.cs rename to src/Discord.Net.WebSocket/API/Gateway/RecipientEvent.cs diff --git a/src/Discord.Net/API/Gateway/RequestMembersParams.cs b/src/Discord.Net.WebSocket/API/Gateway/RequestMembersParams.cs similarity index 62% rename from src/Discord.Net/API/Gateway/RequestMembersParams.cs rename to src/Discord.Net.WebSocket/API/Gateway/RequestMembersParams.cs index d32f80522..05ec87f56 100644 --- a/src/Discord.Net/API/Gateway/RequestMembersParams.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/RequestMembersParams.cs @@ -1,7 +1,6 @@ #pragma warning disable CS1591 using Newtonsoft.Json; using System.Collections.Generic; -using System.Linq; namespace Discord.API.Gateway { @@ -14,8 +13,6 @@ namespace Discord.API.Gateway public int Limit { get; set; } [JsonProperty("guild_id")] - private ulong[] _guildIds { get; set; } - public IEnumerable GuildIds { set { _guildIds = value.ToArray(); } } - public IEnumerable Guilds { set { _guildIds = value.Select(x => x.Id).ToArray(); } } + public IEnumerable GuildIds { get; set; } } } diff --git a/src/Discord.Net/API/Gateway/ResumeParams.cs b/src/Discord.Net.WebSocket/API/Gateway/ResumeParams.cs similarity index 100% rename from src/Discord.Net/API/Gateway/ResumeParams.cs rename to src/Discord.Net.WebSocket/API/Gateway/ResumeParams.cs diff --git a/src/Discord.Net/API/Gateway/ResumedEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/ResumedEvent.cs similarity index 100% rename from src/Discord.Net/API/Gateway/ResumedEvent.cs rename to src/Discord.Net.WebSocket/API/Gateway/ResumedEvent.cs diff --git a/src/Discord.Net/API/Gateway/StatusUpdateParams.cs b/src/Discord.Net.WebSocket/API/Gateway/StatusUpdateParams.cs similarity index 62% rename from src/Discord.Net/API/Gateway/StatusUpdateParams.cs rename to src/Discord.Net.WebSocket/API/Gateway/StatusUpdateParams.cs index 26b726b36..ae1f79283 100644 --- a/src/Discord.Net/API/Gateway/StatusUpdateParams.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/StatusUpdateParams.cs @@ -6,8 +6,12 @@ namespace Discord.API.Gateway [JsonObject(MemberSerialization = MemberSerialization.OptIn)] public class StatusUpdateParams { - [JsonProperty("idle_since"), Int53] + [JsonProperty("status")] + public UserStatus Status { get; set; } + [JsonProperty("since"), Int53] public long? IdleSince { get; set; } + [JsonProperty("afk")] + public bool IsAFK { get; set; } [JsonProperty("game")] public Game Game { get; set; } } diff --git a/src/Discord.Net/API/Gateway/TypingStartEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/TypingStartEvent.cs similarity index 100% rename from src/Discord.Net/API/Gateway/TypingStartEvent.cs rename to src/Discord.Net.WebSocket/API/Gateway/TypingStartEvent.cs diff --git a/src/Discord.Net/API/Gateway/UpdateStatusParams.cs b/src/Discord.Net.WebSocket/API/Gateway/UpdateStatusParams.cs similarity index 100% rename from src/Discord.Net/API/Gateway/UpdateStatusParams.cs rename to src/Discord.Net.WebSocket/API/Gateway/UpdateStatusParams.cs diff --git a/src/Discord.Net/API/Gateway/VoiceServerUpdateEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/VoiceServerUpdateEvent.cs similarity index 88% rename from src/Discord.Net/API/Gateway/VoiceServerUpdateEvent.cs rename to src/Discord.Net.WebSocket/API/Gateway/VoiceServerUpdateEvent.cs index bc65ea6e8..a300f0d2c 100644 --- a/src/Discord.Net/API/Gateway/VoiceServerUpdateEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/VoiceServerUpdateEvent.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Gateway { - public class VoiceServerUpdateEvent + public class VoiceServerUpdateEvent { [JsonProperty("guild_id")] public ulong GuildId { get; set; } diff --git a/src/Discord.Net/API/Gateway/VoiceStateUpdateParams.cs b/src/Discord.Net.WebSocket/API/Gateway/VoiceStateUpdateParams.cs similarity index 100% rename from src/Discord.Net/API/Gateway/VoiceStateUpdateParams.cs rename to src/Discord.Net.WebSocket/API/Gateway/VoiceStateUpdateParams.cs diff --git a/src/Discord.Net/API/WebSocketMessage.cs b/src/Discord.Net.WebSocket/API/SocketFrame.cs similarity index 93% rename from src/Discord.Net/API/WebSocketMessage.cs rename to src/Discord.Net.WebSocket/API/SocketFrame.cs index e7c6a35b2..fd9367ca4 100644 --- a/src/Discord.Net/API/WebSocketMessage.cs +++ b/src/Discord.Net.WebSocket/API/SocketFrame.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API { - public class WebSocketMessage + public class SocketFrame { [JsonProperty("op")] public int Operation { get; set; } diff --git a/src/Discord.Net/API/Voice/IdentifyParams.cs b/src/Discord.Net.WebSocket/API/Voice/IdentifyParams.cs similarity index 100% rename from src/Discord.Net/API/Voice/IdentifyParams.cs rename to src/Discord.Net.WebSocket/API/Voice/IdentifyParams.cs diff --git a/src/Discord.Net/API/Voice/ReadyEvent.cs b/src/Discord.Net.WebSocket/API/Voice/ReadyEvent.cs similarity index 100% rename from src/Discord.Net/API/Voice/ReadyEvent.cs rename to src/Discord.Net.WebSocket/API/Voice/ReadyEvent.cs diff --git a/src/Discord.Net/API/Voice/SelectProtocolParams.cs b/src/Discord.Net.WebSocket/API/Voice/SelectProtocolParams.cs similarity index 100% rename from src/Discord.Net/API/Voice/SelectProtocolParams.cs rename to src/Discord.Net.WebSocket/API/Voice/SelectProtocolParams.cs diff --git a/src/Discord.Net/API/Voice/SessionDescriptionEvent.cs b/src/Discord.Net.WebSocket/API/Voice/SessionDescriptionEvent.cs similarity index 100% rename from src/Discord.Net/API/Voice/SessionDescriptionEvent.cs rename to src/Discord.Net.WebSocket/API/Voice/SessionDescriptionEvent.cs diff --git a/src/Discord.Net/API/Voice/SpeakingParams.cs b/src/Discord.Net.WebSocket/API/Voice/SpeakingParams.cs similarity index 100% rename from src/Discord.Net/API/Voice/SpeakingParams.cs rename to src/Discord.Net.WebSocket/API/Voice/SpeakingParams.cs diff --git a/src/Discord.Net/API/Voice/UdpProtocolInfo.cs b/src/Discord.Net.WebSocket/API/Voice/UdpProtocolInfo.cs similarity index 100% rename from src/Discord.Net/API/Voice/UdpProtocolInfo.cs rename to src/Discord.Net.WebSocket/API/Voice/UdpProtocolInfo.cs diff --git a/src/Discord.Net/API/Voice/VoiceOpCode.cs b/src/Discord.Net.WebSocket/API/Voice/VoiceOpCode.cs similarity index 100% rename from src/Discord.Net/API/Voice/VoiceOpCode.cs rename to src/Discord.Net.WebSocket/API/Voice/VoiceOpCode.cs diff --git a/src/Discord.Net.WebSocket/AssemblyInfo.cs b/src/Discord.Net.WebSocket/AssemblyInfo.cs new file mode 100644 index 000000000..c6b5997b4 --- /dev/null +++ b/src/Discord.Net.WebSocket/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Discord.Net.Tests")] \ No newline at end of file diff --git a/src/Discord.Net/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs similarity index 92% rename from src/Discord.Net/Audio/AudioClient.cs rename to src/Discord.Net.WebSocket/Audio/AudioClient.cs index 2c4511edf..04eec4541 100644 --- a/src/Discord.Net/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -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; @@ -13,7 +14,7 @@ using System.Threading.Tasks; namespace Discord.Audio { - internal class AudioClient : IAudioClient, IDisposable + public class AudioClient : IAudioClient, IDisposable { public event Func Connected { @@ -34,10 +35,7 @@ namespace Discord.Audio } private readonly AsyncEvent> _latencyUpdatedEvent = new AsyncEvent>(); - private readonly ILogger _audioLogger; -#if BENCHMARK - private readonly ILogger _benchmarkLogger; -#endif + private readonly Logger _audioLogger; internal readonly SemaphoreSlim _connectionLock; private readonly JsonSerializer _serializer; @@ -58,14 +56,11 @@ namespace Discord.Audio private DiscordSocketClient Discord => Guild.Discord; /// Creates a new REST/WebSocket discord client. - public AudioClient(SocketGuild guild, int id) + internal AudioClient(SocketGuild guild, int id) { Guild = guild; _audioLogger = Discord.LogManager.CreateLogger($"Audio #{id}"); -#if BENCHMARK - _benchmarkLogger = logManager.CreateLogger("Benchmark"); -#endif _connectionLock = new SemaphoreSlim(1, 1); @@ -95,7 +90,7 @@ namespace Discord.Audio } /// - public async Task ConnectAsync(string url, ulong userId, string sessionId, string token) + internal async Task ConnectAsync(string url, ulong userId, string sessionId, string token) { await _connectionLock.WaitAsync().ConfigureAwait(false); try @@ -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) { @@ -288,7 +269,7 @@ namespace Discord.Audio catch { return; } await _audioLogger.DebugAsync("Received Discovery").ConfigureAwait(false); - await ApiClient.SendSelectProtocol(ip, port); + await ApiClient.SendSelectProtocol(ip, port).ConfigureAwait(false); } } } @@ -319,7 +300,7 @@ namespace Discord.Audio catch (OperationCanceledException) { } } - internal virtual void Dispose(bool disposing) + internal void Dispose(bool disposing) { if (!_isDisposed) _isDisposed = true; diff --git a/src/Discord.Net/Audio/AudioMode.cs b/src/Discord.Net.WebSocket/Audio/AudioMode.cs similarity index 100% rename from src/Discord.Net/Audio/AudioMode.cs rename to src/Discord.Net.WebSocket/Audio/AudioMode.cs diff --git a/src/Discord.Net/Audio/Opus/OpusConverter.cs b/src/Discord.Net.WebSocket/Audio/Opus/OpusConverter.cs similarity index 100% rename from src/Discord.Net/Audio/Opus/OpusConverter.cs rename to src/Discord.Net.WebSocket/Audio/Opus/OpusConverter.cs diff --git a/src/Discord.Net/Audio/Opus/OpusCtl.cs b/src/Discord.Net.WebSocket/Audio/Opus/OpusCtl.cs similarity index 100% rename from src/Discord.Net/Audio/Opus/OpusCtl.cs rename to src/Discord.Net.WebSocket/Audio/Opus/OpusCtl.cs diff --git a/src/Discord.Net/Audio/Opus/OpusDecoder.cs b/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs similarity index 100% rename from src/Discord.Net/Audio/Opus/OpusDecoder.cs rename to src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs diff --git a/src/Discord.Net/Audio/Opus/OpusEncoder.cs b/src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs similarity index 100% rename from src/Discord.Net/Audio/Opus/OpusEncoder.cs rename to src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs diff --git a/src/Discord.Net/Audio/Opus/OpusError.cs b/src/Discord.Net.WebSocket/Audio/Opus/OpusError.cs similarity index 100% rename from src/Discord.Net/Audio/Opus/OpusError.cs rename to src/Discord.Net.WebSocket/Audio/Opus/OpusError.cs diff --git a/src/Discord.Net/Audio/Sodium/SecretBox.cs b/src/Discord.Net.WebSocket/Audio/Sodium/SecretBox.cs similarity index 100% rename from src/Discord.Net/Audio/Sodium/SecretBox.cs rename to src/Discord.Net.WebSocket/Audio/Sodium/SecretBox.cs diff --git a/src/Discord.Net/Audio/Streams/OpusDecodeStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs similarity index 94% rename from src/Discord.Net/Audio/Streams/OpusDecodeStream.cs rename to src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs index c059955a8..3a650eeaf 100644 --- a/src/Discord.Net/Audio/Streams/OpusDecodeStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs @@ -1,6 +1,6 @@ namespace Discord.Audio { - public class OpusDecodeStream : RTPReadStream + internal class OpusDecodeStream : RTPReadStream { private readonly byte[] _buffer; private readonly OpusDecoder _decoder; diff --git a/src/Discord.Net/Audio/Streams/OpusEncodeStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs similarity index 95% rename from src/Discord.Net/Audio/Streams/OpusEncodeStream.cs rename to src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs index ef773ca56..69d8b3d81 100644 --- a/src/Discord.Net/Audio/Streams/OpusEncodeStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs @@ -1,6 +1,6 @@ namespace Discord.Audio { - public class OpusEncodeStream : RTPWriteStream + internal class OpusEncodeStream : RTPWriteStream { public int SampleRate = 48000; public int Channels = 2; diff --git a/src/Discord.Net/Audio/Streams/RTPReadStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs similarity index 97% rename from src/Discord.Net/Audio/Streams/RTPReadStream.cs rename to src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs index 4bf7f5e1b..cfc804abe 100644 --- a/src/Discord.Net/Audio/Streams/RTPReadStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs @@ -4,7 +4,7 @@ using System.IO; namespace Discord.Audio { - public class RTPReadStream : Stream + internal class RTPReadStream : Stream { private readonly BlockingCollection _queuedData; //TODO: Replace with max-length ring buffer private readonly AudioClient _audioClient; diff --git a/src/Discord.Net/Audio/Streams/RTPWriteStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs similarity index 98% rename from src/Discord.Net/Audio/Streams/RTPWriteStream.cs rename to src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs index d547f021a..db755c877 100644 --- a/src/Discord.Net/Audio/Streams/RTPWriteStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs @@ -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; diff --git a/src/Discord.Net/WebSocket/DataStore.cs b/src/Discord.Net.WebSocket/ClientState.cs similarity index 79% rename from src/Discord.Net/WebSocket/DataStore.cs rename to src/Discord.Net.WebSocket/ClientState.cs index 9be13b9f0..a452113e2 100644 --- a/src/Discord.Net/WebSocket/DataStore.cs +++ b/src/Discord.Net.WebSocket/ClientState.cs @@ -5,20 +5,19 @@ using System.Linq; namespace Discord.WebSocket { - internal class DataStore + internal class ClientState { - private const int CollectionConcurrencyLevel = 1; //WebSocket updater/event handler. //TODO: Needs profiling, increase to 2? private const double AverageChannelsPerGuild = 10.22; //Source: Googie2149 private const double AverageUsersPerGuild = 47.78; //Source: Googie2149 private const double CollectionMultiplier = 1.05; //Add 5% buffer to handle growth - private readonly ConcurrentDictionary _channels; + private readonly ConcurrentDictionary _channels; private readonly ConcurrentDictionary _dmChannels; private readonly ConcurrentDictionary _guilds; private readonly ConcurrentDictionary _users; private readonly ConcurrentHashSet _groupChannels; - internal IReadOnlyCollection Channels => _channels.ToReadOnlyCollection(); + internal IReadOnlyCollection Channels => _channels.ToReadOnlyCollection(); internal IReadOnlyCollection DMChannels => _dmChannels.ToReadOnlyCollection(); internal IReadOnlyCollection GroupChannels => _groupChannels.Select(x => GetChannel(x) as SocketGroupChannel).ToReadOnlyCollection(_groupChannels); internal IReadOnlyCollection Guilds => _guilds.ToReadOnlyCollection(); @@ -29,20 +28,20 @@ namespace Discord.WebSocket _groupChannels.Select(x => GetChannel(x) as ISocketPrivateChannel)) .ToReadOnlyCollection(() => _dmChannels.Count + _groupChannels.Count); - public DataStore(int guildCount, int dmChannelCount) + public ClientState(int guildCount, int dmChannelCount) { double estimatedChannelCount = guildCount * AverageChannelsPerGuild + dmChannelCount; double estimatedUsersCount = guildCount * AverageUsersPerGuild; - _channels = new ConcurrentDictionary(CollectionConcurrencyLevel, (int)(estimatedChannelCount * CollectionMultiplier)); - _dmChannels = new ConcurrentDictionary(CollectionConcurrencyLevel, (int)(dmChannelCount * CollectionMultiplier)); - _guilds = new ConcurrentDictionary(CollectionConcurrencyLevel, (int)(guildCount * CollectionMultiplier)); - _users = new ConcurrentDictionary(CollectionConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier)); - _groupChannels = new ConcurrentHashSet(CollectionConcurrencyLevel, (int)(10 * CollectionMultiplier)); + _channels = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedChannelCount * CollectionMultiplier)); + _dmChannels = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(dmChannelCount * CollectionMultiplier)); + _guilds = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(guildCount * CollectionMultiplier)); + _users = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier)); + _groupChannels = new ConcurrentHashSet(ConcurrentHashSet.DefaultConcurrencyLevel, (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 +53,7 @@ namespace Discord.WebSocket return channel; return null; } - internal void AddChannel(ISocketChannel channel) + internal void AddChannel(SocketChannel channel) { _channels[channel.Id] = channel; @@ -68,9 +67,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; diff --git a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xproj b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xproj new file mode 100644 index 000000000..45e13b5ce --- /dev/null +++ b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xproj @@ -0,0 +1,19 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 22ab6c66-536c-4ac2-bbdb-a8bc4eb6b14d + Discord.WebSocket + .\obj + .\bin\ + v4.6.1 + + + 2.0 + + + \ No newline at end of file diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs new file mode 100644 index 000000000..3c9bf4fba --- /dev/null +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs @@ -0,0 +1,203 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.WebSocket +{ + //TODO: Add event docstrings + public partial class DiscordSocketClient + { + //General + public event Func Connected + { + add { _connectedEvent.Add(value); } + remove { _connectedEvent.Remove(value); } + } + private readonly AsyncEvent> _connectedEvent = new AsyncEvent>(); + public event Func Disconnected + { + add { _disconnectedEvent.Add(value); } + remove { _disconnectedEvent.Remove(value); } + } + private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); + public event Func Ready + { + add { _readyEvent.Add(value); } + remove { _readyEvent.Remove(value); } + } + private readonly AsyncEvent> _readyEvent = new AsyncEvent>(); + public event Func LatencyUpdated + { + add { _latencyUpdatedEvent.Add(value); } + remove { _latencyUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _latencyUpdatedEvent = new AsyncEvent>(); + + //Channels + public event Func ChannelCreated + { + add { _channelCreatedEvent.Add(value); } + remove { _channelCreatedEvent.Remove(value); } + } + private readonly AsyncEvent> _channelCreatedEvent = new AsyncEvent>(); + public event Func ChannelDestroyed + { + add { _channelDestroyedEvent.Add(value); } + remove { _channelDestroyedEvent.Remove(value); } + } + private readonly AsyncEvent> _channelDestroyedEvent = new AsyncEvent>(); + public event Func ChannelUpdated + { + add { _channelUpdatedEvent.Add(value); } + remove { _channelUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _channelUpdatedEvent = new AsyncEvent>(); + + //Messages + public event Func MessageReceived + { + add { _messageReceivedEvent.Add(value); } + remove { _messageReceivedEvent.Remove(value); } + } + private readonly AsyncEvent> _messageReceivedEvent = new AsyncEvent>(); + public event Func, Task> MessageDeleted + { + add { _messageDeletedEvent.Add(value); } + remove { _messageDeletedEvent.Remove(value); } + } + private readonly AsyncEvent, Task>> _messageDeletedEvent = new AsyncEvent, Task>>(); + public event Func, SocketMessage, Task> MessageUpdated + { + add { _messageUpdatedEvent.Add(value); } + remove { _messageUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent, SocketMessage, Task>> _messageUpdatedEvent = new AsyncEvent, SocketMessage, Task>>(); + + //Roles + public event Func RoleCreated + { + add { _roleCreatedEvent.Add(value); } + remove { _roleCreatedEvent.Remove(value); } + } + private readonly AsyncEvent> _roleCreatedEvent = new AsyncEvent>(); + public event Func RoleDeleted + { + add { _roleDeletedEvent.Add(value); } + remove { _roleDeletedEvent.Remove(value); } + } + private readonly AsyncEvent> _roleDeletedEvent = new AsyncEvent>(); + public event Func RoleUpdated + { + add { _roleUpdatedEvent.Add(value); } + remove { _roleUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _roleUpdatedEvent = new AsyncEvent>(); + + //Guilds + public event Func JoinedGuild + { + add { _joinedGuildEvent.Add(value); } + remove { _joinedGuildEvent.Remove(value); } + } + private AsyncEvent> _joinedGuildEvent = new AsyncEvent>(); + public event Func LeftGuild + { + add { _leftGuildEvent.Add(value); } + remove { _leftGuildEvent.Remove(value); } + } + private AsyncEvent> _leftGuildEvent = new AsyncEvent>(); + public event Func GuildAvailable + { + add { _guildAvailableEvent.Add(value); } + remove { _guildAvailableEvent.Remove(value); } + } + private AsyncEvent> _guildAvailableEvent = new AsyncEvent>(); + public event Func GuildUnavailable + { + add { _guildUnavailableEvent.Add(value); } + remove { _guildUnavailableEvent.Remove(value); } + } + private AsyncEvent> _guildUnavailableEvent = new AsyncEvent>(); + public event Func GuildMembersDownloaded + { + add { _guildMembersDownloadedEvent.Add(value); } + remove { _guildMembersDownloadedEvent.Remove(value); } + } + private AsyncEvent> _guildMembersDownloadedEvent = new AsyncEvent>(); + public event Func GuildUpdated + { + add { _guildUpdatedEvent.Add(value); } + remove { _guildUpdatedEvent.Remove(value); } + } + private AsyncEvent> _guildUpdatedEvent = new AsyncEvent>(); + + //Users + public event Func UserJoined + { + add { _userJoinedEvent.Add(value); } + remove { _userJoinedEvent.Remove(value); } + } + private readonly AsyncEvent> _userJoinedEvent = new AsyncEvent>(); + public event Func UserLeft + { + add { _userLeftEvent.Add(value); } + remove { _userLeftEvent.Remove(value); } + } + private readonly AsyncEvent> _userLeftEvent = new AsyncEvent>(); + public event Func UserBanned + { + add { _userBannedEvent.Add(value); } + remove { _userBannedEvent.Remove(value); } + } + private readonly AsyncEvent> _userBannedEvent = new AsyncEvent>(); + public event Func UserUnbanned + { + add { _userUnbannedEvent.Add(value); } + remove { _userUnbannedEvent.Remove(value); } + } + private readonly AsyncEvent> _userUnbannedEvent = new AsyncEvent>(); + public event Func UserUpdated + { + add { _userUpdatedEvent.Add(value); } + remove { _userUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _userUpdatedEvent = new AsyncEvent>(); + public event Func UserPresenceUpdated + { + add { _userPresenceUpdatedEvent.Add(value); } + remove { _userPresenceUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _userPresenceUpdatedEvent = new AsyncEvent>(); + public event Func UserVoiceStateUpdated + { + add { _userVoiceStateUpdatedEvent.Add(value); } + remove { _userVoiceStateUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _userVoiceStateUpdatedEvent = new AsyncEvent>(); + public event Func CurrentUserUpdated + { + add { _selfUpdatedEvent.Add(value); } + remove { _selfUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _selfUpdatedEvent = new AsyncEvent>(); + public event Func UserIsTyping + { + add { _userIsTypingEvent.Add(value); } + remove { _userIsTypingEvent.Remove(value); } + } + private readonly AsyncEvent> _userIsTypingEvent = new AsyncEvent>(); + public event Func RecipientAdded + { + add { _recipientAddedEvent.Add(value); } + remove { _recipientAddedEvent.Remove(value); } + } + private readonly AsyncEvent> _recipientAddedEvent = new AsyncEvent>(); + public event Func RecipientRemoved + { + add { _recipientRemovedEvent.Add(value); } + remove { _recipientRemovedEvent.Remove(value); } + } + private readonly AsyncEvent> _recipientRemovedEvent = new AsyncEvent>(); + + //TODO: Add PresenceUpdated? VoiceStateUpdated?, VoiceConnected, VoiceDisconnected; + } +} diff --git a/src/Discord.Net/WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs similarity index 79% rename from src/Discord.Net/WebSocket/DiscordSocketClient.cs rename to src/Discord.Net.WebSocket/DiscordSocketClient.cs index 3bd5df7de..618fcf5e3 100644 --- a/src/Discord.Net/WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -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 : BaseDiscordClient, IDiscordClient { private readonly ConcurrentQueue _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 _voiceRegions; + private ImmutableDictionary _voiceRegions; private TaskCompletionSource _connectTask; private CancellationTokenSource _cancelToken, _reconnectCancelToken; private Task _heartbeatTask, _guildDownloadTask, _reconnectTask; @@ -38,7 +37,7 @@ namespace Discord.WebSocket private int _nextAudioId; private bool _canReconnect; - /// Gets the shard id of this client. + /// Gets the shard of of this client. public int ShardId { get; } /// Gets the current connection state of this client. public ConnectionState ConnectionState { get; private set; } @@ -50,20 +49,21 @@ namespace Discord.WebSocket internal int MessageCacheSize { get; private set; } internal int LargeThreshold { get; private set; } internal AudioMode AudioMode { get; private set; } - internal DataStore DataStore { get; private set; } + internal ClientState State { get; private set; } 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; - internal IReadOnlyCollection Guilds => DataStore.Guilds; - internal IReadOnlyCollection VoiceRegions => _voiceRegions.ToReadOnlyCollection(); + public new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; + public new SocketSelfUser CurrentUser { get { return base.CurrentUser as SocketSelfUser; } private set { base.CurrentUser = value; } } + public IReadOnlyCollection Guilds => State.Guilds; + public IReadOnlyCollection PrivateChannels => State.PrivateChannels; /// Creates a new REST/WebSocket discord client. public DiscordSocketClient() : this(new DiscordSocketConfig()) { } /// Creates a new REST/WebSocket discord client. - 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); + State = new ClientState(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(); + _voiceRegions = ImmutableDictionary.Create(); _largeGuilds = new ConcurrentQueue(); } 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); + var voiceRegions = await ApiClient.GetVoiceRegionsAsync(new RequestOptions { IgnoreState = true}).ConfigureAwait(false); + _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(); + _voiceRegions = ImmutableDictionary.Create(); } - + /// public async Task ConnectAsync(bool waitForGuilds = true) { @@ -167,18 +163,27 @@ namespace Discord.WebSocket //Abort connection on timeout var _ = Task.Run(async () => { - await Task.Delay(ConnectionTimeout); + await Task.Delay(ConnectionTimeout).ConfigureAwait(false); connectTask.TrySetException(new TimeoutException()); }); + await _gatewayLogger.DebugAsync("Connecting ApiClient").ConfigureAwait(false); await ApiClient.ConnectAsync().ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Raising Event").ConfigureAwait(false); await _connectedEvent.InvokeAsync().ConfigureAwait(false); if (_sessionId != null) + { + await _gatewayLogger.DebugAsync("Resuming").ConfigureAwait(false); await ApiClient.SendResumeAsync(_sessionId, _lastSeq).ConfigureAwait(false); + } else - await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards:TotalShards).ConfigureAwait(false); + { + await _gatewayLogger.DebugAsync("Identifying").ConfigureAwait(false); + await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards).ConfigureAwait(false); + } + await _gatewayLogger.DebugAsync("Raising Event").ConfigureAwait(false); await _connectTask.Task.ConfigureAwait(false); if (!isReconnecting) _canReconnect = true; @@ -220,33 +225,34 @@ namespace Discord.WebSocket ConnectionState = ConnectionState.Disconnecting; await _gatewayLogger.InfoAsync("Disconnecting").ConfigureAwait(false); - await _gatewayLogger.DebugAsync("Disconnecting - CancelToken").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Cancelling current tasks").ConfigureAwait(false); //Signal tasks to complete try { _cancelToken.Cancel(); } catch { } - await _gatewayLogger.DebugAsync("Disconnecting - ApiClient").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Disconnecting ApiClient").ConfigureAwait(false); //Disconnect from server await ApiClient.DisconnectAsync().ConfigureAwait(false); //Wait for tasks to complete - await _gatewayLogger.DebugAsync("Disconnecting - Heartbeat").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Waiting for heartbeater").ConfigureAwait(false); var heartbeatTask = _heartbeatTask; if (heartbeatTask != null) await heartbeatTask.ConfigureAwait(false); _heartbeatTask = null; - await _gatewayLogger.DebugAsync("Disconnecting - Guild Downloader").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Waiting for guild downloader").ConfigureAwait(false); var guildDownloadTask = _guildDownloadTask; if (guildDownloadTask != null) await guildDownloadTask.ConfigureAwait(false); _guildDownloadTask = null; //Clear large guild queue - await _gatewayLogger.DebugAsync("Disconnecting - Clean Large Guilds").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Clearing large guild queue").ConfigureAwait(false); while (_largeGuilds.TryDequeue(out guildId)) { } //Raise virtual GUILD_UNAVAILABLEs - foreach (var guild in DataStore.Guilds) + await _gatewayLogger.DebugAsync("Raising virtual GuildUnavailables").ConfigureAwait(false); + foreach (var guild in State.Guilds) { if (guild._available) await _guildUnavailableEvent.InvokeAsync(guild).ConfigureAwait(false); @@ -319,128 +325,78 @@ namespace Discord.WebSocket } /// - public override Task GetVoiceRegionAsync(string id) - { - VoiceRegion region; - if (_voiceRegions.TryGetValue(id, out region)) - return Task.FromResult(region); - return Task.FromResult(null); - } + public Task GetApplicationInfoAsync() + => ClientHelper.GetApplicationInfoAsync(this); /// - public override Task GetGuildAsync(ulong id) - { - return Task.FromResult(DataStore.GetGuild(id)); - } - public override Task GetGuildEmbedAsync(ulong id) - { - var guild = DataStore.GetGuild(id); - if (guild != null) - return Task.FromResult(new GuildEmbed(guild.IsEmbeddable, guild.EmbedChannelId)); - else - return Task.FromResult(null); - } - public override Task> GetGuildSummariesAsync() - { - return Task.FromResult>(Guilds); - } - public override Task> GetGuildsAsync() - { - return Task.FromResult>(Guilds); - } - internal SocketGuild AddGuild(ExtendedGuild model, DataStore dataStore) + public SocketGuild GetGuild(ulong id) { - var guild = new SocketGuild(this, model, dataStore); - dataStore.AddGuild(guild); - if (model.Large) - _largeGuilds.Enqueue(model.Id); - return guild; + return State.GetGuild(id); } - internal SocketGuild RemoveGuild(ulong id) + /// + public Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null) + => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon); + + /// + public SocketChannel GetChannel(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; + return State.GetChannel(id); } - + /// - public override Task GetChannelAsync(ulong id) + public Task> GetConnectionsAsync() + => ClientHelper.GetConnectionsAsync(this); + + /// + public Task GetInviteAsync(string inviteId) + => ClientHelper.GetInviteAsync(this, inviteId); + + /// + public SocketUser GetUser(ulong id) { - return Task.FromResult(DataStore.GetChannel(id)); + return State.GetUser(id); } - public override Task> GetPrivateChannelsAsync() + /// + public SocketUser GetUser(string username, string discriminator) { - return Task.FromResult>(DataStore.PrivateChannels); + return State.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault(); } - internal ISocketChannel AddPrivateChannel(API.Channel model, DataStore dataStore) + internal SocketGlobalUser GetOrCreateUser(ClientState state, Discord.API.User model) { - switch (model.Type) + return state.GetOrAddUser(model.Id, x => { - 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, UpdateSource.Creation, dataStore); - dataStore.AddChannel(channel); - return channel; - } - default: - throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); - } + var user = SocketGlobalUser.Create(this, state, model); + user.GlobalUser.AddRef(); + return user; + }); } - internal ISocketChannel RemovePrivateChannel(ulong id) + internal SocketGlobalUser GetOrCreateSelfUser(ClientState state, Discord.API.User model) { - var channel = DataStore.RemoveChannel(id) as ISocketPrivateChannel; - if (channel != null) + return state.GetOrAddUser(model.Id, x => { - foreach (var recipient in channel.Recipients) - recipient.User.RemoveRef(this); - } - return channel; + var user = SocketGlobalUser.Create(this, state, model); + user.GlobalUser.AddRef(); + user.Presence = new SocketPresence(UserStatus.Online, null); + return user; + }); } - - /// - public override Task GetUserAsync(ulong id) - { - return Task.FromResult(DataStore.GetUser(id)); - } - /// - public override Task GetUserAsync(string username, string discriminator) + internal void RemoveUser(ulong id) { - return Task.FromResult(DataStore.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault()); + State.RemoveUser(id); } + /// - public override Task GetCurrentUserAsync() - { - return Task.FromResult(_currentUser); - } - internal SocketGlobalUser GetOrAddUser(API.User model, DataStore dataStore) - { - var user = dataStore.GetOrAddUser(model.Id, _ => new SocketGlobalUser(model)); - user.AddRef(); - return user; - } - internal SocketGlobalUser RemoveUser(ulong id) + public RestVoiceRegion GetVoiceRegion(string id) { - return DataStore.RemoveUser(id); + RestVoiceRegion region; + if (_voiceRegions.TryGetValue(id, out region)) + return region; + return null; } /// Downloads the users list for all large guilds. public Task DownloadAllUsersAsync() - => DownloadUsersAsync(DataStore.Guilds.Where(x => !x.HasAllMembers)); + => DownloadUsersAsync(State.Guilds.Where(x => !x.HasAllMembers)); /// Downloads the users list for the provided guilds, if they don't have a complete list. public Task DownloadUsersAsync(IEnumerable guilds) => DownloadUsersAsync(guilds.Select(x => x as SocketGuild).Where(x => x != null)); @@ -448,13 +404,13 @@ namespace Discord.WebSocket => DownloadUsersAsync(guilds.Select(x => x as SocketGuild).Where(x => x != null)); private async Task DownloadUsersAsync(IEnumerable guilds) { - var cachedGuilds = guilds.ToArray(); + var cachedGuilds = guilds.ToImmutableArray(); if (cachedGuilds.Length == 0) return; //Wait for unsynced guilds to sync first. - var unsyncedGuilds = guilds.Select(x => x.SyncPromise).Where(x => !x.IsCompleted).ToArray(); + var unsyncedGuilds = guilds.Select(x => x.SyncPromise).Where(x => !x.IsCompleted).ToImmutableArray(); if (unsyncedGuilds.Length > 0) - await Task.WhenAll(unsyncedGuilds); + await Task.WhenAll(unsyncedGuilds).ConfigureAwait(false); //Download offline members const short batchSize = 50; @@ -492,18 +448,8 @@ namespace Discord.WebSocket } } - public override Task> GetVoiceRegionsAsync() - { - return Task.FromResult>(_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 +462,7 @@ namespace Discord.WebSocket var data = (payload as JToken).ToObject(_serializer); _heartbeatTime = 0; - _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _cancelToken.Token, _clientLogger); + _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _cancelToken.Token, LogManager.ClientLogger); } break; case GatewayOpCode.Heartbeat: @@ -572,26 +518,26 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var dataStore = new DataStore(data.Guilds.Length, data.PrivateChannels.Length); + var state = new ClientState(data.Guilds.Length, data.PrivateChannels.Length); - var currentUser = new SocketSelfUser(this, data.User); + var currentUser = SocketSelfUser.Create(this, state, data.User); int unavailableGuilds = 0; for (int i = 0; i < data.Guilds.Length; i++) { var model = data.Guilds[i]; - var guild = AddGuild(model, dataStore); + var guild = AddGuild(model, state); if (!guild._available || ApiClient.AuthTokenType == TokenType.User) unavailableGuilds++; else await _guildAvailableEvent.InvokeAsync(guild).ConfigureAwait(false); } for (int i = 0; i < data.PrivateChannels.Length; i++) - AddPrivateChannel(data.PrivateChannels[i], dataStore); + AddPrivateChannel(data.PrivateChannels[i], state); _sessionId = data.SessionId; - _currentUser = currentUser; _unavailableGuilds = unavailableGuilds; - DataStore = dataStore; + CurrentUser = currentUser; + State = state; } catch (Exception ex) { @@ -603,7 +549,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); @@ -618,7 +564,7 @@ namespace Discord.WebSocket var _ = _connectTask.TrySetResultAsync(true); //Signal the .Connect() call to complete //Notify the client that these guilds are available again - foreach (var guild in DataStore.Guilds) + foreach (var guild in State.Guilds) { if (guild._available) await _guildAvailableEvent.InvokeAsync(guild).ConfigureAwait(false); @@ -639,10 +585,10 @@ namespace Discord.WebSocket _lastGuildAvailableTime = Environment.TickCount; await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_AVAILABLE)").ConfigureAwait(false); - var guild = DataStore.GetGuild(data.Id); + var guild = State.GetGuild(data.Id); if (guild != null) { - guild.Update(data, UpdateSource.WebSocket, DataStore); + guild.Update(State, data); var unavailableGuilds = _unavailableGuilds; if (unavailableGuilds != 0) @@ -659,7 +605,7 @@ namespace Discord.WebSocket { await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_CREATE)").ConfigureAwait(false); - var guild = AddGuild(data, DataStore); + var guild = AddGuild(data, State); if (guild != null) { if (ApiClient.AuthTokenType == TokenType.User) @@ -679,11 +625,11 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.Id); + var guild = State.GetGuild(data.Id); if (guild != null) { var before = guild.Clone(); - guild.Update(data, UpdateSource.WebSocket); + guild.Update(State, data); await _guildUpdatedEvent.InvokeAsync(before, guild).ConfigureAwait(false); } else @@ -698,11 +644,11 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_EMOJIS_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); + var guild = State.GetGuild(data.GuildId); if (guild != null) { var before = guild.Clone(); - guild.Update(data, UpdateSource.WebSocket); + guild.Update(State, data); await _guildUpdatedEvent.InvokeAsync(before, guild).ConfigureAwait(false); } else @@ -712,20 +658,15 @@ namespace Discord.WebSocket } } return; - case "GUILD_INTEGRATIONS_UPDATE": - { - await _gatewayLogger.DebugAsync("Ignored Dispatch (GUILD_INTEGRATIONS_UPDATE)").ConfigureAwait(false); - } - return; case "GUILD_SYNC": { await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_SYNC)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.Id); + var guild = State.GetGuild(data.Id); if (guild != null) { var before = guild.Clone(); - guild.Update(data, UpdateSource.WebSocket, DataStore); + guild.Update(State, data); //This is treated as an extension of GUILD_AVAILABLE _unavailableGuilds--; _lastGuildAvailableTime = Environment.TickCount; @@ -747,11 +688,9 @@ namespace Discord.WebSocket type = "GUILD_UNAVAILABLE"; await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_UNAVAILABLE)").ConfigureAwait(false); - var guild = DataStore.GetGuild(data.Id); + var guild = State.GetGuild(data.Id); if (guild != null) { - foreach (var member in guild.Members) - member.User.RemoveRef(this); await _guildUnavailableEvent.InvokeAsync(guild).ConfigureAwait(false); _unavailableGuilds++; } @@ -786,13 +725,13 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_CREATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - ISocketChannel channel = null; + SocketChannel channel = null; if (data.GuildId.IsSpecified) { - var guild = DataStore.GetGuild(data.GuildId.Value); + var guild = State.GetGuild(data.GuildId.Value); if (guild != null) { - channel = guild.AddChannel(data, DataStore); + channel = guild.AddChannel(State, data); if (!guild.IsSynced) { @@ -807,7 +746,7 @@ namespace Discord.WebSocket } } else - channel = AddPrivateChannel(data, DataStore); + channel = AddPrivateChannel(data, State) as SocketChannel; if (channel != null) await _channelCreatedEvent.InvokeAsync(channel).ConfigureAwait(false); @@ -818,13 +757,13 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = DataStore.GetChannel(data.Id); + var channel = State.GetChannel(data.Id); if (channel != null) { var before = channel.Clone(); - channel.Update(data, UpdateSource.WebSocket); + channel.Update(State, data); - if (!((channel as ISocketGuildChannel)?.Guild.IsSynced ?? true)) + if (!((channel as SocketGuildChannel)?.Guild.IsSynced ?? true)) { await _gatewayLogger.DebugAsync("Ignored CHANNEL_UPDATE, guild is not synced yet.").ConfigureAwait(false); return; @@ -843,14 +782,14 @@ namespace Discord.WebSocket { await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_DELETE)").ConfigureAwait(false); - ISocketChannel channel = null; + SocketChannel channel = null; var data = (payload as JToken).ToObject(_serializer); if (data.GuildId.IsSpecified) { - var guild = DataStore.GetGuild(data.GuildId.Value); + var guild = State.GetGuild(data.GuildId.Value); if (guild != null) { - channel = guild.RemoveChannel(data.Id); + channel = guild.RemoveChannel(State, data.Id); if (!guild.IsSynced) { @@ -865,7 +804,7 @@ namespace Discord.WebSocket } } else - channel = RemovePrivateChannel(data.Id); + channel = RemovePrivateChannel(data.Id) as SocketChannel; if (channel != null) await _channelDestroyedEvent.InvokeAsync(channel).ConfigureAwait(false); @@ -883,10 +822,10 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_ADD)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); + var guild = State.GetGuild(data.GuildId); if (guild != null) { - var user = guild.AddOrUpdateUser(data, DataStore); + var user = guild.AddOrUpdateUser(data); guild.MemberCount++; if (!guild.IsSynced) @@ -909,7 +848,7 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); + var guild = State.GetGuild(data.GuildId); if (guild != null) { var user = guild.GetUser(data.User.Id); @@ -923,7 +862,7 @@ namespace Discord.WebSocket if (user != null) { var before = user.Clone(); - user.Update(data, UpdateSource.WebSocket); + user.Update(State, data); await _userUpdatedEvent.InvokeAsync(before, user).ConfigureAwait(false); } else @@ -950,7 +889,7 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_REMOVE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); + var guild = State.GetGuild(data.GuildId); if (guild != null) { var user = guild.RemoveUser(data.User.Id); @@ -963,10 +902,7 @@ namespace Discord.WebSocket } if (user != null) - { - user.User.RemoveRef(this); await _userLeftEvent.InvokeAsync(user).ConfigureAwait(false); - } else { if (!guild.HasAllMembers) @@ -991,15 +927,15 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBERS_CHUNK)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); + var guild = State.GetGuild(data.GuildId); if (guild != null) { foreach (var memberModel in data.Members) - guild.AddOrUpdateUser(memberModel, DataStore); + guild.AddOrUpdateUser(memberModel); if (guild.DownloadedMemberCount >= guild.MemberCount) //Finished downloading for there { - guild.CompleteDownloadMembers(); + guild.CompleteDownloadUsers(); await _guildMembersDownloadedEvent.InvokeAsync(guild).ConfigureAwait(false); } } @@ -1015,10 +951,10 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_RECIPIENT_ADD)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = DataStore.GetChannel(data.ChannelId) as SocketGroupChannel; + var channel = State.GetChannel(data.ChannelId) as SocketGroupChannel; if (channel != null) { - var user = channel.AddUser(data.User, DataStore); + var user = channel.AddUser(data.User); await _recipientAddedEvent.InvokeAsync(user).ConfigureAwait(false); } else @@ -1033,15 +969,12 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_RECIPIENT_REMOVE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = DataStore.GetChannel(data.ChannelId) as SocketGroupChannel; + var channel = State.GetChannel(data.ChannelId) as SocketGroupChannel; if (channel != null) { var user = channel.RemoveUser(data.User.Id); if (user != null) - { - user.User.RemoveRef(this); await _recipientRemovedEvent.InvokeAsync(user).ConfigureAwait(false); - } else { await _gatewayLogger.WarningAsync("CHANNEL_RECIPIENT_REMOVE referenced an unknown user.").ConfigureAwait(false); @@ -1062,7 +995,7 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_CREATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); + var guild = State.GetGuild(data.GuildId); if (guild != null) { var role = guild.AddRole(data.Role); @@ -1086,14 +1019,14 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); + var guild = State.GetGuild(data.GuildId); if (guild != null) { var role = guild.GetRole(data.Role.Id); if (role != null) { var before = role.Clone(); - role.Update(data.Role, UpdateSource.WebSocket); + role.Update(State, data.Role); if (!guild.IsSynced) { @@ -1121,7 +1054,7 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_DELETE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); + var guild = State.GetGuild(data.GuildId); if (guild != null) { var role = guild.RemoveRole(data.RoleId); @@ -1155,7 +1088,7 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_BAN_ADD)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); + var guild = State.GetGuild(data.GuildId); if (guild != null) { if (!guild.IsSynced) @@ -1163,8 +1096,8 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Ignored GUILD_BAN_ADD, guild is not synced yet.").ConfigureAwait(false); return; } - - await _userBannedEvent.InvokeAsync(new User(data.User), guild).ConfigureAwait(false); + + await _userBannedEvent.InvokeAsync(SocketSimpleUser.Create(this, State, data.User), guild).ConfigureAwait(false); } else { @@ -1178,7 +1111,7 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_BAN_REMOVE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); + var guild = State.GetGuild(data.GuildId); if (guild != null) { if (!guild.IsSynced) @@ -1187,7 +1120,10 @@ namespace Discord.WebSocket return; } - await _userUnbannedEvent.InvokeAsync(new User(data.User), guild).ConfigureAwait(false); + SocketUser user = State.GetUser(data.User.Id); + if (user == null) + user = SocketSimpleUser.Create(this, State, data.User); + await _userUnbannedEvent.InvokeAsync(user, guild).ConfigureAwait(false); } else { @@ -1203,20 +1139,28 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_CREATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = DataStore.GetChannel(data.ChannelId) as ISocketMessageChannel; + var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; if (channel != null) { - if (!((channel as ISocketGuildChannel)?.Guild.IsSynced ?? true)) + var guild = (channel as SocketGuildChannel)?.Guild; + if (guild != null && !guild.IsSynced) { await _gatewayLogger.DebugAsync("Ignored MESSAGE_CREATE, guild is not synced yet.").ConfigureAwait(false); return; } - var author = channel.GetUser(data.Author.Value.Id, true); + SocketUser author; + if (guild != null) + author = guild.GetUser(data.Author.Value.Id); + else + author = (channel as SocketChannel).GetUser(data.Author.Value.Id); + if (author == null) + author = SocketSimpleUser.Create(this, State, data.Author.Value); if (author != null) { - var msg = channel.AddMessage(author, data); + var msg = SocketMessage.Create(this, State, author, channel, data); + SocketChannelHelper.AddMessage(channel, this, msg); await _messageReceivedEvent.InvokeAsync(msg).ConfigureAwait(false); } else @@ -1237,37 +1181,41 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = DataStore.GetChannel(data.ChannelId) as ISocketMessageChannel; + var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; if (channel != null) { - if (!((channel as ISocketGuildChannel)?.Guild.IsSynced ?? true)) + var guild = (channel as SocketGuildChannel)?.Guild; + if (guild != null && !guild.IsSynced) { await _gatewayLogger.DebugAsync("Ignored MESSAGE_UPDATE, guild is not synced yet.").ConfigureAwait(false); return; } - IMessage before = null, after = null; - ISocketMessage cachedMsg = channel.GetMessage(data.Id); + SocketMessage before = null, after = null; + SocketMessage cachedMsg = channel.GetCachedMessage(data.Id); if (cachedMsg != null) { before = cachedMsg.Clone(); - cachedMsg.Update(data, UpdateSource.WebSocket); + cachedMsg.Update(State, data); after = cachedMsg; } else if (data.Author.IsSpecified) { //Edited message isnt in cache, create a detached one - var author = channel.GetUser(data.Author.Value.Id, true); - if (author != null) - after = channel.CreateMessage(author, data); - } - if (after != null) - { - if (before == null) - await _messageUpdatedEvent.InvokeAsync(Optional.Create(), after).ConfigureAwait(false); + SocketUser author; + if (guild != null) + author = guild.GetUser(data.Author.Value.Id); else - await _messageUpdatedEvent.InvokeAsync(Optional.Create(before), after).ConfigureAwait(false); + author = (channel as SocketChannel).GetUser(data.Author.Value.Id); + if (author == null) + author = SocketSimpleUser.Create(this, State, data.Author.Value); + + after = SocketMessage.Create(this, State, author, channel, data); } + if (before != null) + await _messageUpdatedEvent.InvokeAsync(before, after).ConfigureAwait(false); + else + await _messageUpdatedEvent.InvokeAsync(Optional.Create(), after).ConfigureAwait(false); } else { @@ -1281,20 +1229,20 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = DataStore.GetChannel(data.ChannelId) as ISocketMessageChannel; + var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; if (channel != null) { - if (!((channel as ISocketGuildChannel)?.Guild.IsSynced ?? true)) + if (!((channel as SocketGuildChannel)?.Guild.IsSynced ?? true)) { await _gatewayLogger.DebugAsync("Ignored MESSAGE_DELETE, guild is not synced yet.").ConfigureAwait(false); return; } - var msg = channel.RemoveMessage(data.Id); + var msg = SocketChannelHelper.RemoveMessage(channel, this, data.Id); if (msg != null) - await _messageDeletedEvent.InvokeAsync(data.Id, Optional.Create(msg)).ConfigureAwait(false); + await _messageDeletedEvent.InvokeAsync(data.Id, msg).ConfigureAwait(false); else - await _messageDeletedEvent.InvokeAsync(data.Id, Optional.Create()).ConfigureAwait(false); + await _messageDeletedEvent.InvokeAsync(data.Id, Optional.Create()).ConfigureAwait(false); } else { @@ -1308,10 +1256,10 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE_BULK)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = DataStore.GetChannel(data.ChannelId) as ISocketMessageChannel; + var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; if (channel != null) { - if (!((channel as ISocketGuildChannel)?.Guild.IsSynced ?? true)) + if (!((channel as SocketGuildChannel)?.Guild.IsSynced ?? true)) { await _gatewayLogger.DebugAsync("Ignored MESSAGE_DELETE_BULK, guild is not synced yet.").ConfigureAwait(false); return; @@ -1319,11 +1267,11 @@ namespace Discord.WebSocket foreach (var id in data.Ids) { - var msg = channel.RemoveMessage(id); + var msg = SocketChannelHelper.RemoveMessage(channel, this, id); if (msg != null) - await _messageDeletedEvent.InvokeAsync(id, Optional.Create(msg)).ConfigureAwait(false); + await _messageDeletedEvent.InvokeAsync(id, msg).ConfigureAwait(false); else - await _messageDeletedEvent.InvokeAsync(id, Optional.Create()).ConfigureAwait(false); + await _messageDeletedEvent.InvokeAsync(id, Optional.Create()).ConfigureAwait(false); } } else @@ -1342,39 +1290,43 @@ namespace Discord.WebSocket var data = (payload as JToken).ToObject(_serializer); if (data.GuildId.IsSpecified) { - var guild = DataStore.GetGuild(data.GuildId.Value); + var guild = State.GetGuild(data.GuildId.Value); if (guild == null) { await _gatewayLogger.WarningAsync("PRESENCE_UPDATE referenced an unknown guild.").ConfigureAwait(false); break; } - if (!guild.IsSynced) { await _gatewayLogger.DebugAsync("Ignored PRESENCE_UPDATE, guild is not synced yet.").ConfigureAwait(false); return; } - IPresence before; - var user = guild.GetUser(data.User.Id); + SocketPresence before; + SocketUser user = guild.GetUser(data.User.Id); if (user != null) { before = user.Presence.Clone(); - user.Update(data, UpdateSource.WebSocket); + user.Update(State, data); } else { - before = new Presence(null, UserStatus.Offline); - user = guild.AddOrUpdateUser(data, DataStore); + before = new SocketPresence(UserStatus.Offline, null); + user = guild.AddOrUpdateUser(data); } - await _userPresenceUpdatedEvent.InvokeAsync(user, before, user).ConfigureAwait(false); + await _userPresenceUpdatedEvent.InvokeAsync(user, before, user.Presence).ConfigureAwait(false); } else { - var channel = DataStore.GetDMChannel(data.User.Id); + var channel = State.GetChannel(data.User.Id); if (channel != null) - channel.Recipient.Update(data, UpdateSource.WebSocket); + { + var user = channel.GetUser(data.User.Id); + var before = user.Presence.Clone(); + user.Update(State, data); + await _userPresenceUpdatedEvent.InvokeAsync(user, before, user.Presence).ConfigureAwait(false); + } } } break; @@ -1383,16 +1335,16 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (TYPING_START)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = DataStore.GetChannel(data.ChannelId) as ISocketMessageChannel; + var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; if (channel != null) { - if (!((channel as ISocketGuildChannel)?.Guild.IsSynced ?? true)) + if (!((channel as SocketGuildChannel)?.Guild.IsSynced ?? true)) { await _gatewayLogger.DebugAsync("Ignored TYPING_START, guild is not synced yet.").ConfigureAwait(false); return; } - var user = channel.GetUser(data.UserId, true); + var user = (channel as SocketChannel).GetUser(data.UserId); if (user != null) await _userIsTypingEvent.InvokeAsync(user, channel).ConfigureAwait(false); } @@ -1408,7 +1360,7 @@ namespace Discord.WebSocket if (data.Id == CurrentUser.Id) { var before = CurrentUser.Clone(); - CurrentUser.Update(data, UpdateSource.WebSocket); + CurrentUser.Update(State, data); await _selfUpdatedEvent.InvokeAsync(before, CurrentUser).ConfigureAwait(false); } else @@ -1427,56 +1379,53 @@ namespace Discord.WebSocket var data = (payload as JToken).ToObject(_serializer); if (data.GuildId.HasValue) { - ISocketUser user; - VoiceState before, after; + SocketUser user; + SocketVoiceState before, after; if (data.GuildId != null) { - var guild = DataStore.GetGuild(data.GuildId.Value); - if (guild != null) + var guild = State.GetGuild(data.GuildId.Value); + if (guild == null) { - if (!guild.IsSynced) - { - await _gatewayLogger.DebugAsync("Ignored VOICE_STATE_UPDATE, guild is not synced yet.").ConfigureAwait(false); - return; - } + await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown guild.").ConfigureAwait(false); + return; + } + else if (!guild.IsSynced) + { + await _gatewayLogger.DebugAsync("Ignored VOICE_STATE_UPDATE, guild is not synced yet.").ConfigureAwait(false); + return; + } - if (data.ChannelId != null) - { - before = guild.GetVoiceState(data.UserId)?.Clone() ?? new VoiceState(null, null, false, false, false); - after = guild.AddOrUpdateVoiceState(data, DataStore); - if (data.UserId == _currentUser.Id) - { - var _ = guild.FinishJoinAudioChannel().ConfigureAwait(false); - } - } - else + if (data.ChannelId != null) + { + before = guild.GetVoiceState(data.UserId)?.Clone() ?? new SocketVoiceState(null, null, false, false, false); + after = guild.AddOrUpdateVoiceState(State, data); + if (data.UserId == CurrentUser.Id) { - before = guild.RemoveVoiceState(data.UserId) ?? new VoiceState(null, null, false, false, false); - after = new VoiceState(null, data); + var _ = guild.FinishJoinAudioChannel().ConfigureAwait(false); } - - user = guild.GetUser(data.UserId); } else { - await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown guild.").ConfigureAwait(false); - return; + before = guild.RemoveVoiceState(data.UserId) ?? new SocketVoiceState(null, null, false, false, false); + after = SocketVoiceState.Create(null, data); } + + user = guild.GetUser(data.UserId); } else { - var groupChannel = DataStore.GetChannel(data.ChannelId.Value) as SocketGroupChannel; + var groupChannel = State.GetChannel(data.ChannelId.Value) as SocketGroupChannel; if (groupChannel != null) { if (data.ChannelId != null) { - before = groupChannel.GetVoiceState(data.UserId)?.Clone() ?? new VoiceState(null, null, false, false, false); - after = groupChannel.AddOrUpdateVoiceState(data, DataStore); + before = groupChannel.GetVoiceState(data.UserId)?.Clone() ?? new SocketVoiceState(null, null, false, false, false); + after = groupChannel.AddOrUpdateVoiceState(State, data); } 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 = SocketVoiceState.Create(null, data); } user = groupChannel.GetUser(data.UserId); } @@ -1503,7 +1452,7 @@ namespace Discord.WebSocket if (AudioMode != AudioMode.Disabled) { var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); + var guild = State.GetGuild(data.GuildId); if (guild != null) { string endpoint = data.Endpoint.Substring(0, data.Endpoint.LastIndexOf(':')); @@ -1515,22 +1464,27 @@ namespace Discord.WebSocket return; } } - return; //Ignored (User only) case "CHANNEL_PINS_ACK": - await _gatewayLogger.DebugAsync("Ignored Dispatch (CHANNEL_PINS_ACK)"); + await _gatewayLogger.DebugAsync("Ignored Dispatch (CHANNEL_PINS_ACK)").ConfigureAwait(false); break; case "CHANNEL_PINS_UPDATE": - await _gatewayLogger.DebugAsync("Ignored Dispatch (CHANNEL_PINS_UPDATE)"); + await _gatewayLogger.DebugAsync("Ignored Dispatch (CHANNEL_PINS_UPDATE)").ConfigureAwait(false); break; - case "USER_SETTINGS_UPDATE": - await _gatewayLogger.DebugAsync("Ignored Dispatch (USER_SETTINGS_UPDATE)").ConfigureAwait(false); + case "GUILD_INTEGRATIONS_UPDATE": + await _gatewayLogger.DebugAsync("Ignored Dispatch (GUILD_INTEGRATIONS_UPDATE)").ConfigureAwait(false); return; case "MESSAGE_ACK": await _gatewayLogger.DebugAsync("Ignored Dispatch (MESSAGE_ACK)").ConfigureAwait(false); return; + case "USER_SETTINGS_UPDATE": + await _gatewayLogger.DebugAsync("Ignored Dispatch (USER_SETTINGS_UPDATE)").ConfigureAwait(false); + return; + case "WEBHOOKS_UPDATE": + await _gatewayLogger.DebugAsync("Ignored Dispatch (WEBHOOKS_UPDATE)").ConfigureAwait(false); + return; //Others default: @@ -1548,18 +1502,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 { @@ -1599,7 +1544,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 @@ -1620,9 +1565,84 @@ namespace Discord.WebSocket } private async Task SyncGuildsAsync() { - var guildIds = Guilds.Where(x => !x.IsSynced).Select(x => x.Id).ToArray(); + var guildIds = Guilds.Where(x => !x.IsSynced).Select(x => x.Id).ToImmutableArray(); if (guildIds.Length > 0) await ApiClient.SendGuildSyncAsync(guildIds).ConfigureAwait(false); } + + internal SocketGuild AddGuild(ExtendedGuild model, ClientState state) + { + var guild = SocketGuild.Create(this, state, model); + state.AddGuild(guild); + if (model.Large) + _largeGuilds.Enqueue(model.Id); + return guild; + } + internal SocketGuild RemoveGuild(ulong id) + { + var guild = State.RemoveGuild(id); + if (guild != null) + { + foreach (var channel in guild.Channels) + State.RemoveChannel(id); + foreach (var user in guild.Users) + user.GlobalUser.RemoveRef(this); + } + return guild; + } + + internal ISocketPrivateChannel AddPrivateChannel(API.Channel model, ClientState state) + { + var channel = SocketChannel.CreatePrivate(this, state, model); + state.AddChannel(channel as SocketChannel); + return channel; + } + internal ISocketPrivateChannel RemovePrivateChannel(ulong id) + { + var channel = State.RemoveChannel(id) as ISocketPrivateChannel; + if (channel != null) + { + foreach (var recipient in channel.Recipients) + recipient.GlobalUser.RemoveRef(this); + } + return channel; + } + + //IDiscordClient + DiscordRestApiClient IDiscordClient.ApiClient => ApiClient; + + Task IDiscordClient.ConnectAsync() + => ConnectAsync(); + + async Task IDiscordClient.GetApplicationInfoAsync() + => await GetApplicationInfoAsync().ConfigureAwait(false); + + Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode) + => Task.FromResult(GetChannel(id)); + Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode) + => Task.FromResult>(PrivateChannels); + + async Task> IDiscordClient.GetConnectionsAsync() + => await GetConnectionsAsync().ConfigureAwait(false); + + async Task IDiscordClient.GetInviteAsync(string inviteId) + => await GetInviteAsync(inviteId).ConfigureAwait(false); + + Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode) + => Task.FromResult(GetGuild(id)); + Task> IDiscordClient.GetGuildsAsync(CacheMode mode) + => Task.FromResult>(Guilds); + async Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon) + => await CreateGuildAsync(name, region, jpegIcon).ConfigureAwait(false); + + Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode) + => Task.FromResult(GetUser(id)); + Task IDiscordClient.GetUserAsync(string username, string discriminator) + => Task.FromResult(GetUser(username, discriminator)); + + Task> IDiscordClient.GetVoiceRegionsAsync() + => Task.FromResult>(_voiceRegions.ToReadOnlyCollection()); + Task IDiscordClient.GetVoiceRegionAsync(string id) + => Task.FromResult(GetVoiceRegion(id)); } } diff --git a/src/Discord.Net/WebSocket/DiscordSocketConfig.cs b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs similarity index 100% rename from src/Discord.Net/WebSocket/DiscordSocketConfig.cs rename to src/Discord.Net.WebSocket/DiscordSocketConfig.cs diff --git a/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs new file mode 100644 index 000000000..7056a4df5 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs @@ -0,0 +1,6 @@ +namespace Discord.WebSocket +{ + public interface ISocketAudioChannel : IAudioChannel + { + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs new file mode 100644 index 000000000..7ba08544b --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs @@ -0,0 +1,30 @@ +using Discord.Rest; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace Discord.WebSocket +{ + public interface ISocketMessageChannel : IMessageChannel + { + /// Gets all messages in this channel's cache. + IReadOnlyCollection CachedMessages { get; } + + /// Sends a message to this message channel. + new Task SendMessageAsync(string text, bool isTTS = false, RequestOptions options = null); + /// Sends a file to this text channel, with an optional caption. + new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, RequestOptions options = null); + /// Sends a file to this text channel, with an optional caption. + new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, RequestOptions options = null); + + SocketMessage GetCachedMessage(ulong id); + /// Gets the last N messages from this message channel. + IReadOnlyCollection GetCachedMessages(int limit = DiscordConfig.MaxMessagesPerBatch); + /// Gets a collection of messages in this channel. + IReadOnlyCollection GetCachedMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch); + /// Gets a collection of messages in this channel. + IReadOnlyCollection GetCachedMessages(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch); + /// Gets a collection of pinned messages in this channel. + new Task> GetPinnedMessagesAsync(RequestOptions options = null); + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Channels/ISocketPrivateChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/ISocketPrivateChannel.cs new file mode 100644 index 000000000..4e91673dd --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketPrivateChannel.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Discord.WebSocket +{ + public interface ISocketPrivateChannel : IPrivateChannel + { + new IReadOnlyCollection Recipients { get; } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs new file mode 100644 index 000000000..f982e66b5 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +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, IChannel + { + public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public IReadOnlyCollection Users => GetUsersInternal(); + + internal SocketChannel(DiscordSocketClient discord, ulong id) + : base(discord, id) + { + } + internal static ISocketPrivateChannel CreatePrivate(DiscordSocketClient discord, ClientState state, Model model) + { + switch (model.Type) + { + case ChannelType.DM: + return SocketDMChannel.Create(discord, state, model); + case ChannelType.Group: + return SocketGroupChannel.Create(discord, state, model); + default: + throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); + } + } + internal abstract void Update(ClientState state, Model model); + + //User + public SocketUser GetUser(ulong id) => GetUserInternal(id); + internal abstract SocketUser GetUserInternal(ulong id); + internal abstract IReadOnlyCollection GetUsersInternal(); + + internal SocketChannel Clone() => MemberwiseClone() as SocketChannel; + + //IChannel + string IChannel.Name => null; + + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(null); //Overridden + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + => AsyncEnumerable.Empty>(); //Overridden + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs new file mode 100644 index 000000000..f91ab4c2d --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs @@ -0,0 +1,87 @@ +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Discord.WebSocket +{ + internal static class SocketChannelHelper + { + public static IAsyncEnumerable> GetMessagesAsync(SocketChannel channel, DiscordSocketClient discord, MessageCache messages, + ulong? fromMessageId, Direction dir, int limit, CacheMode mode, IGuild guild, RequestOptions options) + { + if (dir == Direction.Around) + throw new NotImplementedException(); //TODO: Impl + + IReadOnlyCollection cachedMessages = null; + IAsyncEnumerable> result = null; + + if (dir == Direction.After && fromMessageId == null) + return AsyncEnumerable.Empty>(); + + if (dir == Direction.Before || mode == CacheMode.CacheOnly) + { + if (messages != null) //Cache enabled + cachedMessages = messages.GetMany(fromMessageId, dir, limit); + else + cachedMessages = ImmutableArray.Create(); + result = ImmutableArray.Create(cachedMessages).ToAsyncEnumerable>(); + } + + if (dir == Direction.Before) + { + limit -= cachedMessages.Count; + if (mode == CacheMode.CacheOnly || limit <= 0) + return result; + + //Download remaining messages + var downloadedMessages = ChannelHelper.GetMessagesAsync(channel, discord, cachedMessages.Min(x => x.Id), dir, limit, guild, options); + return result.Concat(downloadedMessages); + } + else + { + if (mode == CacheMode.CacheOnly) + return result; + + //Dont use cache in this case + return ChannelHelper.GetMessagesAsync(channel, discord, fromMessageId, dir, limit, guild, options); + } + } + public static IReadOnlyCollection GetCachedMessages(SocketChannel channel, DiscordSocketClient discord, MessageCache messages, + ulong? fromMessageId, Direction dir, int limit) + { + if (messages != null) //Cache enabled + return messages.GetMany(fromMessageId, dir, limit); + else + return ImmutableArray.Create(); + } + + public static void AddMessage(ISocketMessageChannel channel, DiscordSocketClient discord, + SocketMessage msg) + { + //TODO: C#7 Candidate for pattern matching + if (channel is SocketDMChannel) + (channel as SocketDMChannel).AddMessage(msg); + else if (channel is SocketGroupChannel) + (channel as SocketGroupChannel).AddMessage(msg); + else if (channel is SocketTextChannel) + (channel as SocketTextChannel).AddMessage(msg); + else + throw new NotSupportedException("Unexpected ISocketMessageChannel type"); + } + public static SocketMessage RemoveMessage(ISocketMessageChannel channel, DiscordSocketClient discord, + ulong id) + { + //TODO: C#7 Candidate for pattern matching + if (channel is SocketDMChannel) + return (channel as SocketDMChannel).RemoveMessage(id); + else if (channel is SocketGroupChannel) + return (channel as SocketGroupChannel).RemoveMessage(id); + else if (channel is SocketTextChannel) + return (channel as SocketTextChannel).RemoveMessage(id); + else + throw new NotSupportedException("Unexpected ISocketMessageChannel type"); + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs new file mode 100644 index 000000000..1e20f12d8 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -0,0 +1,150 @@ +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 Model = Discord.API.Channel; + +namespace Discord.WebSocket +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketDMChannel : SocketChannel, IDMChannel, ISocketPrivateChannel, ISocketMessageChannel + { + private readonly MessageCache _messages; + + public SocketUser Recipient { get; private set; } + + public IReadOnlyCollection CachedMessages => _messages?.Messages ?? ImmutableArray.Create(); + public new IReadOnlyCollection Users => ImmutableArray.Create(Discord.CurrentUser, Recipient); + + internal SocketDMChannel(DiscordSocketClient discord, ulong id, SocketGlobalUser recipient) + : base(discord, id) + { + Recipient = recipient; + if (Discord.MessageCacheSize > 0) + _messages = new MessageCache(Discord, this); + } + internal static SocketDMChannel Create(DiscordSocketClient discord, ClientState state, Model model) + { + var entity = new SocketDMChannel(discord, model.Id, discord.GetOrCreateUser(state, model.Recipients.Value[0])); + entity.Update(state, model); + return entity; + } + internal override void Update(ClientState state, Model model) + { + Recipient.Update(state, model.Recipients.Value[0]); + } + + public Task CloseAsync(RequestOptions options = null) + => ChannelHelper.DeleteAsync(this, Discord, options); + + //Messages + public SocketMessage GetCachedMessage(ulong id) + => _messages?.Get(id); + public async Task GetMessageAsync(ulong id, RequestOptions options = null) + { + IMessage msg = _messages?.Get(id); + if (msg == null) + msg = await ChannelHelper.GetMessageAsync(this, Discord, id, null, options).ConfigureAwait(false); + return msg; + } + public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, CacheMode.AllowDownload, null, options); + public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, CacheMode.AllowDownload, null, options); + public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, CacheMode.AllowDownload, null, options); + public IReadOnlyCollection GetCachedMessages(int limit = DiscordConfig.MaxMessagesPerBatch) + => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, null, Direction.Before, limit); + public IReadOnlyCollection GetCachedMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessageId, dir, limit); + public IReadOnlyCollection GetCachedMessages(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessage.Id, dir, limit); + public Task> GetPinnedMessagesAsync(RequestOptions options = null) + => ChannelHelper.GetPinnedMessagesAsync(this, Discord, null, options); + + public Task SendMessageAsync(string text, bool isTTS, RequestOptions options = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, null, options); + public Task SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, null, options); + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, null, options); + + public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + + public Task TriggerTypingAsync(RequestOptions options = null) + => ChannelHelper.TriggerTypingAsync(this, Discord, options); + public IDisposable EnterTypingState(RequestOptions options = null) + => ChannelHelper.EnterTypingState(this, Discord, options); + + internal void AddMessage(SocketMessage msg) + => _messages?.Add(msg); + internal SocketMessage RemoveMessage(ulong id) + => _messages?.Remove(id); + + //Users + public new SocketUser GetUser(ulong id) + { + if (id == Recipient.Id) + return Recipient; + else if (id == Discord.CurrentUser.Id) + return Discord.CurrentUser as SocketSelfUser; + else + return null; + } + + public override string ToString() => $"@{Recipient}"; + private string DebuggerDisplay => $"@{Recipient} ({Id}, DM)"; + internal new SocketDMChannel Clone() => MemberwiseClone() as SocketDMChannel; + + //SocketChannel + internal override IReadOnlyCollection GetUsersInternal() => Users; + internal override SocketUser GetUserInternal(ulong id) => GetUser(id); + + //IDMChannel + IUser IDMChannel.Recipient => Recipient; + + //ISocketPrivateChannel + IReadOnlyCollection ISocketPrivateChannel.Recipients => ImmutableArray.Create(Recipient); + + //IPrivateChannel + IReadOnlyCollection IPrivateChannel.Recipients => ImmutableArray.Create(Recipient); + + //IMessageChannel + async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetMessageAsync(id, options).ConfigureAwait(false); + else + return GetCachedMessage(id); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, mode, null, options); + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, mode, null, options); + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, mode, null, options); + async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) + => await GetPinnedMessagesAsync(options).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) + => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) + => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, RequestOptions options) + => await SendMessageAsync(text, isTTS, options).ConfigureAwait(false); + IDisposable IMessageChannel.EnterTypingState(RequestOptions options) + => EnterTypingState(options); + + //IChannel + string IChannel.Name => $"@{Recipient}"; + + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetUser(id)); + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs new file mode 100644 index 000000000..450460d8b --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -0,0 +1,211 @@ +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 Model = Discord.API.Channel; +using UserModel = Discord.API.User; +using VoiceStateModel = Discord.API.VoiceState; + +namespace Discord.WebSocket +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketGroupChannel : SocketChannel, IGroupChannel, ISocketPrivateChannel, ISocketMessageChannel, ISocketAudioChannel + { + private readonly MessageCache _messages; + + private string _iconId; + private ConcurrentDictionary _users; + private ConcurrentDictionary _voiceStates; + + public string Name { get; private set; } + + public IReadOnlyCollection CachedMessages => _messages?.Messages ?? ImmutableArray.Create(); + public new IReadOnlyCollection Users => _users.ToReadOnlyCollection(); + public IReadOnlyCollection 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); + _voiceStates = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, 5); + _users = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, 5); + } + internal static SocketGroupChannel Create(DiscordSocketClient discord, ClientState state, Model model) + { + var entity = new SocketGroupChannel(discord, model.Id); + entity.Update(state, model); + return entity; + } + internal override void Update(ClientState state, Model model) + { + if (model.Name.IsSpecified) + Name = model.Name.Value; + if (model.Icon.IsSpecified) + _iconId = model.Icon.Value; + + if (model.Recipients.IsSpecified) + UpdateUsers(state, model.Recipients.Value); + } + private void UpdateUsers(ClientState state, UserModel[] models) + { + var users = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(models.Length * 1.05)); + for (int i = 0; i < models.Length; i++) + users[models[i].Id] = SocketGroupUser.Create(this, state, models[i]); + _users = users; + } + + public Task LeaveAsync(RequestOptions options = null) + => ChannelHelper.DeleteAsync(this, Discord, options); + + //Messages + public SocketMessage GetCachedMessage(ulong id) + => _messages?.Get(id); + public async Task GetMessageAsync(ulong id, RequestOptions options = null) + { + IMessage msg = _messages?.Get(id); + if (msg == null) + msg = await ChannelHelper.GetMessageAsync(this, Discord, id, null, options).ConfigureAwait(false); + return msg; + } + public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, CacheMode.AllowDownload, null, options); + public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, CacheMode.AllowDownload, null, options); + public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, CacheMode.AllowDownload, null, options); + public IReadOnlyCollection GetCachedMessages(int limit = DiscordConfig.MaxMessagesPerBatch) + => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, null, Direction.Before, limit); + public IReadOnlyCollection GetCachedMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessageId, dir, limit); + public IReadOnlyCollection GetCachedMessages(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessage.Id, dir, limit); + public Task> GetPinnedMessagesAsync(RequestOptions options = null) + => ChannelHelper.GetPinnedMessagesAsync(this, Discord, null, options); + + public Task SendMessageAsync(string text, bool isTTS, RequestOptions options = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, null, options); + public Task SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, null, options); + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, null, options); + + public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + + public Task TriggerTypingAsync(RequestOptions options = null) + => ChannelHelper.TriggerTypingAsync(this, Discord, options); + public IDisposable EnterTypingState(RequestOptions options = null) + => ChannelHelper.EnterTypingState(this, Discord, options); + + internal void AddMessage(SocketMessage msg) + => _messages?.Add(msg); + internal SocketMessage RemoveMessage(ulong id) + => _messages?.Remove(id); + + //Users + public new SocketGroupUser GetUser(ulong id) + { + SocketGroupUser user; + if (_users.TryGetValue(id, out user)) + return user; + return null; + } + internal SocketGroupUser AddUser(UserModel model) + { + SocketGroupUser user; + if (_users.TryGetValue(model.Id, out user)) + return user as SocketGroupUser; + else + { + var privateUser = SocketGroupUser.Create(this, Discord.State, model); + _users[privateUser.Id] = privateUser; + return privateUser; + } + } + internal SocketGroupUser RemoveUser(ulong id) + { + SocketGroupUser user; + if (_users.TryRemove(id, out user)) + { + user.GlobalUser.RemoveRef(Discord); + return user as SocketGroupUser; + } + return null; + } + + //Voice States + internal SocketVoiceState AddOrUpdateVoiceState(ClientState state, VoiceStateModel model) + { + var voiceChannel = state.GetChannel(model.ChannelId.Value) as SocketVoiceChannel; + var voiceState = SocketVoiceState.Create(voiceChannel, model); + _voiceStates[model.UserId] = voiceState; + return voiceState; + } + internal SocketVoiceState? GetVoiceState(ulong id) + { + SocketVoiceState voiceState; + if (_voiceStates.TryGetValue(id, out voiceState)) + return voiceState; + return null; + } + internal SocketVoiceState? RemoveVoiceState(ulong id) + { + SocketVoiceState voiceState; + if (_voiceStates.TryRemove(id, out voiceState)) + return voiceState; + return null; + } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id}, Group)"; + internal new SocketGroupChannel Clone() => MemberwiseClone() as SocketGroupChannel; + + //SocketChannel + internal override IReadOnlyCollection GetUsersInternal() => Users; + internal override SocketUser GetUserInternal(ulong id) => GetUser(id); + + //ISocketPrivateChannel + IReadOnlyCollection ISocketPrivateChannel.Recipients => Recipients; + + //IPrivateChannel + IReadOnlyCollection IPrivateChannel.Recipients => Recipients; + + //IMessageChannel + async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetMessageAsync(id, options).ConfigureAwait(false); + else + return GetCachedMessage(id); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, mode, null, options); + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, mode, null, options); + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, mode, null, options); + async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) + => await GetPinnedMessagesAsync(options).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) + => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) + => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, RequestOptions options) + => await SendMessageAsync(text, isTTS, options).ConfigureAwait(false); + IDisposable IMessageChannel.EnterTypingState(RequestOptions options) + => EnterTypingState(options); + + //IChannel + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetUser(id)); + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs new file mode 100644 index 000000000..4253b3c51 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -0,0 +1,160 @@ +using Discord.API.Rest; +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 +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public abstract class SocketGuildChannel : SocketChannel, IGuildChannel + { + private ImmutableArray _overwrites; + + public SocketGuild Guild { get; } + public string Name { get; private set; } + public int Position { get; private set; } + + public IReadOnlyCollection PermissionOverwrites => _overwrites; + public new abstract IReadOnlyCollection Users { get; } + + internal SocketGuildChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) + : base(discord, id) + { + Guild = guild; + } + internal static SocketGuildChannel Create(SocketGuild guild, ClientState state, Model model) + { + switch (model.Type) + { + case ChannelType.Text: + return SocketTextChannel.Create(guild, state, model); + case ChannelType.Voice: + return SocketVoiceChannel.Create(guild, state, model); + default: + throw new InvalidOperationException("Unknown guild channel type"); + } + } + internal override void Update(ClientState state, Model model) + { + Name = model.Name.Value; + Position = model.Position.Value; + + var overwrites = model.PermissionOverwrites.Value; + var newOverwrites = ImmutableArray.CreateBuilder(overwrites.Length); + for (int i = 0; i < overwrites.Length; i++) + newOverwrites.Add(new Overwrite(overwrites[i])); + _overwrites = newOverwrites.ToImmutable(); + } + + public Task ModifyAsync(Action func, RequestOptions options = null) + => ChannelHelper.ModifyAsync(this, Discord, func, options); + public Task DeleteAsync(RequestOptions options = null) + => ChannelHelper.DeleteAsync(this, Discord, options); + + public OverwritePermissions? GetPermissionOverwrite(IUser user) + { + for (int i = 0; i < _overwrites.Length; i++) + { + if (_overwrites[i].TargetId == user.Id) + return _overwrites[i].Permissions; + } + return null; + } + public OverwritePermissions? GetPermissionOverwrite(IRole role) + { + 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, RequestOptions options = null) + { + await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, user, perms, options).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, RequestOptions options = null) + { + await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, role, perms, options).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, RequestOptions options = null) + { + await ChannelHelper.RemovePermissionOverwriteAsync(this, Discord, user, options).ConfigureAwait(false); + + for (int i = 0; i < _overwrites.Length; i++) + { + if (_overwrites[i].TargetId == user.Id) + { + _overwrites = _overwrites.RemoveAt(i); + return; + } + } + } + public async Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) + { + await ChannelHelper.RemovePermissionOverwriteAsync(this, Discord, role, options).ConfigureAwait(false); + + for (int i = 0; i < _overwrites.Length; i++) + { + if (_overwrites[i].TargetId == role.Id) + { + _overwrites = _overwrites.RemoveAt(i); + return; + } + } + } + + public async Task> GetInvitesAsync(RequestOptions options = null) + => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); + public async Task CreateInviteAsync(int? maxAge = 3600, int? maxUses = null, bool isTemporary = true, RequestOptions options = null) + => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, options).ConfigureAwait(false); + + public new abstract SocketGuildUser GetUser(ulong id); + + public override string ToString() => Name; + internal new SocketGuildChannel Clone() => MemberwiseClone() as SocketGuildChannel; + + //SocketChannel + internal override IReadOnlyCollection GetUsersInternal() => Users; + internal override SocketUser GetUserInternal(ulong id) => GetUser(id); + + //IGuildChannel + ulong IGuildChannel.GuildId => Guild.Id; + + async Task> IGuildChannel.GetInvitesAsync(RequestOptions options) + => await GetInvitesAsync(options).ConfigureAwait(false); + async Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, RequestOptions options) + => await CreateInviteAsync(maxAge, maxUses, isTemporary, options).ConfigureAwait(false); + + OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IRole role) + => GetPermissionOverwrite(role); + OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IUser user) + => GetPermissionOverwrite(user); + async Task IGuildChannel.AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options) + => await AddPermissionOverwriteAsync(role, permissions, options).ConfigureAwait(false); + async Task IGuildChannel.AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options) + => await AddPermissionOverwriteAsync(user, permissions, options).ConfigureAwait(false); + async Task IGuildChannel.RemovePermissionOverwriteAsync(IRole role, RequestOptions options) + => await RemovePermissionOverwriteAsync(role, options).ConfigureAwait(false); + async Task IGuildChannel.RemovePermissionOverwriteAsync(IUser user, RequestOptions options) + => await RemovePermissionOverwriteAsync(user, options).ConfigureAwait(false); + + IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetUser(id)); + + //IChannel + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + => ImmutableArray.Create>(Users).ToAsyncEnumerable(); //Overriden in Text/Voice + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetUser(id)); //Overriden in Text/Voice + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs new file mode 100644 index 000000000..dddecbad5 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -0,0 +1,143 @@ +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 Model = Discord.API.Channel; + +namespace Discord.WebSocket +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketTextChannel : SocketGuildChannel, ITextChannel, ISocketMessageChannel + { + private readonly MessageCache _messages; + + public string Topic { get; private set; } + + public string Mention => MentionUtils.MentionChannel(Id); + public IReadOnlyCollection CachedMessages => _messages?.Messages ?? ImmutableArray.Create(); + public override IReadOnlyCollection Users + => Guild.Users.Where(x => Permissions.GetValue( + Permissions.ResolveChannel(Guild, x, this, Permissions.ResolveGuild(Guild, x)), + ChannelPermission.ReadMessages)).ToImmutableArray(); + + internal SocketTextChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) + : base(discord, id, guild) + { + if (Discord.MessageCacheSize > 0) + _messages = new MessageCache(Discord, this); + } + internal new static SocketTextChannel Create(SocketGuild guild, ClientState state, Model model) + { + var entity = new SocketTextChannel(guild.Discord, model.Id, guild); + entity.Update(state, model); + return entity; + } + internal override void Update(ClientState state, Model model) + { + base.Update(state, model); + + Topic = model.Topic.Value; + } + + public Task ModifyAsync(Action func, RequestOptions options = null) + => ChannelHelper.ModifyAsync(this, Discord, func, options); + + //Messages + public SocketMessage GetCachedMessage(ulong id) + => _messages?.Get(id); + public async Task GetMessageAsync(ulong id, RequestOptions options = null) + { + IMessage msg = _messages?.Get(id); + if (msg == null) + msg = await ChannelHelper.GetMessageAsync(this, Discord, id, Guild, options).ConfigureAwait(false); + return msg; + } + public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, CacheMode.AllowDownload, Guild, options); + public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, CacheMode.AllowDownload, Guild, options); + public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, CacheMode.AllowDownload, Guild, options); + public IReadOnlyCollection GetCachedMessages(int limit = DiscordConfig.MaxMessagesPerBatch) + => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, null, Direction.Before, limit); + public IReadOnlyCollection GetCachedMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessageId, dir, limit); + public IReadOnlyCollection GetCachedMessages(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessage.Id, dir, limit); + public Task> GetPinnedMessagesAsync(RequestOptions options = null) + => ChannelHelper.GetPinnedMessagesAsync(this, Discord, Guild, options); + + public Task SendMessageAsync(string text, bool isTTS, RequestOptions options = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, Guild, options); + public Task SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, Guild, options); + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, Guild, options); + + public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + + public Task TriggerTypingAsync(RequestOptions options = null) + => ChannelHelper.TriggerTypingAsync(this, Discord, options); + public IDisposable EnterTypingState(RequestOptions options = null) + => ChannelHelper.EnterTypingState(this, Discord, options); + + internal void AddMessage(SocketMessage msg) + => _messages?.Add(msg); + internal SocketMessage RemoveMessage(ulong id) + => _messages?.Remove(id); + + //Users + public override SocketGuildUser GetUser(ulong id) + { + var user = Guild.GetUser(id); + if (user != null) + { + var guildPerms = Permissions.ResolveGuild(Guild, user); + var channelPerms = Permissions.ResolveChannel(Guild, user, this, guildPerms); + if (Permissions.GetValue(channelPerms, ChannelPermission.ReadMessages)) + return user; + } + return null; + } + + private string DebuggerDisplay => $"{Name} ({Id}, Text)"; + internal new SocketTextChannel Clone() => MemberwiseClone() as SocketTextChannel; + + //IGuildChannel + Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetUser(id)); + IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + + //IMessageChannel + async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetMessageAsync(id, options).ConfigureAwait(false); + else + return GetCachedMessage(id); + } + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, mode, Guild, options); + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, mode, Guild, options); + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, mode, Guild, options); + async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) + => await GetPinnedMessagesAsync(options).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) + => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) + => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, RequestOptions options) + => await SendMessageAsync(text, isTTS, options).ConfigureAwait(false); + IDisposable IMessageChannel.EnterTypingState(RequestOptions options) + => EnterTypingState(options); + } +} \ No newline at end of file diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs new file mode 100644 index 000000000..b311f3c01 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs @@ -0,0 +1,64 @@ +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 +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketVoiceChannel : SocketGuildChannel, IVoiceChannel, ISocketAudioChannel + { + public int Bitrate { get; private set; } + public int UserLimit { get; private set; } + + public override IReadOnlyCollection Users + => Guild.Users.Where(x => x.VoiceChannel?.Id == Id).ToImmutableArray(); + + internal SocketVoiceChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) + : base(discord, id, guild) + { + } + internal new static SocketVoiceChannel Create(SocketGuild guild, ClientState state, Model model) + { + var entity = new SocketVoiceChannel(guild.Discord, model.Id, guild); + entity.Update(state, model); + return entity; + } + internal override void Update(ClientState state, Model model) + { + base.Update(state, model); + + Bitrate = model.Bitrate.Value; + UserLimit = model.UserLimit.Value; + } + + public Task ModifyAsync(Action func, RequestOptions options = null) + => ChannelHelper.ModifyAsync(this, Discord, func, options); + + public override SocketGuildUser GetUser(ulong id) + { + var user = Guild.GetUser(id); + if (user?.VoiceChannel?.Id == Id) + return user; + return null; + } + + private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; + internal new SocketVoiceChannel Clone() => MemberwiseClone() as SocketVoiceChannel; + + //IVoiceChannel + Task IVoiceChannel.ConnectAsync() { throw new NotSupportedException(); } + + //IGuildChannel + Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetUser(id)); + IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs new file mode 100644 index 000000000..d87b82c6f --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -0,0 +1,568 @@ +using Discord.API.Rest; +using Discord.Audio; +using Discord.Rest; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using ChannelModel = Discord.API.Channel; +using EmojiUpdateModel = Discord.API.Gateway.GuildEmojiUpdateEvent; +using ExtendedModel = Discord.API.Gateway.ExtendedGuild; +using GuildSyncModel = Discord.API.Gateway.GuildSyncEvent; +using MemberModel = Discord.API.GuildMember; +using Model = Discord.API.Guild; +using PresenceModel = Discord.API.Presence; +using RoleModel = Discord.API.Role; +using VoiceStateModel = Discord.API.VoiceState; + +namespace Discord.WebSocket +{ + public class SocketGuild : SocketEntity, IGuild + { + private readonly SemaphoreSlim _audioLock; + private TaskCompletionSource _syncPromise, _downloaderPromise; + private TaskCompletionSource _audioConnectPromise; + private ConcurrentHashSet _channels; + private ConcurrentDictionary _members; + private ConcurrentDictionary _roles; + private ConcurrentDictionary _voiceStates; + private ImmutableArray _emojis; + private ImmutableArray _features; + internal bool _available; + + 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 int MemberCount { get; set; } + public int DownloadedMemberCount { get; private set; } + public AudioClient AudioClient { 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 DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public ulong DefaultChannelId => Id; + public string IconUrl => API.CDN.GetGuildIconUrl(Id, IconId); + public string SplashUrl => API.CDN.GetGuildSplashUrl(Id, SplashId); + public bool HasAllMembers => _downloaderPromise.Task.IsCompleted; + public bool IsSynced => _syncPromise.Task.IsCompleted; + public Task SyncPromise => _syncPromise.Task; + public Task DownloaderPromise => _downloaderPromise.Task; + + public SocketRole EveryoneRole => GetRole(Id); + public IReadOnlyCollection Channels + { + get + { + var channels = _channels; + var state = Discord.State; + return channels.Select(x => state.GetChannel(x) as SocketGuildChannel).Where(x => x != null).ToReadOnlyCollection(channels); + } + } + public IReadOnlyCollection Emojis => _emojis; + public IReadOnlyCollection Features => _features; + public IReadOnlyCollection Users => _members.ToReadOnlyCollection(); + public IReadOnlyCollection Roles => _roles.ToReadOnlyCollection(); + public IReadOnlyCollection VoiceStates => _voiceStates.ToReadOnlyCollection(); + + internal SocketGuild(DiscordSocketClient client, ulong id) + : base(client, id) + { + _audioLock = new SemaphoreSlim(1, 1); + _emojis = ImmutableArray.Create(); + _features = ImmutableArray.Create(); + } + internal static SocketGuild Create(DiscordSocketClient discord, ClientState state, ExtendedModel model) + { + var entity = new SocketGuild(discord, model.Id); + entity.Update(state, model); + return entity; + } + internal void Update(ClientState state, ExtendedModel model) + { + _available = !(model.Unavailable ?? false); + if (!_available) + { + if (_channels == null) + _channels = new ConcurrentHashSet(); + if (_members == null) + _members = new ConcurrentDictionary(); + if (_roles == null) + _roles = new ConcurrentDictionary(); + /*if (Emojis == null) + _emojis = ImmutableArray.Create(); + if (Features == null) + _features = ImmutableArray.Create();*/ + return; + } + + Update(state, model as Model); + + var channels = new ConcurrentHashSet(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Channels.Length * 1.05)); + { + for (int i = 0; i < model.Channels.Length; i++) + { + var channel = SocketGuildChannel.Create(this, state, model.Channels[i]); + state.AddChannel(channel); + channels.TryAdd(channel.Id); + } + } + _channels = channels; + + var members = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Members.Length * 1.05)); + { + for (int i = 0; i < model.Members.Length; i++) + { + var member = SocketGuildUser.Create(this, state, model.Members[i]); + members.TryAdd(member.Id, member); + } + DownloadedMemberCount = members.Count; + + for (int i = 0; i < model.Presences.Length; i++) + { + SocketGuildUser member; + if (members.TryGetValue(model.Presences[i].User.Id, out member)) + member.Update(state, model.Presences[i]); + else + Debug.Assert(false); + } + } + _members = members; + MemberCount = model.MemberCount; + + var voiceStates = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.VoiceStates.Length * 1.05)); + { + for (int i = 0; i < model.VoiceStates.Length; i++) + { + SocketVoiceChannel channel = null; + if (model.VoiceStates[i].ChannelId.HasValue) + channel = state.GetChannel(model.VoiceStates[i].ChannelId.Value) as SocketVoiceChannel; + var voiceState = SocketVoiceState.Create(channel, model.VoiceStates[i]); + voiceStates.TryAdd(model.VoiceStates[i].UserId, voiceState); + } + } + _voiceStates = voiceStates; + + _syncPromise = new TaskCompletionSource(); + _downloaderPromise = new TaskCompletionSource(); + if (Discord.ApiClient.AuthTokenType != TokenType.User) + { + var _ = _syncPromise.TrySetResultAsync(true); + if (!model.Large) + _ = _downloaderPromise.TrySetResultAsync(true); + } + } + internal void Update(ClientState state, 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(model.Emojis.Length); + for (int i = 0; i < model.Emojis.Length; i++) + emojis.Add(GuildEmoji.Create(model.Emojis[i])); + _emojis = emojis.ToImmutable(); + } + else + _emojis = ImmutableArray.Create(); + + if (model.Features != null) + _features = model.Features.ToImmutableArray(); + else + _features = ImmutableArray.Create(); + + var roles = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Roles.Length * 1.05)); + if (model.Roles != null) + { + for (int i = 0; i < model.Roles.Length; i++) + { + var role = SocketRole.Create(this, state, model.Roles[i]); + roles.TryAdd(role.Id, role); + } + } + _roles = roles; + } + internal void Update(ClientState state, GuildSyncModel model) + { + var members = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Members.Length * 1.05)); + { + for (int i = 0; i < model.Members.Length; i++) + { + var member = SocketGuildUser.Create(this, state, model.Members[i]); + members.TryAdd(member.Id, member); + } + DownloadedMemberCount = members.Count; + + for (int i = 0; i < model.Presences.Length; i++) + { + SocketGuildUser member; + if (members.TryGetValue(model.Presences[i].User.Id, out member)) + member.Update(state, model.Presences[i]); + else + Debug.Assert(false); + } + } + _members = members; + + var _ = _syncPromise.TrySetResultAsync(true); + if (!model.Large) + _ = _downloaderPromise.TrySetResultAsync(true); + } + + internal void Update(ClientState state, EmojiUpdateModel model) + { + var emojis = ImmutableArray.CreateBuilder(model.Emojis.Length); + for (int i = 0; i < model.Emojis.Length; i++) + emojis.Add(GuildEmoji.Create(model.Emojis[i])); + _emojis = emojis.ToImmutable(); + } + + //General + public Task DeleteAsync(RequestOptions options = null) + => GuildHelper.DeleteAsync(this, Discord, options); + + public Task ModifyAsync(Action func, RequestOptions options = null) + => GuildHelper.ModifyAsync(this, Discord, func, options); + public Task ModifyEmbedAsync(Action func, RequestOptions options = null) + => GuildHelper.ModifyEmbedAsync(this, Discord, func, options); + public Task ModifyChannelsAsync(IEnumerable args, RequestOptions options = null) + => GuildHelper.ModifyChannelsAsync(this, Discord, args, options); + public Task ModifyRolesAsync(IEnumerable args, RequestOptions options = null) + => GuildHelper.ModifyRolesAsync(this, Discord, args, options); + + public Task LeaveAsync(RequestOptions options = null) + => GuildHelper.LeaveAsync(this, Discord, options); + + //Bans + public Task> GetBansAsync(RequestOptions options = null) + => GuildHelper.GetBansAsync(this, Discord, options); + + public Task AddBanAsync(IUser user, int pruneDays = 0, RequestOptions options = null) + => GuildHelper.AddBanAsync(this, Discord, user.Id, pruneDays, options); + public Task AddBanAsync(ulong userId, int pruneDays = 0, RequestOptions options = null) + => GuildHelper.AddBanAsync(this, Discord, userId, pruneDays, options); + + public Task RemoveBanAsync(IUser user, RequestOptions options = null) + => GuildHelper.RemoveBanAsync(this, Discord, user.Id, options); + public Task RemoveBanAsync(ulong userId, RequestOptions options = null) + => GuildHelper.RemoveBanAsync(this, Discord, userId, options); + + //Channels + public SocketGuildChannel GetChannel(ulong id) + { + var channel = Discord.State.GetChannel(id) as SocketGuildChannel; + if (channel?.Guild.Id == Id) + return channel; + return null; + } + public Task CreateTextChannelAsync(string name, RequestOptions options = null) + => GuildHelper.CreateTextChannelAsync(this, Discord, name, options); + public Task CreateVoiceChannelAsync(string name, RequestOptions options = null) + => GuildHelper.CreateVoiceChannelAsync(this, Discord, name, options); + internal SocketGuildChannel AddChannel(ClientState state, ChannelModel model) + { + var channel = SocketGuildChannel.Create(this, state, model); + _channels.TryAdd(model.Id); + state.AddChannel(channel); + return channel; + } + internal SocketGuildChannel RemoveChannel(ClientState state, ulong id) + { + if (_channels.TryRemove(id)) + return state.RemoveChannel(id) as SocketGuildChannel; + return null; + } + + //Integrations + public Task> GetIntegrationsAsync(RequestOptions options = null) + => GuildHelper.GetIntegrationsAsync(this, Discord, options); + public Task CreateIntegrationAsync(ulong id, string type, RequestOptions options = null) + => GuildHelper.CreateIntegrationAsync(this, Discord, id, type, options); + + //Invites + public Task> GetInvitesAsync(RequestOptions options = null) + => GuildHelper.GetInvitesAsync(this, Discord, options); + + //Roles + public SocketRole GetRole(ulong id) + { + SocketRole value; + if (_roles.TryGetValue(id, out value)) + return value; + return null; + } + public Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), + bool isHoisted = false, RequestOptions options = null) + => GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, options); + internal SocketRole AddRole(RoleModel model) + { + var role = SocketRole.Create(this, Discord.State, model); + _roles[model.Id] = role; + return role; + } + internal SocketRole RemoveRole(ulong id) + { + SocketRole role; + if (_roles.TryRemove(id, out role)) + return role; + return null; + } + + //Users + public SocketGuildUser GetUser(ulong id) + { + SocketGuildUser member; + if (_members.TryGetValue(id, out member)) + return member; + return null; + } + public SocketGuildUser GetCurrentUser() + { + SocketGuildUser member; + if (_members.TryGetValue(Discord.CurrentUser.Id, out member)) + return member; + return null; + } + public Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null) + => GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options); + + internal SocketGuildUser AddOrUpdateUser(MemberModel model) + { + SocketGuildUser member; + if (_members.TryGetValue(model.User.Id, out member)) + member.Update(Discord.State, model); + else + { + member = SocketGuildUser.Create(this, Discord.State, model); + _members[member.Id] = member; + DownloadedMemberCount++; + } + return member; + } + internal SocketGuildUser AddOrUpdateUser(PresenceModel model) + { + SocketGuildUser member; + if (_members.TryGetValue(model.User.Id, out member)) + member.Update(Discord.State, model); + else + { + member = SocketGuildUser.Create(this, Discord.State, model); + _members[member.Id] = member; + DownloadedMemberCount++; + } + return member; + } + internal SocketGuildUser RemoveUser(ulong id) + { + SocketGuildUser member; + if (_members.TryRemove(id, out member)) + { + DownloadedMemberCount--; + return member; + } + member.GlobalUser.RemoveRef(Discord); + return null; + } + + public async Task DownloadUsersAsync() + { + await Discord.DownloadUsersAsync(new[] { this }).ConfigureAwait(false); + } + internal void CompleteDownloadUsers() + { + _downloaderPromise.TrySetResultAsync(true); + } + + //Voice States + internal SocketVoiceState AddOrUpdateVoiceState(ClientState state, VoiceStateModel model) + { + var voiceChannel = state.GetChannel(model.ChannelId.Value) as SocketVoiceChannel; + var voiceState = SocketVoiceState.Create(voiceChannel, model); + _voiceStates[model.UserId] = voiceState; + return voiceState; + } + internal SocketVoiceState? GetVoiceState(ulong id) + { + SocketVoiceState voiceState; + if (_voiceStates.TryGetValue(id, out voiceState)) + return voiceState; + return null; + } + internal SocketVoiceState? RemoveVoiceState(ulong id) + { + SocketVoiceState voiceState; + if (_voiceStates.TryRemove(id, out voiceState)) + return voiceState; + return null; + } + + //Audio + 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); + } + } + } + internal async Task FinishConnectAudio(int id, string url, string token) + { + var voiceState = GetVoiceState(Discord.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(Discord.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, Discord.CurrentUser.Id, voiceState.VoiceSessionId, token).ConfigureAwait(false); + await _audioConnectPromise.TrySetResultAsync(AudioClient).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + await DisconnectAudioAsync().ConfigureAwait(false); + } + catch (Exception e) + { + await _audioConnectPromise.SetExceptionAsync(e).ConfigureAwait(false); + await DisconnectAudioAsync().ConfigureAwait(false); + } + finally + { + _audioLock.Release(); + } + } + internal async Task FinishJoinAudioChannel() + { + await _audioLock.WaitAsync().ConfigureAwait(false); + try + { + if (AudioClient != null) + await _audioConnectPromise.TrySetResultAsync(AudioClient).ConfigureAwait(false); + } + finally + { + _audioLock.Release(); + } + } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id})"; + internal SocketGuild Clone() => MemberwiseClone() as SocketGuild; + + //IGuild + bool IGuild.Available => true; + IAudioClient IGuild.AudioClient => null; + IRole IGuild.EveryoneRole => EveryoneRole; + IReadOnlyCollection IGuild.Roles => Roles; + + async Task> IGuild.GetBansAsync(RequestOptions options) + => await GetBansAsync(options).ConfigureAwait(false); + + Task> IGuild.GetChannelsAsync(CacheMode mode, RequestOptions options) + => Task.FromResult>(Channels); + Task IGuild.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetChannel(id)); + async Task IGuild.CreateTextChannelAsync(string name, RequestOptions options) + => await CreateTextChannelAsync(name, options).ConfigureAwait(false); + async Task IGuild.CreateVoiceChannelAsync(string name, RequestOptions options) + => await CreateVoiceChannelAsync(name, options).ConfigureAwait(false); + + async Task> IGuild.GetIntegrationsAsync(RequestOptions options) + => await GetIntegrationsAsync(options).ConfigureAwait(false); + async Task IGuild.CreateIntegrationAsync(ulong id, string type, RequestOptions options) + => await CreateIntegrationAsync(id, type, options).ConfigureAwait(false); + + async Task> IGuild.GetInvitesAsync(RequestOptions options) + => await GetInvitesAsync(options).ConfigureAwait(false); + + IRole IGuild.GetRole(ulong id) + => GetRole(id); + async Task IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted, RequestOptions options) + => await CreateRoleAsync(name, permissions, color, isHoisted, options).ConfigureAwait(false); + + Task> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options) + => Task.FromResult>(Users); + Task IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetUser(id)); + Task IGuild.GetCurrentUserAsync(CacheMode mode, RequestOptions options) + => Task.FromResult(GetCurrentUser()); + Task IGuild.DownloadUsersAsync() { throw new NotSupportedException(); } + } +} diff --git a/src/Discord.Net/WebSocket/Entities/Channels/MessageCache.cs b/src/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs similarity index 58% rename from src/Discord.Net/WebSocket/Entities/Channels/MessageCache.cs rename to src/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs index 7fafebba5..7b8d9c2cd 100644 --- a/src/Discord.Net/WebSocket/Entities/Channels/MessageCache.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs @@ -3,57 +3,55 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Threading.Tasks; namespace Discord.WebSocket { - internal class MessageCache : MessageManager + internal class MessageCache { - private readonly ConcurrentDictionary _messages; + private readonly ConcurrentDictionary _messages; private readonly ConcurrentQueue _orderedMessages; private readonly int _size; - public override IReadOnlyCollection Messages => _messages.ToReadOnlyCollection(); + public IReadOnlyCollection Messages => _messages.ToReadOnlyCollection(); - public MessageCache(DiscordSocketClient discord, ISocketMessageChannel channel) - : base(discord, channel) + public MessageCache(DiscordSocketClient discord, IChannel channel) { _size = discord.MessageCacheSize; - _messages = new ConcurrentDictionary(1, (int)(_size * 1.05)); + _messages = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(_size * 1.05)); _orderedMessages = new ConcurrentQueue(); } - 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 GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + public IReadOnlyCollection GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) { if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); - if (limit == 0) return ImmutableArray.Empty; + if (limit == 0) return ImmutableArray.Empty; IEnumerable cachedMessageIds; if (fromMessageId == null) @@ -63,25 +61,20 @@ namespace Discord.WebSocket else cachedMessageIds = _orderedMessages.Where(x => x > fromMessageId.Value); + if (dir == Direction.Before) + cachedMessageIds = cachedMessageIds.Reverse(); + return cachedMessageIds - .Take(limit) .Select(x => { - ISocketMessage msg; + SocketMessage msg; if (_messages.TryGetValue(x, out msg)) return msg; return null; }) .Where(x => x != null) + .Take(limit) .ToImmutableArray(); } - - public override async Task DownloadAsync(ulong id) - { - var msg = Get(id); - if (msg != null) - return msg; - return await base.DownloadAsync(id).ConfigureAwait(false); - } } } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs new file mode 100644 index 000000000..0043ff8d2 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Model = Discord.API.Message; + +namespace Discord.WebSocket +{ + public abstract class SocketMessage : SocketEntity, IMessage + { + private long _timestampTicks; + + public SocketUser Author { get; } + public ISocketMessageChannel Channel { get; } + + public string Content { get; private set; } + + public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public virtual bool IsTTS => false; + public virtual bool IsPinned => false; + public virtual DateTimeOffset? EditedTimestamp => null; + public virtual IReadOnlyCollection Attachments => ImmutableArray.Create(); + public virtual IReadOnlyCollection Embeds => ImmutableArray.Create(); + public virtual IReadOnlyCollection MentionedChannels => ImmutableArray.Create(); + public virtual IReadOnlyCollection MentionedRoles => ImmutableArray.Create(); + public virtual IReadOnlyCollection MentionedUsers => ImmutableArray.Create(); + public virtual IReadOnlyCollection Tags => ImmutableArray.Create(); + public virtual ulong? WebhookId => null; + public bool IsWebhook => WebhookId != null; + + public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); + + internal SocketMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author) + : base(discord, id) + { + Channel = channel; + Author = author; + } + internal static SocketMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) + { + if (model.Type == MessageType.Default) + return SocketUserMessage.Create(discord, state, author, channel, model); + else + return SocketSystemMessage.Create(discord, state, author, channel, model); + } + internal virtual void Update(ClientState state, Model model) + { + if (model.Timestamp.IsSpecified) + _timestampTicks = model.Timestamp.Value.UtcTicks; + + if (model.Content.IsSpecified) + Content = model.Content.Value; + } + + public override string ToString() => Content; + internal SocketMessage Clone() => MemberwiseClone() as SocketMessage; + + //IMessage + IUser IMessage.Author => Author; + IMessageChannel IMessage.Channel => Channel; + MessageType IMessage.Type => MessageType.Default; + IReadOnlyCollection IMessage.Attachments => Attachments; + IReadOnlyCollection IMessage.Embeds => Embeds; + IReadOnlyCollection IMessage.MentionedChannelIds => MentionedChannels.Select(x => x.Id).ToImmutableArray(); + IReadOnlyCollection IMessage.MentionedRoleIds => MentionedRoles.Select(x => x.Id).ToImmutableArray(); + IReadOnlyCollection IMessage.MentionedUserIds => MentionedUsers.Select(x => x.Id).ToImmutableArray(); + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs new file mode 100644 index 000000000..7678bb412 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs @@ -0,0 +1,31 @@ +using System.Diagnostics; +using Model = Discord.API.Message; + +namespace Discord.WebSocket +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + internal class SocketSystemMessage : SocketMessage, ISystemMessage + { + public MessageType Type { get; private set; } + + internal SocketSystemMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author) + : base(discord, id, channel, author) + { + } + internal new static SocketSystemMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) + { + var entity = new SocketSystemMessage(discord, model.Id, channel, author); + entity.Update(state, model); + return entity; + } + internal override void Update(ClientState state, Model model) + { + base.Update(state, model); + + Type = model.Type; + } + + private string DebuggerDisplay => $"{Author}: {Content} ({Id}, {Type})"; + internal new SocketSystemMessage Clone() => MemberwiseClone() as SocketSystemMessage; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs new file mode 100644 index 000000000..81a9ff4c7 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -0,0 +1,130 @@ +using Discord.API.Rest; +using Discord.Rest; +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 class SocketUserMessage : SocketMessage, IUserMessage + { + private bool _isMentioningEveryone, _isTTS, _isPinned; + private long? _editedTimestampTicks; + private ulong? _webhookId; + private ImmutableArray _attachments; + private ImmutableArray _embeds; + private ImmutableArray _tags; + + public override bool IsTTS => _isTTS; + public override bool IsPinned => _isPinned; + public override ulong? WebhookId => _webhookId; + public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks); + public override IReadOnlyCollection Attachments => _attachments; + public override IReadOnlyCollection Embeds => _embeds; + public override IReadOnlyCollection Tags => _tags; + public override IReadOnlyCollection MentionedChannels => MessageHelper.FilterTagsByValue(TagType.ChannelMention, _tags); + public override IReadOnlyCollection MentionedRoles => MessageHelper.FilterTagsByValue(TagType.RoleMention, _tags); + public override IReadOnlyCollection MentionedUsers => MessageHelper.FilterTagsByValue(TagType.UserMention, _tags); + + internal SocketUserMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author) + : base(discord, id, channel, author) + { + } + internal new static SocketUserMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) + { + var entity = new SocketUserMessage(discord, model.Id, channel, author); + entity.Update(state, model); + return entity; + } + + internal override void Update(ClientState state, Model model) + { + base.Update(state, 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.WebhookId.IsSpecified) + _webhookId = model.WebhookId.Value; + + if (model.Attachments.IsSpecified) + { + var value = model.Attachments.Value; + if (value.Length > 0) + { + var attachments = ImmutableArray.CreateBuilder(value.Length); + for (int i = 0; i < value.Length; i++) + attachments.Add(Attachment.Create(value[i])); + _attachments = attachments.ToImmutable(); + } + else + _attachments = ImmutableArray.Create(); + } + + if (model.Embeds.IsSpecified) + { + var value = model.Embeds.Value; + if (value.Length > 0) + { + var embeds = ImmutableArray.CreateBuilder(value.Length); + for (int i = 0; i < value.Length; i++) + embeds.Add(Embed.Create(value[i])); + _embeds = embeds.ToImmutable(); + } + else + _embeds = ImmutableArray.Create(); + } + + ImmutableArray mentions = ImmutableArray.Create(); + if (model.UserMentions.IsSpecified) + { + var value = model.UserMentions.Value; + if (value.Length > 0) + { + var newMentions = ImmutableArray.CreateBuilder(value.Length); + for (int i = 0; i < value.Length; i++) + { + var val = value[i]; + if (val.Object != null) + newMentions.Add(SocketSimpleUser.Create(Discord, Discord.State, val.Object)); + } + mentions = newMentions.ToImmutable(); + } + } + + if (model.Content.IsSpecified) + { + var text = model.Content.Value; + var guild = (Channel as SocketGuildChannel)?.Guild; + _tags = MessageHelper.ParseTags(text, Channel, guild, mentions); + model.Content = text; + } + } + + public Task ModifyAsync(Action func, RequestOptions options = null) + => MessageHelper.ModifyAsync(this, Discord, func, options); + public Task DeleteAsync(RequestOptions options = null) + => MessageHelper.DeleteAsync(this, Discord, options); + + public Task PinAsync(RequestOptions options = null) + => MessageHelper.PinAsync(this, Discord, options); + public Task UnpinAsync(RequestOptions options = null) + => MessageHelper.UnpinAsync(this, Discord, options); + + public string Resolve(TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, + TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) + => MentionUtils.Resolve(this, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); + + private string DebuggerDisplay => $"{Author}: {Content} ({Id}{(Attachments.Count > 0 ? $", {Attachments.Count} Attachments" : "")})"; + internal new SocketUserMessage Clone() => MemberwiseClone() as SocketUserMessage; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs b/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs new file mode 100644 index 000000000..515389da1 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs @@ -0,0 +1,61 @@ +using Discord.API.Rest; +using Discord.Rest; +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.Role; + +namespace Discord.WebSocket +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketRole : SocketEntity, IRole + { + public SocketGuild Guild { get; } + + public Color Color { get; private set; } + public bool IsHoisted { get; private set; } + public bool IsManaged { get; private set; } + public bool IsMentionable { get; private set; } + public string Name { get; private set; } + public GuildPermissions Permissions { get; private set; } + public int Position { get; private set; } + + public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public bool IsEveryone => Id == Guild.Id; + public string Mention => MentionUtils.MentionRole(Id); + + internal SocketRole(SocketGuild guild, ulong id) + : base(guild.Discord, id) + { + Guild = guild; + } + internal static SocketRole Create(SocketGuild guild, ClientState state, Model model) + { + var entity = new SocketRole(guild, model.Id); + entity.Update(state, model); + return entity; + } + internal void Update(ClientState state, Model model) + { + Name = model.Name; + IsHoisted = model.Hoist; + IsManaged = model.Managed; + IsMentionable = model.Mentionable; + Position = model.Position; + Color = new Color(model.Color); + Permissions = new GuildPermissions(model.Permissions); + } + + public Task ModifyAsync(Action func, RequestOptions options = null) + => RoleHelper.ModifyAsync(this, Discord, func, options); + public Task DeleteAsync(RequestOptions options = null) + => RoleHelper.DeleteAsync(this, Discord, options); + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id})"; + internal SocketRole Clone() => MemberwiseClone() as SocketRole; + + //IRole + IGuild IRole.Guild => Guild; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/SocketEntity.cs b/src/Discord.Net.WebSocket/Entities/SocketEntity.cs new file mode 100644 index 000000000..072e414f8 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/SocketEntity.cs @@ -0,0 +1,19 @@ +using System; + +namespace Discord.WebSocket +{ + public abstract class SocketEntity : IEntity + where T : IEquatable + { + public DiscordSocketClient Discord { get; } + public T Id { get; } + + internal SocketEntity(DiscordSocketClient discord, T id) + { + Discord = discord; + Id = id; + } + + IDiscordClient IEntity.Discord => Discord; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs new file mode 100644 index 000000000..f0b23543e --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs @@ -0,0 +1,57 @@ +using System.Diagnostics; +using Model = Discord.API.User; + +namespace Discord.WebSocket +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + internal class SocketGlobalUser : SocketUser + { + public override bool IsBot { get; internal set; } + public override string Username { get; internal set; } + public override ushort DiscriminatorValue { get; internal set; } + public override string AvatarId { get; internal set; } + public SocketDMChannel DMChannel { get; internal set; } + + internal override SocketGlobalUser GlobalUser => this; + internal override SocketPresence Presence { get; set; } + + private readonly object _lockObj = new object(); + private ushort _references; + + private SocketGlobalUser(DiscordSocketClient discord, ulong id) + : base(discord, id) + { + } + internal static SocketGlobalUser Create(DiscordSocketClient discord, ClientState state, Model model) + { + var entity = new SocketGlobalUser(discord, model.Id); + entity.Update(state, model); + return entity; + } + + internal void AddRef() + { + checked + { + lock (_lockObj) + _references++; + } + } + internal void RemoveRef(DiscordSocketClient discord) + { + lock (_lockObj) + { + if (--_references <= 0) + discord.RemoveUser(Id); + } + } + + internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser; + + //Updates are only ever called from the gateway thread, thus threadsafe + internal override void Update(ClientState state, Model model) + { + base.Update(state, model); + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs new file mode 100644 index 000000000..694d0ccb9 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs @@ -0,0 +1,42 @@ +using System.Diagnostics; +using Model = Discord.API.User; + +namespace Discord.WebSocket +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class SocketGroupUser : SocketUser, IGroupUser + { + public SocketGroupChannel Channel { get; } + internal override SocketGlobalUser GlobalUser { get; } + + public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } } + public override string Username { get { return GlobalUser.Username; } internal set { GlobalUser.Username = value; } } + public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } } + public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } + internal override SocketPresence Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } + + internal SocketGroupUser(SocketGroupChannel channel, SocketGlobalUser globalUser) + : base(channel.Discord, globalUser.Id) + { + Channel = channel; + GlobalUser = globalUser; + } + internal static SocketGroupUser Create(SocketGroupChannel channel, ClientState state, Model model) + { + var entity = new SocketGroupUser(channel, channel.Discord.GetOrCreateUser(state, model)); + entity.Update(state, model); + return entity; + } + + internal new SocketGroupUser Clone() => MemberwiseClone() as SocketGroupUser; + + //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; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs new file mode 100644 index 000000000..00972f51a --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -0,0 +1,106 @@ +using Discord.API.Rest; +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.GuildMember; +using PresenceModel = Discord.API.Presence; + +namespace Discord.WebSocket +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketGuildUser : SocketUser, IGuildUser + { + private long? _joinedAtTicks; + private ImmutableArray _roleIds; + + internal override SocketGlobalUser GlobalUser { get; } + public SocketGuild Guild { get; } + public string Nickname { get; private set; } + + public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } } + public override string Username { get { return GlobalUser.Username; } internal set { GlobalUser.Username = value; } } + public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } } + public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } + public GuildPermissions GuildPermissions => new GuildPermissions(Permissions.ResolveGuild(Guild, this)); + public IReadOnlyCollection RoleIds => _roleIds; + internal override SocketPresence Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } + + public SocketVoiceState? 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 DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); + + internal SocketGuildUser(SocketGuild guild, SocketGlobalUser globalUser) + : base(guild.Discord, globalUser.Id) + { + Guild = guild; + GlobalUser = globalUser; + } + internal static SocketGuildUser Create(SocketGuild guild, ClientState state, Model model) + { + var entity = new SocketGuildUser(guild, guild.Discord.GetOrCreateUser(state, model.User)); + entity.Update(state, model); + return entity; + } + internal static SocketGuildUser Create(SocketGuild guild, ClientState state, PresenceModel model) + { + var entity = new SocketGuildUser(guild, guild.Discord.GetOrCreateUser(state, model.User)); + entity.Update(state, model); + return entity; + } + internal void Update(ClientState state, Model model) + { + base.Update(state, model.User); + _joinedAtTicks = model.JoinedAt.UtcTicks; + if (model.Nick.IsSpecified) + Nickname = model.Nick.Value; + UpdateRoles(model.Roles); + } + internal override void Update(ClientState state, PresenceModel model) + { + base.Update(state, model); + if (model.Roles.IsSpecified) + UpdateRoles(model.Roles.Value); + if (model.Nick.IsSpecified) + Nickname = model.Nick.Value; + } + private void UpdateRoles(ulong[] roleIds) + { + var roles = ImmutableArray.CreateBuilder(roleIds.Length + 1); + roles.Add(Guild.Id); + for (int i = 0; i < roleIds.Length; i++) + roles.Add(roleIds[i]); + _roleIds = roles.ToImmutable(); + } + + public Task ModifyAsync(Action func, RequestOptions options = null) + => UserHelper.ModifyAsync(this, Discord, func, options); + public Task KickAsync(RequestOptions options = null) + => UserHelper.KickAsync(this, Discord, options); + + public ChannelPermissions GetPermissions(IGuildChannel channel) + => new ChannelPermissions(Permissions.ResolveChannel(Guild, this, channel, GuildPermissions.RawValue)); + + internal new SocketGuildUser Clone() => MemberwiseClone() as SocketGuildUser; + + //IGuildUser + ulong IGuildUser.GuildId => Guild.Id; + IReadOnlyCollection IGuildUser.RoleIds => RoleIds; + + //IUser + Task IUser.GetDMChannelAsync(CacheMode mode, RequestOptions options) + => Task.FromResult(GlobalUser.DMChannel); + + //IVoiceState + IVoiceChannel IVoiceState.VoiceChannel => VoiceChannel; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs new file mode 100644 index 000000000..629aa2093 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs @@ -0,0 +1,28 @@ +using System.Diagnostics; +using Model = Discord.API.Presence; + +namespace Discord.WebSocket +{ + //TODO: C#7 Candidate for record type + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public struct SocketPresence : IPresence + { + public UserStatus Status { get; } + public Game? Game { get; } + + internal SocketPresence(UserStatus status, Game? game) + { + Status = status; + Game = game; + } + internal static SocketPresence Create(Model model) + { + return new SocketPresence(model.Status, model.Game != null ? Discord.Game.Create(model.Game) : (Game?)null); + } + + public override string ToString() => Status.ToString(); + internal string DebuggerDisplay => $"{Status}{(Game != null ? $", {Game.Value.Name} ({Game.Value.StreamType})" : "")}"; + + internal SocketPresence Clone() => this; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs new file mode 100644 index 000000000..6e230c317 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs @@ -0,0 +1,103 @@ +using Discord.API.Rest; +using Discord.Rest; +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using GameEntity = Discord.Game; +using Model = Discord.API.User; + +namespace Discord.WebSocket +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketSelfUser : SocketUser, ISelfUser + { + private DateTimeOffset? _statusSince; + + public string Email { get; private set; } + public bool IsVerified { get; private set; } + public bool IsMfaEnabled { get; private set; } + internal override SocketGlobalUser GlobalUser { get; } + + public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } } + public override string Username { get { return GlobalUser.Username; } internal set { GlobalUser.Username = value; } } + public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } } + public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } + internal override SocketPresence Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } + + internal SocketSelfUser(DiscordSocketClient discord, SocketGlobalUser globalUser) + : base(discord, globalUser.Id) + { + GlobalUser = globalUser; + } + internal static SocketSelfUser Create(DiscordSocketClient discord, ClientState state, Model model) + { + var entity = new SocketSelfUser(discord, discord.GetOrCreateSelfUser(state, model)); + entity.Update(state, model); + return entity; + } + internal override void Update(ClientState state, Model model) + { + base.Update(state, model); + + if (model.Email.IsSpecified) + Email = model.Email.Value; + if (model.Verified.IsSpecified) + IsVerified = model.Verified.Value; + if (model.MfaEnabled.IsSpecified) + IsMfaEnabled = model.MfaEnabled.Value; + } + + public Task ModifyAsync(Action func, RequestOptions options = null) + => UserHelper.ModifyAsync(this, Discord, func, options); + public async Task ModifyStatusAsync(Action func, RequestOptions options = null) + { + var args = new ModifyPresenceParams(); + func(args); + + UserStatus status; + if (args.Status.IsSpecified) + { + status = args.Status.Value; + if (status == UserStatus.AFK) + _statusSince = DateTimeOffset.UtcNow; + else + _statusSince = null; + } + else + status = Status; + + GameEntity? game; + if (args.Game.IsSpecified) + { + var model = args.Game.Value; + if (model != null) + game = GameEntity.Create(model); + else + game = null; + } + else + game = Game; + + Presence = new SocketPresence(status, game); + + await SendStatus(status, game).ConfigureAwait(false); + } + internal async Task SendStatus(UserStatus status, GameEntity? game) + { + var gameModel = game != null ? new API.Game + { + Name = game.Value.Name, + StreamType = game.Value.StreamType, + StreamUrl = game.Value.StreamUrl + } : null; + + await Discord.ApiClient.SendStatusUpdateAsync( + status, + status == UserStatus.AFK, + _statusSince != null ? _statusSince.Value.ToUnixTimeMilliseconds() : (long?)null, + gameModel).ConfigureAwait(false); + } + + internal new SocketSelfUser Clone() => MemberwiseClone() as SocketSelfUser; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketSimpleUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketSimpleUser.cs new file mode 100644 index 000000000..1ecb5e578 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketSimpleUser.cs @@ -0,0 +1,36 @@ +using System; +using System.Diagnostics; +using Model = Discord.API.User; +using PresenceModel = Discord.API.Presence; + +namespace Discord.WebSocket +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketSimpleUser : SocketUser + { + public override bool IsBot { get; internal set; } + public override string Username { get; internal set; } + public override ushort DiscriminatorValue { get; internal set; } + public override string AvatarId { get; internal set; } + internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null); } set { } } + + internal override SocketGlobalUser GlobalUser { get { throw new NotSupportedException(); } } + + internal SocketSimpleUser(DiscordSocketClient discord, ulong id) + : base(discord, id) + { + } + internal static SocketSimpleUser Create(DiscordSocketClient discord, ClientState state, Model model) + { + var entity = new SocketSimpleUser(discord, model.Id); + entity.Update(state, model); + return entity; + } + + internal override void Update(ClientState state, PresenceModel model) + { + } + + internal new SocketSimpleUser Clone() => MemberwiseClone() as SocketSimpleUser; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs new file mode 100644 index 000000000..674239be7 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -0,0 +1,58 @@ +using Discord.Rest; +using System; +using System.Threading.Tasks; +using Model = Discord.API.User; +using PresenceModel = Discord.API.Presence; + +namespace Discord.WebSocket +{ + public abstract class SocketUser : SocketEntity, IUser + { + public abstract bool IsBot { get; internal set; } + public abstract string Username { get; internal set; } + public abstract ushort DiscriminatorValue { get; internal set; } + public abstract string AvatarId { get; internal set; } + internal abstract SocketGlobalUser GlobalUser { get; } + internal abstract SocketPresence Presence { get; set; } + + public string AvatarUrl => API.CDN.GetUserAvatarUrl(Id, AvatarId); + public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public string Discriminator => DiscriminatorValue.ToString("D4"); + public string Mention => MentionUtils.MentionUser(Id); + public Game? Game => Presence.Game; + public UserStatus Status => Presence.Status; + + internal SocketUser(DiscordSocketClient discord, ulong id) + : base(discord, id) + { + } + internal virtual void Update(ClientState state, Model model) + { + if (model.Avatar.IsSpecified) + AvatarId = model.Avatar.Value; + if (model.Discriminator.IsSpecified) + DiscriminatorValue = ushort.Parse(model.Discriminator.Value); + if (model.Bot.IsSpecified) + IsBot = model.Bot.Value; + if (model.Username.IsSpecified) + Username = model.Username.Value; + } + internal virtual void Update(ClientState state, PresenceModel model) + { + Presence = SocketPresence.Create(model); + } + + public Task CreateDMChannelAsync(RequestOptions options = null) + => UserHelper.CreateDMChannelAsync(this, Discord, options); + + public override string ToString() => $"{Username}#{Discriminator}"; + internal string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; + internal SocketUser Clone() => MemberwiseClone() as SocketUser; + + //IUser + Task IUser.GetDMChannelAsync(CacheMode mode, RequestOptions options) + => Task.FromResult(GlobalUser.DMChannel); + async Task IUser.CreateDMChannelAsync(RequestOptions options) + => await CreateDMChannelAsync(options).ConfigureAwait(false); + } +} diff --git a/src/Discord.Net/WebSocket/Entities/Users/VoiceState.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs similarity index 62% rename from src/Discord.Net/WebSocket/Entities/Users/VoiceState.cs rename to src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs index 123c7ee98..ed4036362 100644 --- a/src/Discord.Net/WebSocket/Entities/Users/VoiceState.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs @@ -1,15 +1,17 @@ using System; +using System.Diagnostics; using Model = Discord.API.VoiceState; namespace Discord.WebSocket { //TODO: C#7 Candidate for record type - internal struct VoiceState : IVoiceState + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public struct SocketVoiceState : IVoiceState { [Flags] private enum Flags : byte { - None = 0x00, + Normal = 0x00, Suppressed = 0x01, Muted = 0x02, Deafened = 0x04, @@ -28,14 +30,12 @@ namespace Discord.WebSocket public bool IsSelfMuted => (_voiceStates & Flags.SelfMuted) != 0; public bool IsSelfDeafened => (_voiceStates & Flags.SelfDeafened) != 0; - public VoiceState(SocketVoiceChannel voiceChannel, Model model) - : this(voiceChannel, model.SessionId, model.SelfMute, model.SelfDeaf, model.Suppress) { } - public VoiceState(SocketVoiceChannel voiceChannel, string sessionId, bool isSelfMuted, bool isSelfDeafened, bool isSuppressed) + internal SocketVoiceState(SocketVoiceChannel voiceChannel, string sessionId, bool isSelfMuted, bool isSelfDeafened, bool isSuppressed) { VoiceChannel = voiceChannel; VoiceSessionId = sessionId; - Flags voiceStates = Flags.None; + Flags voiceStates = Flags.Normal; if (isSelfMuted) voiceStates |= Flags.SelfMuted; if (isSelfDeafened) @@ -44,8 +44,14 @@ namespace Discord.WebSocket voiceStates |= Flags.Suppressed; _voiceStates = voiceStates; } + internal static SocketVoiceState Create(SocketVoiceChannel voiceChannel, Model model) + { + return new SocketVoiceState(voiceChannel, model.SessionId, model.SelfMute, model.SelfDeaf, model.Suppress); + } - public VoiceState Clone() => this; + public override string ToString() => VoiceChannel?.Name ?? "Unknown"; + internal string DebuggerDisplay => $"{VoiceChannel?.Name ?? "Unknown"} ({_voiceStates})"; + internal SocketVoiceState Clone() => this; IVoiceChannel IVoiceState.VoiceChannel => VoiceChannel; } diff --git a/src/Discord.Net.WebSocket/project.json b/src/Discord.Net.WebSocket/project.json new file mode 100644 index 000000000..a37f4f29b --- /dev/null +++ b/src/Discord.Net.WebSocket/project.json @@ -0,0 +1,43 @@ +{ + "version": "1.0.0-beta2-*", + + "buildOptions": { + "allowUnsafe": true + }, + + "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" + }, + "System.IO.Compression": "4.1.0", + "System.Net.NameResolution": "4.0.0", + "System.Net.Sockets": "4.1.0", + "System.Net.WebSockets.Client": "4.0.0", + "System.Runtime.InteropServices": "4.1.0" + }, + + "frameworks": { + "netstandard1.3": { + "imports": [ + "dotnet5.4", + "dnxcore50", + "portable-net45+win8" + ] + } + } +} diff --git a/src/Discord.Net/API/DiscordRestApiClient.cs b/src/Discord.Net/API/DiscordRestApiClient.cs deleted file mode 100644 index 0d0b22c25..000000000 --- a/src/Discord.Net/API/DiscordRestApiClient.cs +++ /dev/null @@ -1,1068 +0,0 @@ -#pragma warning disable CS1591 -using Discord.API.Rest; -using Discord.Net; -using Discord.Net.Converters; -using Discord.Net.Queue; -using Discord.Net.Rest; -using Discord.Rest; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Discord.API -{ - public class DiscordRestApiClient : IDisposable - { - public event Func SentRequest { add { _sentRequestEvent.Add(value); } remove { _sentRequestEvent.Remove(value); } } - private readonly AsyncEvent> _sentRequestEvent = new AsyncEvent>(); - - protected readonly JsonSerializer _serializer; - protected readonly SemaphoreSlim _stateLock; - private readonly RestClientProvider _restClientProvider; - - protected string _authToken; - protected bool _isDisposed; - private CancellationTokenSource _loginCancelToken; - private IRestClient _restClient; - - public LoginState LoginState { get; private set; } - public TokenType AuthTokenType { get; private set; } - internal RequestQueue RequestQueue { get; private set; } - - public DiscordRestApiClient(RestClientProvider restClientProvider, JsonSerializer serializer = null, RequestQueue requestQueue = null) - { - _restClientProvider = restClientProvider; - _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; - RequestQueue = requestQueue; - - _stateLock = new SemaphoreSlim(1, 1); - - SetBaseUrl(DiscordConfig.ClientAPIUrl); - } - internal void SetBaseUrl(string baseUrl) - { - _restClient = _restClientProvider(baseUrl); - _restClient.SetHeader("accept", "*/*"); - _restClient.SetHeader("user-agent", DiscordRestConfig.UserAgent); - _restClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, _authToken)); - } - internal static string GetPrefixedToken(TokenType tokenType, string token) - { - switch (tokenType) - { - case TokenType.Bot: - return $"Bot {token}"; - case TokenType.Bearer: - return $"Bearer {token}"; - case TokenType.User: - return token; - default: - throw new ArgumentException("Unknown OAuth token type", nameof(tokenType)); - } - } - internal virtual void Dispose(bool disposing) - { - if (!_isDisposed) - { - if (disposing) - { - _loginCancelToken?.Dispose(); - (_restClient as IDisposable)?.Dispose(); - } - _isDisposed = true; - } - } - public void Dispose() => Dispose(true); - - public async Task LoginAsync(TokenType tokenType, string token, RequestOptions options = null) - { - await _stateLock.WaitAsync().ConfigureAwait(false); - try - { - await LoginInternalAsync(tokenType, token, options).ConfigureAwait(false); - } - finally { _stateLock.Release(); } - } - private async Task LoginInternalAsync(TokenType tokenType, string token, RequestOptions options = null) - { - if (LoginState != LoginState.LoggedOut) - await LogoutInternalAsync().ConfigureAwait(false); - LoginState = LoginState.LoggingIn; - - try - { - _loginCancelToken = new CancellationTokenSource(); - - AuthTokenType = TokenType.User; - _authToken = null; - await RequestQueue.SetCancelTokenAsync(_loginCancelToken.Token).ConfigureAwait(false); - _restClient.SetCancelToken(_loginCancelToken.Token); - - AuthTokenType = tokenType; - _authToken = token; - _restClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, _authToken)); - - LoginState = LoginState.LoggedIn; - } - catch (Exception) - { - await LogoutInternalAsync().ConfigureAwait(false); - throw; - } - } - - public async Task LogoutAsync() - { - await _stateLock.WaitAsync().ConfigureAwait(false); - try - { - await LogoutInternalAsync().ConfigureAwait(false); - } - finally { _stateLock.Release(); } - } - private async Task LogoutInternalAsync() - { - //An exception here will lock the client into the unusable LoggingOut state, but that's probably fine since our client is in an undefined state too. - if (LoginState == LoginState.LoggedOut) return; - LoginState = LoginState.LoggingOut; - - try { _loginCancelToken?.Cancel(false); } - catch { } - - await DisconnectInternalAsync().ConfigureAwait(false); - await RequestQueue.ClearAsync().ConfigureAwait(false); - - await RequestQueue.SetCancelTokenAsync(CancellationToken.None).ConfigureAwait(false); - _restClient.SetCancelToken(CancellationToken.None); - - LoginState = LoginState.LoggedOut; - } - - internal virtual Task ConnectInternalAsync() => Task.CompletedTask; - internal virtual Task DisconnectInternalAsync() => Task.CompletedTask; - - //REST - public Task SendAsync(string method, string endpoint, - GlobalBucket bucket = GlobalBucket.GeneralRest, bool ignoreState = false, RequestOptions options = null) - => SendInternalAsync(method, endpoint, null, true, BucketGroup.Global, (int)bucket, 0, ignoreState, options); - public Task SendAsync(string method, string endpoint, object payload, - GlobalBucket bucket = GlobalBucket.GeneralRest, bool ignoreState = false, RequestOptions options = null) - => SendInternalAsync(method, endpoint, payload, true, BucketGroup.Global, (int)bucket, 0, ignoreState, options); - public async Task SendAsync(string method, string endpoint, - GlobalBucket bucket = GlobalBucket.GeneralRest, bool ignoreState = false, RequestOptions options = null) where TResponse : class - => DeserializeJson(await SendInternalAsync(method, endpoint, null, false, BucketGroup.Global, (int)bucket, 0, ignoreState, options).ConfigureAwait(false)); - public async Task SendAsync(string method, string endpoint, object payload, GlobalBucket bucket = - GlobalBucket.GeneralRest, bool ignoreState = false, RequestOptions options = null) where TResponse : class - => DeserializeJson(await SendInternalAsync(method, endpoint, payload, false, BucketGroup.Global, (int)bucket, 0, ignoreState, options).ConfigureAwait(false)); - - public Task SendAsync(string method, string endpoint, - GuildBucket bucket, ulong guildId, bool ignoreState = false, RequestOptions options = null) - => SendInternalAsync(method, endpoint, null, true, BucketGroup.Guild, (int)bucket, guildId, ignoreState, options); - public Task SendAsync(string method, string endpoint, object payload, - GuildBucket bucket, ulong guildId, bool ignoreState = false, RequestOptions options = null) - => SendInternalAsync(method, endpoint, payload, true, BucketGroup.Guild, (int)bucket, guildId, ignoreState, options); - public async Task SendAsync(string method, string endpoint, - GuildBucket bucket, ulong guildId, bool ignoreState = false, RequestOptions options = null) where TResponse : class - => DeserializeJson(await SendInternalAsync(method, endpoint, null, false, BucketGroup.Guild, (int)bucket, guildId, ignoreState, options).ConfigureAwait(false)); - public async Task SendAsync(string method, string endpoint, object payload, - GuildBucket bucket, ulong guildId, bool ignoreState = false, RequestOptions options = null) where TResponse : class - => DeserializeJson(await SendInternalAsync(method, endpoint, payload, false, BucketGroup.Guild, (int)bucket, guildId, ignoreState, options).ConfigureAwait(false)); - - //REST - Multipart - public Task SendMultipartAsync(string method, string endpoint, IReadOnlyDictionary multipartArgs, - GlobalBucket bucket = GlobalBucket.GeneralRest, RequestOptions options = null) - => SendMultipartInternalAsync(method, endpoint, multipartArgs, true, BucketGroup.Global, (int)bucket, 0, options); - public async Task SendMultipartAsync(string method, string endpoint, IReadOnlyDictionary multipartArgs, - GlobalBucket bucket = GlobalBucket.GeneralRest, RequestOptions options = null) where TResponse : class - => DeserializeJson(await SendMultipartInternalAsync(method, endpoint, multipartArgs, false, BucketGroup.Global, (int)bucket, 0, options).ConfigureAwait(false)); - - public Task SendMultipartAsync(string method, string endpoint, IReadOnlyDictionary multipartArgs, - GuildBucket bucket, ulong guildId, RequestOptions options = null) - => SendMultipartInternalAsync(method, endpoint, multipartArgs, true, BucketGroup.Guild, (int)bucket, guildId, options); - public async Task SendMultipartAsync(string method, string endpoint, IReadOnlyDictionary multipartArgs, - GuildBucket bucket, ulong guildId, RequestOptions options = null) where TResponse : class - => DeserializeJson(await SendMultipartInternalAsync(method, endpoint, multipartArgs, false, BucketGroup.Guild, (int)bucket, guildId, options).ConfigureAwait(false)); - - //Core - private async Task SendInternalAsync(string method, string endpoint, object payload, bool headerOnly, - BucketGroup group, int bucketId, ulong guildId, bool ignoreState, RequestOptions options = null) - { - if (!ignoreState) - CheckState(); - - var stopwatch = Stopwatch.StartNew(); - string json = null; - if (payload != null) - json = SerializeJson(payload); - var responseStream = await RequestQueue.SendAsync(new RestRequest(_restClient, method, endpoint, json, headerOnly, options), group, bucketId, guildId).ConfigureAwait(false); - stopwatch.Stop(); - - double milliseconds = ToMilliseconds(stopwatch); - await _sentRequestEvent.InvokeAsync(method, endpoint, milliseconds).ConfigureAwait(false); - - return responseStream; - } - private async Task SendMultipartInternalAsync(string method, string endpoint, IReadOnlyDictionary multipartArgs, bool headerOnly, - BucketGroup group, int bucketId, ulong guildId, RequestOptions options = null) - { - CheckState(); - - var stopwatch = Stopwatch.StartNew(); - var responseStream = await RequestQueue.SendAsync(new RestRequest(_restClient, method, endpoint, multipartArgs, headerOnly, options), group, bucketId, guildId).ConfigureAwait(false); - int bytes = headerOnly ? 0 : (int)responseStream.Length; - stopwatch.Stop(); - - double milliseconds = ToMilliseconds(stopwatch); - await _sentRequestEvent.InvokeAsync(method, endpoint, milliseconds).ConfigureAwait(false); - - return responseStream; - } - - //Auth - public async Task ValidateTokenAsync(RequestOptions options = null) - { - await SendAsync("GET", "auth/login", options: options).ConfigureAwait(false); - } - - //Channels - public async Task GetChannelAsync(ulong channelId, RequestOptions options = null) - { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); - - try - { - return await SendAsync("GET", $"channels/{channelId}", options: options).ConfigureAwait(false); - } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } - } - public async Task GetChannelAsync(ulong guildId, ulong channelId, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - Preconditions.NotEqual(channelId, 0, nameof(channelId)); - - try - { - var model = await SendAsync("GET", $"channels/{channelId}", options: options).ConfigureAwait(false); - if (!model.GuildId.IsSpecified || model.GuildId.Value != guildId) - return null; - return model; - } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } - } - public async Task> GetGuildChannelsAsync(ulong guildId, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - - return await SendAsync>("GET", $"guilds/{guildId}/channels", options: options).ConfigureAwait(false); - } - public async Task CreateGuildChannelAsync(ulong guildId, CreateGuildChannelParams args, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - Preconditions.NotNull(args, nameof(args)); - Preconditions.GreaterThan(args._bitrate, 0, nameof(args.Bitrate)); - Preconditions.NotNullOrWhitespace(args._name, nameof(args.Name)); - - return await SendAsync("POST", $"guilds/{guildId}/channels", args, options: options).ConfigureAwait(false); - } - public async Task DeleteChannelAsync(ulong channelId, RequestOptions options = null) - { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); - - return await SendAsync("DELETE", $"channels/{channelId}", options: options).ConfigureAwait(false); - } - public async Task ModifyGuildChannelAsync(ulong channelId, ModifyGuildChannelParams args, RequestOptions options = null) - { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); - Preconditions.NotNull(args, nameof(args)); - Preconditions.AtLeast(args._position, 0, nameof(args.Position)); - Preconditions.NotNullOrEmpty(args._name, nameof(args.Name)); - - return await SendAsync("PATCH", $"channels/{channelId}", args, options: options).ConfigureAwait(false); - } - public async Task ModifyGuildChannelAsync(ulong channelId, ModifyTextChannelParams args, RequestOptions options = null) - { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); - Preconditions.NotNull(args, nameof(args)); - Preconditions.AtLeast(args._position, 0, nameof(args.Position)); - Preconditions.NotNullOrEmpty(args._name, nameof(args.Name)); - - return await SendAsync("PATCH", $"channels/{channelId}", args, options: options).ConfigureAwait(false); - } - public async Task ModifyGuildChannelAsync(ulong channelId, ModifyVoiceChannelParams args, RequestOptions options = null) - { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); - Preconditions.NotNull(args, nameof(args)); - Preconditions.GreaterThan(args._bitrate, 0, nameof(args.Bitrate)); - Preconditions.AtLeast(args._userLimit, 0, nameof(args.Bitrate)); - Preconditions.AtLeast(args._position, 0, nameof(args.Position)); - Preconditions.NotNullOrEmpty(args._name, nameof(args.Name)); - - return await SendAsync("PATCH", $"channels/{channelId}", args, options: options).ConfigureAwait(false); - } - public async Task ModifyGuildChannelsAsync(ulong guildId, IEnumerable args, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - Preconditions.NotNull(args, nameof(args)); - - var channels = args.ToArray(); - switch (channels.Length) - { - case 0: - return; - case 1: - await ModifyGuildChannelAsync(channels[0].Id, new ModifyGuildChannelParams { Position = channels[0].Position }).ConfigureAwait(false); - break; - default: - await SendAsync("PATCH", $"guilds/{guildId}/channels", channels, options: options).ConfigureAwait(false); - break; - } - } - - //Channel Permissions - public async Task ModifyChannelPermissionsAsync(ulong channelId, ulong targetId, ModifyChannelPermissionsParams args, RequestOptions options = null) - { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); - Preconditions.NotEqual(targetId, 0, nameof(targetId)); - Preconditions.NotNull(args, nameof(args)); - - await SendAsync("PUT", $"channels/{channelId}/permissions/{targetId}", args, options: options).ConfigureAwait(false); - } - public async Task DeleteChannelPermissionAsync(ulong channelId, ulong targetId, RequestOptions options = null) - { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); - Preconditions.NotEqual(targetId, 0, nameof(targetId)); - - await SendAsync("DELETE", $"channels/{channelId}/permissions/{targetId}", options: options).ConfigureAwait(false); - } - - //Channel Pins - public async Task AddPinAsync(ulong channelId, ulong messageId, RequestOptions options = null) - { - Preconditions.GreaterThan(channelId, 0, nameof(channelId)); - Preconditions.GreaterThan(messageId, 0, nameof(messageId)); - - await SendAsync("PUT", $"channels/{channelId}/pins/{messageId}", options: options).ConfigureAwait(false); - - } - public async Task RemovePinAsync(ulong channelId, ulong messageId, RequestOptions options = null) - { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); - Preconditions.NotEqual(messageId, 0, nameof(messageId)); - - await SendAsync("DELETE", $"channels/{channelId}/pins/{messageId}", options: options).ConfigureAwait(false); - } - public async Task> GetPinsAsync(ulong channelId, RequestOptions options = null) - { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); - - return await SendAsync>("GET", $"channels/{channelId}/pins", options: options).ConfigureAwait(false); - } - - //Channel Recipients - public async Task AddGroupRecipientAsync(ulong channelId, ulong userId, RequestOptions options = null) - { - Preconditions.GreaterThan(channelId, 0, nameof(channelId)); - Preconditions.GreaterThan(userId, 0, nameof(userId)); - - await SendAsync("PUT", $"channels/{channelId}/recipients/{userId}", options: options).ConfigureAwait(false); - - } - public async Task RemoveGroupRecipientAsync(ulong channelId, ulong userId, RequestOptions options = null) - { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); - Preconditions.NotEqual(userId, 0, nameof(userId)); - - await SendAsync("DELETE", $"channels/{channelId}/recipients/{userId}", options: options).ConfigureAwait(false); - } - - //Guilds - public async Task GetGuildAsync(ulong guildId, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - - try - { - return await SendAsync("GET", $"guilds/{guildId}", options: options).ConfigureAwait(false); - } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } - } - public async Task CreateGuildAsync(CreateGuildParams args, RequestOptions options = null) - { - Preconditions.NotNull(args, nameof(args)); - Preconditions.NotNullOrWhitespace(args.Name, nameof(args.Name)); - Preconditions.NotNullOrWhitespace(args.Region, nameof(args.Region)); - - return await SendAsync("POST", "guilds", args, options: options).ConfigureAwait(false); - } - public async Task DeleteGuildAsync(ulong guildId, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - - return await SendAsync("DELETE", $"guilds/{guildId}", options: options).ConfigureAwait(false); - } - public async Task LeaveGuildAsync(ulong guildId, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - - return await SendAsync("DELETE", $"users/@me/guilds/{guildId}", options: options).ConfigureAwait(false); - } - public async Task ModifyGuildAsync(ulong guildId, ModifyGuildParams args, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - Preconditions.NotNull(args, nameof(args)); - Preconditions.NotEqual(args._afkChannelId, 0, nameof(args.AFKChannelId)); - Preconditions.AtLeast(args._afkTimeout, 0, nameof(args.AFKTimeout)); - Preconditions.NotNullOrEmpty(args._name, nameof(args.Name)); - Preconditions.GreaterThan(args._ownerId, 0, nameof(args.OwnerId)); - Preconditions.NotNull(args._region, nameof(args.Region)); - - return await SendAsync("PATCH", $"guilds/{guildId}", args, options: options).ConfigureAwait(false); - } - public async Task BeginGuildPruneAsync(ulong guildId, GuildPruneParams args, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - Preconditions.NotNull(args, nameof(args)); - Preconditions.AtLeast(args.Days, 0, nameof(args.Days)); - - return await SendAsync("POST", $"guilds/{guildId}/prune", args, options: options).ConfigureAwait(false); - } - public async Task GetGuildPruneCountAsync(ulong guildId, GuildPruneParams args, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - Preconditions.NotNull(args, nameof(args)); - Preconditions.AtLeast(args.Days, 0, nameof(args.Days)); - - return await SendAsync("GET", $"guilds/{guildId}/prune", args, options: options).ConfigureAwait(false); - } - - //Guild Bans - public async Task> GetGuildBansAsync(ulong guildId, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - - return await SendAsync>("GET", $"guilds/{guildId}/bans", options: options).ConfigureAwait(false); - } - public async Task CreateGuildBanAsync(ulong guildId, ulong userId, CreateGuildBanParams args, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - Preconditions.NotEqual(userId, 0, nameof(userId)); - Preconditions.NotNull(args, nameof(args)); - Preconditions.AtLeast(args._deleteMessageDays, 0, nameof(args.DeleteMessageDays)); - - await SendAsync("PUT", $"guilds/{guildId}/bans/{userId}", args, options: options).ConfigureAwait(false); - } - public async Task RemoveGuildBanAsync(ulong guildId, ulong userId, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - Preconditions.NotEqual(userId, 0, nameof(userId)); - - await SendAsync("DELETE", $"guilds/{guildId}/bans/{userId}", options: options).ConfigureAwait(false); - } - - //Guild Embeds - public async Task GetGuildEmbedAsync(ulong guildId, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - - try - { - return await SendAsync("GET", $"guilds/{guildId}/embed", options: options).ConfigureAwait(false); - } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } - } - public async Task ModifyGuildEmbedAsync(ulong guildId, ModifyGuildEmbedParams args, RequestOptions options = null) - { - Preconditions.NotNull(args, nameof(args)); - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - - return await SendAsync("PATCH", $"guilds/{guildId}/embed", args, options: options).ConfigureAwait(false); - } - - //Guild Integrations - public async Task> GetGuildIntegrationsAsync(ulong guildId, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - - return await SendAsync>("GET", $"guilds/{guildId}/integrations", options: options).ConfigureAwait(false); - } - public async Task CreateGuildIntegrationAsync(ulong guildId, CreateGuildIntegrationParams args, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - Preconditions.NotNull(args, nameof(args)); - Preconditions.NotEqual(args.Id, 0, nameof(args.Id)); - - return await SendAsync("POST", $"guilds/{guildId}/integrations", options: options).ConfigureAwait(false); - } - public async Task DeleteGuildIntegrationAsync(ulong guildId, ulong integrationId, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - Preconditions.NotEqual(integrationId, 0, nameof(integrationId)); - - return await SendAsync("DELETE", $"guilds/{guildId}/integrations/{integrationId}", options: options).ConfigureAwait(false); - } - public async Task ModifyGuildIntegrationAsync(ulong guildId, ulong integrationId, ModifyGuildIntegrationParams args, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - Preconditions.NotEqual(integrationId, 0, nameof(integrationId)); - Preconditions.NotNull(args, nameof(args)); - Preconditions.AtLeast(args._expireBehavior, 0, nameof(args.ExpireBehavior)); - Preconditions.AtLeast(args._expireGracePeriod, 0, nameof(args.ExpireGracePeriod)); - - return await SendAsync("PATCH", $"guilds/{guildId}/integrations/{integrationId}", args, options: options).ConfigureAwait(false); - } - public async Task SyncGuildIntegrationAsync(ulong guildId, ulong integrationId, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - Preconditions.NotEqual(integrationId, 0, nameof(integrationId)); - - return await SendAsync("POST", $"guilds/{guildId}/integrations/{integrationId}/sync", options: options).ConfigureAwait(false); - } - - //Guild Invites - public async Task GetInviteAsync(string inviteId, RequestOptions options = null) - { - Preconditions.NotNullOrEmpty(inviteId, nameof(inviteId)); - - //Remove trailing slash - if (inviteId[inviteId.Length - 1] == '/') - inviteId = inviteId.Substring(0, inviteId.Length - 1); - //Remove leading URL - int index = inviteId.LastIndexOf('/'); - if (index >= 0) - inviteId = inviteId.Substring(index + 1); - - try - { - return await SendAsync("GET", $"invites/{inviteId}", options: options).ConfigureAwait(false); - } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } - } - public async Task> GetGuildInvitesAsync(ulong guildId, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - - return await SendAsync>("GET", $"guilds/{guildId}/invites", options: options).ConfigureAwait(false); - } - public async Task GetChannelInvitesAsync(ulong channelId, RequestOptions options = null) - { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); - - return await SendAsync("GET", $"channels/{channelId}/invites", options: options).ConfigureAwait(false); - } - public async Task CreateChannelInviteAsync(ulong channelId, CreateChannelInviteParams args, RequestOptions options = null) - { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); - Preconditions.NotNull(args, nameof(args)); - Preconditions.AtLeast(args._maxAge, 0, nameof(args.MaxAge)); - Preconditions.AtLeast(args._maxUses, 0, nameof(args.MaxUses)); - - return await SendAsync("POST", $"channels/{channelId}/invites", args, options: options).ConfigureAwait(false); - } - public async Task DeleteInviteAsync(string inviteCode, RequestOptions options = null) - { - Preconditions.NotNullOrEmpty(inviteCode, nameof(inviteCode)); - - return await SendAsync("DELETE", $"invites/{inviteCode}", options: options).ConfigureAwait(false); - } - public async Task AcceptInviteAsync(string inviteCode, RequestOptions options = null) - { - Preconditions.NotNullOrEmpty(inviteCode, nameof(inviteCode)); - - await SendAsync("POST", $"invites/{inviteCode}", options: options).ConfigureAwait(false); - } - - //Guild Members - public async Task GetGuildMemberAsync(ulong guildId, ulong userId, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - Preconditions.NotEqual(userId, 0, nameof(userId)); - - try - { - return await SendAsync("GET", $"guilds/{guildId}/members/{userId}", options: options).ConfigureAwait(false); - } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } - } - public async Task> GetGuildMembersAsync(ulong guildId, GetGuildMembersParams args, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - Preconditions.NotNull(args, nameof(args)); - Preconditions.GreaterThan(args._limit, 0, nameof(args.Limit)); - Preconditions.GreaterThan(args._afterUserId, 0, nameof(args.AfterUserId)); - - int limit = args._limit.GetValueOrDefault(int.MaxValue); - ulong afterUserId = args._afterUserId.GetValueOrDefault(0); - - List result; - if (args._limit.IsSpecified) - result = new List((limit + DiscordConfig.MaxUsersPerBatch - 1) / DiscordConfig.MaxUsersPerBatch); - else - result = new List(); - - while (true) - { - int runLimit = (limit >= DiscordConfig.MaxUsersPerBatch) ? DiscordConfig.MaxUsersPerBatch : limit; - string endpoint = $"guilds/{guildId}/members?limit={runLimit}&after={afterUserId}"; - var models = await SendAsync("GET", endpoint, options: options).ConfigureAwait(false); - - //Was this an empty batch? - if (models.Length == 0) break; - - result.Add(models); - - limit -= DiscordConfig.MaxUsersPerBatch; - afterUserId = models[models.Length - 1].User.Id; - - //Was this an incomplete (the last) batch? - if (models.Length != DiscordConfig.MaxUsersPerBatch) break; - } - - if (result.Count > 1) - return result.SelectMany(x => x).ToImmutableArray(); - else if (result.Count == 1) - return result[0]; - else - return ImmutableArray.Create(); - } - public async Task RemoveGuildMemberAsync(ulong guildId, ulong userId, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - Preconditions.NotEqual(userId, 0, nameof(userId)); - - await SendAsync("DELETE", $"guilds/{guildId}/members/{userId}", options: options).ConfigureAwait(false); - } - public async Task ModifyGuildMemberAsync(ulong guildId, ulong userId, ModifyGuildMemberParams args, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - Preconditions.NotEqual(userId, 0, nameof(userId)); - Preconditions.NotNull(args, nameof(args)); - - await SendAsync("PATCH", $"guilds/{guildId}/members/{userId}", args, GuildBucket.ModifyMember, guildId, options: options).ConfigureAwait(false); - } - - //Guild Roles - public async Task> GetGuildRolesAsync(ulong guildId, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - - return await SendAsync>("GET", $"guilds/{guildId}/roles", options: options).ConfigureAwait(false); - } - public async Task CreateGuildRoleAsync(ulong guildId, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - - return await SendAsync("POST", $"guilds/{guildId}/roles", options: options).ConfigureAwait(false); - } - public async Task DeleteGuildRoleAsync(ulong guildId, ulong roleId, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - Preconditions.NotEqual(roleId, 0, nameof(roleId)); - - await SendAsync("DELETE", $"guilds/{guildId}/roles/{roleId}", options: options).ConfigureAwait(false); - } - public async Task ModifyGuildRoleAsync(ulong guildId, ulong roleId, ModifyGuildRoleParams args, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - Preconditions.NotEqual(roleId, 0, nameof(roleId)); - Preconditions.NotNull(args, nameof(args)); - Preconditions.AtLeast(args._color, 0, nameof(args.Color)); - Preconditions.NotNullOrEmpty(args._name, nameof(args.Name)); - Preconditions.AtLeast(args._position, 0, nameof(args.Position)); - - return await SendAsync("PATCH", $"guilds/{guildId}/roles/{roleId}", args, options: options).ConfigureAwait(false); - } - public async Task> ModifyGuildRolesAsync(ulong guildId, IEnumerable args, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - Preconditions.NotNull(args, nameof(args)); - - var roles = args.ToArray(); - switch (roles.Length) - { - case 0: - return ImmutableArray.Create(); - case 1: - return ImmutableArray.Create(await ModifyGuildRoleAsync(guildId, roles[0].Id, roles[0]).ConfigureAwait(false)); - default: - return await SendAsync>("PATCH", $"guilds/{guildId}/roles", args, options: options).ConfigureAwait(false); - } - } - - //Messages - public async Task GetChannelMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) - { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); - Preconditions.NotEqual(messageId, 0, nameof(messageId)); - - try - { - return await SendAsync("GET", $"channels/{channelId}/messages/{messageId}", options: options).ConfigureAwait(false); - } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } - } - public async Task> GetChannelMessagesAsync(ulong channelId, GetChannelMessagesParams args, RequestOptions options = null) - { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); - Preconditions.NotNull(args, nameof(args)); - Preconditions.AtLeast(args.Limit, 0, nameof(args.Limit)); - - int limit = args.Limit; - ulong? relativeId = args._relativeMessageId.IsSpecified ? args._relativeMessageId.Value : (ulong?)null; - string relativeDir; - - switch (args.RelativeDirection) - { - case Direction.Before: - default: - relativeDir = "before"; - break; - case Direction.After: - relativeDir = "after"; - break; - case Direction.Around: - relativeDir = "around"; - break; - } - - int runs = (limit + DiscordConfig.MaxMessagesPerBatch - 1) / DiscordConfig.MaxMessagesPerBatch; - int lastRunCount = limit - (runs - 1) * DiscordConfig.MaxMessagesPerBatch; - var result = new API.Message[runs][]; - - int i = 0; - for (; i < runs; i++) - { - int runCount = i == (runs - 1) ? lastRunCount : DiscordConfig.MaxMessagesPerBatch; - string endpoint; - if (relativeId != null) - endpoint = $"channels/{channelId}/messages?limit={runCount}&{relativeDir}={relativeId}"; - else - endpoint = $"channels/{channelId}/messages?limit={runCount}"; - var models = await SendAsync("GET", endpoint, options: options).ConfigureAwait(false); - - //Was this an empty batch? - if (models.Length == 0) break; - - //We can't assume these messages to be sorted by id (fails in rare cases), lets search for the highest/lowest id ourselves - switch (args.RelativeDirection) - { - case Direction.Before: - case Direction.Around: - default: - result[i] = models; - relativeId = ulong.MaxValue; - //Lowest id *should* be the last one - for (int j = models.Length - 1; j >= 0; j--) - { - if (models[j].Id < relativeId.Value) - relativeId = models[j].Id; - } - break; - case Direction.After: - result[runs - i - 1] = models; - relativeId = ulong.MinValue; - //Highest id *should* be the first one - for (int j = 0; j < models.Length; j++) - { - if (models[j].Id > relativeId.Value) - relativeId = models[j].Id; - } - break; - } - - //Was this an incomplete (the last) batch? - if (models.Length != DiscordConfig.MaxMessagesPerBatch) { i++; break; } - } - - if (i > 1) - { - switch (args.RelativeDirection) - { - case Direction.Before: - case Direction.Around: - default: - return result.Take(i).SelectMany(x => x).ToImmutableArray(); - case Direction.After: - return result.Skip(runs - i).Take(i).SelectMany(x => x).ToImmutableArray(); - } - } - else if (i == 1) - { - switch (args.RelativeDirection) - { - case Direction.Before: - case Direction.Around: - default: - return result[0]; - case Direction.After: - return result[runs - 1]; - } - } - else - return ImmutableArray.Create(); - } - public Task CreateMessageAsync(ulong guildId, ulong channelId, CreateMessageParams args, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - - return CreateMessageInternalAsync(guildId, channelId, args); - } - public Task CreateDMMessageAsync(ulong channelId, CreateMessageParams args, RequestOptions options = null) - { - return CreateMessageInternalAsync(0, channelId, args); - } - private async Task CreateMessageInternalAsync(ulong guildId, ulong channelId, CreateMessageParams args, RequestOptions options = null) - { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); - Preconditions.NotNull(args, nameof(args)); - Preconditions.NotNullOrEmpty(args._content, nameof(args.Content)); - if (args._content.Length > DiscordConfig.MaxMessageSize) - throw new ArgumentException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); - - if (guildId != 0) - return await SendAsync("POST", $"channels/{channelId}/messages", args, GuildBucket.SendEditMessage, guildId, options: options).ConfigureAwait(false); - else - return await SendAsync("POST", $"channels/{channelId}/messages", args, GlobalBucket.DirectMessage, options: options).ConfigureAwait(false); - } - public Task UploadFileAsync(ulong guildId, ulong channelId, UploadFileParams args, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - - return UploadFileInternalAsync(guildId, channelId, args); - } - public Task UploadDMFileAsync(ulong channelId, UploadFileParams args, RequestOptions options = null) - { - return UploadFileInternalAsync(0, channelId, args); - } - private async Task UploadFileInternalAsync(ulong guildId, ulong channelId, UploadFileParams args, RequestOptions options = null) - { - Preconditions.NotNull(args, nameof(args)); - Preconditions.NotEqual(channelId, 0, nameof(channelId)); - - if (args._content.GetValueOrDefault(null) == null) - args._content = ""; - else if (args._content.IsSpecified) - { - if (args._content.Value == null) - args._content = ""; - if (args._content.Value?.Length > DiscordConfig.MaxMessageSize) - throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); - } - - if (guildId != 0) - return await SendMultipartAsync("POST", $"channels/{channelId}/messages", args.ToDictionary(), GuildBucket.SendEditMessage, guildId, options: options).ConfigureAwait(false); - else - return await SendMultipartAsync("POST", $"channels/{channelId}/messages", args.ToDictionary(), GlobalBucket.DirectMessage, options: options).ConfigureAwait(false); - } - public Task DeleteMessageAsync(ulong guildId, ulong channelId, ulong messageId, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - - return DeleteMessageInternalAsync(guildId, channelId, messageId); - } - public Task DeleteDMMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) - { - return DeleteMessageInternalAsync(0, channelId, messageId); - } - private async Task DeleteMessageInternalAsync(ulong guildId, ulong channelId, ulong messageId, RequestOptions options = null) - { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); - Preconditions.NotEqual(messageId, 0, nameof(messageId)); - - if (guildId != 0) - await SendAsync("DELETE", $"channels/{channelId}/messages/{messageId}", GuildBucket.DeleteMessage, guildId, options: options).ConfigureAwait(false); - else - await SendAsync("DELETE", $"channels/{channelId}/messages/{messageId}", options: options).ConfigureAwait(false); - } - public Task DeleteMessagesAsync(ulong guildId, ulong channelId, DeleteMessagesParams args, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - - return DeleteMessagesInternalAsync(guildId, channelId, args); - } - public Task DeleteDMMessagesAsync(ulong channelId, DeleteMessagesParams args, RequestOptions options = null) - { - return DeleteMessagesInternalAsync(0, channelId, args); - } - private async Task DeleteMessagesInternalAsync(ulong guildId, ulong channelId, DeleteMessagesParams args, RequestOptions options = null) - { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); - Preconditions.NotNull(args, nameof(args)); - - var messageIds = args._messages; - Preconditions.NotNull(args._messages, nameof(args.MessageIds)); - Preconditions.AtMost(messageIds.Length, 100, nameof(messageIds.Length)); - - switch (messageIds.Length) - { - case 0: - return; - case 1: - await DeleteMessageInternalAsync(guildId, channelId, messageIds[0]).ConfigureAwait(false); - break; - default: - if (guildId != 0) - await SendAsync("POST", $"channels/{channelId}/messages/bulk_delete", args, GuildBucket.DeleteMessages, guildId, options: options).ConfigureAwait(false); - else - await SendAsync("POST", $"channels/{channelId}/messages/bulk_delete", args, options: options).ConfigureAwait(false); - break; - } - } - public Task ModifyMessageAsync(ulong guildId, ulong channelId, ulong messageId, ModifyMessageParams args, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - - return ModifyMessageInternalAsync(guildId, channelId, messageId, args); - } - public Task ModifyDMMessageAsync(ulong channelId, ulong messageId, ModifyMessageParams args, RequestOptions options = null) - { - return ModifyMessageInternalAsync(0, channelId, messageId, args); - } - private async Task ModifyMessageInternalAsync(ulong guildId, ulong channelId, ulong messageId, ModifyMessageParams args, RequestOptions options = null) - { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); - Preconditions.NotEqual(messageId, 0, nameof(messageId)); - Preconditions.NotNull(args, nameof(args)); - if (args._content.IsSpecified) - { - Preconditions.NotNullOrEmpty(args._content, nameof(args.Content)); - if (args._content.Value.Length > DiscordConfig.MaxMessageSize) - throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); - } - - if (guildId != 0) - return await SendAsync("PATCH", $"channels/{channelId}/messages/{messageId}", args, GuildBucket.SendEditMessage, guildId, options: options).ConfigureAwait(false); - else - return await SendAsync("PATCH", $"channels/{channelId}/messages/{messageId}", args, options: options).ConfigureAwait(false); - } - public async Task AckMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) - { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); - Preconditions.NotEqual(messageId, 0, nameof(messageId)); - - await SendAsync("POST", $"channels/{channelId}/messages/{messageId}/ack", options: options).ConfigureAwait(false); - } - public async Task TriggerTypingIndicatorAsync(ulong channelId, RequestOptions options = null) - { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); - - await SendAsync("POST", $"channels/{channelId}/typing", options: options).ConfigureAwait(false); - } - - //Users - public async Task GetUserAsync(ulong userId, RequestOptions options = null) - { - Preconditions.NotEqual(userId, 0, nameof(userId)); - - try - { - return await SendAsync("GET", $"users/{userId}", options: options).ConfigureAwait(false); - } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } - } - public async Task GetUserAsync(string username, string discriminator, RequestOptions options = null) - { - Preconditions.NotNullOrEmpty(username, nameof(username)); - Preconditions.NotNullOrEmpty(discriminator, nameof(discriminator)); - - try - { - var models = await QueryUsersAsync($"{username}#{discriminator}", 1, options: options).ConfigureAwait(false); - return models.FirstOrDefault(); - } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } - } - public async Task> QueryUsersAsync(string query, int limit, RequestOptions options = null) - { - Preconditions.NotNullOrEmpty(query, nameof(query)); - Preconditions.AtLeast(limit, 0, nameof(limit)); - - return await SendAsync>("GET", $"users?q={Uri.EscapeDataString(query)}&limit={limit}", options: options).ConfigureAwait(false); - } - - //Current User/DMs - public async Task GetMyUserAsync(RequestOptions options = null) - { - return await SendAsync("GET", "users/@me", options: options).ConfigureAwait(false); - } - public async Task> GetMyConnectionsAsync(RequestOptions options = null) - { - return await SendAsync>("GET", "users/@me/connections", options: options).ConfigureAwait(false); - } - public async Task> GetMyPrivateChannelsAsync(RequestOptions options = null) - { - return await SendAsync>("GET", "users/@me/channels", options: options).ConfigureAwait(false); - } - public async Task> GetMyGuildsAsync(RequestOptions options = null) - { - return await SendAsync>("GET", "users/@me/guilds", options: options).ConfigureAwait(false); - } - public async Task GetMyApplicationAsync(RequestOptions options = null) - { - return await SendAsync("GET", "oauth2/applications/@me", options: options).ConfigureAwait(false); - } - public async Task ModifySelfAsync(ModifyCurrentUserParams args, RequestOptions options = null) - { - Preconditions.NotNull(args, nameof(args)); - Preconditions.NotNullOrEmpty(args._username, nameof(args.Username)); - - return await SendAsync("PATCH", "users/@me", args, options: options).ConfigureAwait(false); - } - public async Task ModifyMyNickAsync(ulong guildId, ModifyCurrentUserNickParams args, RequestOptions options = null) - { - Preconditions.NotNull(args, nameof(args)); - Preconditions.NotNull(args.Nickname, nameof(args.Nickname)); - - await SendAsync("PATCH", $"guilds/{guildId}/members/@me/nick", args, options: options).ConfigureAwait(false); - } - public async Task CreateDMChannelAsync(CreateDMChannelParams args, RequestOptions options = null) - { - Preconditions.NotNull(args, nameof(args)); - Preconditions.GreaterThan(args._recipientId, 0, nameof(args.Recipient)); - - return await SendAsync("POST", $"users/@me/channels", args, options: options).ConfigureAwait(false); - } - - //Voice Regions - public async Task> GetVoiceRegionsAsync(RequestOptions options = null) - { - return await SendAsync>("GET", "voice/regions", options: options).ConfigureAwait(false); - } - public async Task> GetGuildVoiceRegionsAsync(ulong guildId, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - - return await SendAsync>("GET", $"guilds/{guildId}/regions", options: options).ConfigureAwait(false); - } - - //Helpers - protected void CheckState() - { - if (LoginState != LoginState.LoggedIn) - throw new InvalidOperationException("Client is not logged in."); - } - protected static double ToMilliseconds(Stopwatch stopwatch) => Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); - protected string SerializeJson(object value) - { - var sb = new StringBuilder(256); - using (TextWriter text = new StringWriter(sb, CultureInfo.InvariantCulture)) - using (JsonWriter writer = new JsonTextWriter(text)) - _serializer.Serialize(writer, value); - return sb.ToString(); - } - protected T DeserializeJson(Stream jsonStream) - { - using (TextReader text = new StreamReader(jsonStream)) - using (JsonReader reader = new JsonTextReader(text)) - return _serializer.Deserialize(reader); - } - } -} diff --git a/src/Discord.Net/API/Rest/CreateChannelInviteParams.cs b/src/Discord.Net/API/Rest/CreateChannelInviteParams.cs deleted file mode 100644 index 0899a7e68..000000000 --- a/src/Discord.Net/API/Rest/CreateChannelInviteParams.cs +++ /dev/null @@ -1,21 +0,0 @@ -#pragma warning disable CS1591 -using Newtonsoft.Json; - -namespace Discord.API.Rest -{ - [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class CreateChannelInviteParams - { - [JsonProperty("max_age")] - internal Optional _maxAge { get; set; } - public int MaxAge { set { _maxAge = value; } } - - [JsonProperty("max_uses")] - internal Optional _maxUses { get; set; } - public int MaxUses { set { _maxUses = value; } } - - [JsonProperty("temporary")] - internal Optional _temporary { get; set; } - public bool Temporary { set { _temporary = value; } } - } -} diff --git a/src/Discord.Net/API/Rest/CreateGuildBanParams.cs b/src/Discord.Net/API/Rest/CreateGuildBanParams.cs deleted file mode 100644 index 3b8fce473..000000000 --- a/src/Discord.Net/API/Rest/CreateGuildBanParams.cs +++ /dev/null @@ -1,13 +0,0 @@ -#pragma warning disable CS1591 -using Newtonsoft.Json; - -namespace Discord.API.Rest -{ - [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class CreateGuildBanParams - { - [JsonProperty("delete-message-days")] - internal Optional _deleteMessageDays { get; set; } - public int DeleteMessageDays { set { _deleteMessageDays = value; } } - } -} diff --git a/src/Discord.Net/API/Rest/CreateGuildChannelParams.cs b/src/Discord.Net/API/Rest/CreateGuildChannelParams.cs deleted file mode 100644 index fe1428cf8..000000000 --- a/src/Discord.Net/API/Rest/CreateGuildChannelParams.cs +++ /dev/null @@ -1,21 +0,0 @@ -#pragma warning disable CS1591 -using Newtonsoft.Json; - -namespace Discord.API.Rest -{ - [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class CreateGuildChannelParams - { - [JsonProperty("name")] - internal string _name { get; set; } - public string Name { set { _name = value; } } - - [JsonProperty("type")] - internal ChannelType _type { get; set; } - public ChannelType Type { set { _type = value; } } - - [JsonProperty("bitrate")] - internal Optional _bitrate { get; set; } - public int Bitrate { set { _bitrate = value; } } - } -} diff --git a/src/Discord.Net/API/Rest/CreateMessageParams.cs b/src/Discord.Net/API/Rest/CreateMessageParams.cs deleted file mode 100644 index 1a586c4cf..000000000 --- a/src/Discord.Net/API/Rest/CreateMessageParams.cs +++ /dev/null @@ -1,21 +0,0 @@ -#pragma warning disable CS1591 -using Newtonsoft.Json; - -namespace Discord.API.Rest -{ - [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class CreateMessageParams - { - [JsonProperty("content")] - internal string _content { get; set; } - public string Content { set { _content = value; } } - - [JsonProperty("nonce")] - internal Optional _nonce { get; set; } - public string Nonce { set { _nonce = value; } } - - [JsonProperty("tts")] - internal Optional _tts { get; set; } - public bool IsTTS { set { _tts = value; } } - } -} diff --git a/src/Discord.Net/API/Rest/DeleteMessagesParams.cs b/src/Discord.Net/API/Rest/DeleteMessagesParams.cs deleted file mode 100644 index 89e279f13..000000000 --- a/src/Discord.Net/API/Rest/DeleteMessagesParams.cs +++ /dev/null @@ -1,16 +0,0 @@ -#pragma warning disable CS1591 -using Newtonsoft.Json; -using System.Collections.Generic; -using System.Linq; - -namespace Discord.API.Rest -{ - [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class DeleteMessagesParams - { - [JsonProperty("messages")] - internal ulong[] _messages { get; set; } - public IEnumerable MessageIds { set { _messages = value.ToArray(); } } - public IEnumerable Messages { set { _messages = value.Select(x => x.Id).ToArray(); } } - } -} diff --git a/src/Discord.Net/API/Rest/GetChannelMessagesParams.cs b/src/Discord.Net/API/Rest/GetChannelMessagesParams.cs deleted file mode 100644 index 68660ab01..000000000 --- a/src/Discord.Net/API/Rest/GetChannelMessagesParams.cs +++ /dev/null @@ -1,14 +0,0 @@ -#pragma warning disable CS1591 -namespace Discord.API.Rest -{ - public class GetChannelMessagesParams - { - public int Limit { internal get; set; } = DiscordConfig.MaxMessagesPerBatch; - - public Direction RelativeDirection { internal get; set; } = Direction.Before; - - internal Optional _relativeMessageId { get; set; } - public ulong RelativeMessageId { set { _relativeMessageId = value; } } - public IMessage RelativeMessage { set { _relativeMessageId = value.Id; } } - } -} diff --git a/src/Discord.Net/API/Rest/GetGuildMembersParams.cs b/src/Discord.Net/API/Rest/GetGuildMembersParams.cs deleted file mode 100644 index cefca801e..000000000 --- a/src/Discord.Net/API/Rest/GetGuildMembersParams.cs +++ /dev/null @@ -1,12 +0,0 @@ -#pragma warning disable CS1591 -namespace Discord.API.Rest -{ - public class GetGuildMembersParams - { - internal Optional _limit { get; set; } - public int Limit { set { _limit = value; } } - - internal Optional _afterUserId { get; set; } - public ulong AfterUserId { set { _afterUserId = value; } } - } -} diff --git a/src/Discord.Net/API/Rest/ModifyGuildEmbedParams.cs b/src/Discord.Net/API/Rest/ModifyGuildEmbedParams.cs deleted file mode 100644 index 84a7f28ca..000000000 --- a/src/Discord.Net/API/Rest/ModifyGuildEmbedParams.cs +++ /dev/null @@ -1,18 +0,0 @@ -#pragma warning disable CS1591 -using Newtonsoft.Json; - -namespace Discord.API.Rest -{ - [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class ModifyGuildEmbedParams - { - [JsonProperty("enabled")] - internal Optional _enabled { get; set; } - public bool Enabled { set { _enabled = value; } } - - [JsonProperty("channel")] - internal Optional _channelId { get; set; } - public ulong? ChannelId { set { _channelId = value; } } - public IVoiceChannel Channel { set { _channelId = value != null ? value.Id : (ulong?)null; } } - } -} diff --git a/src/Discord.Net/API/Rest/ModifyGuildIntegrationParams.cs b/src/Discord.Net/API/Rest/ModifyGuildIntegrationParams.cs deleted file mode 100644 index 9b280f8ae..000000000 --- a/src/Discord.Net/API/Rest/ModifyGuildIntegrationParams.cs +++ /dev/null @@ -1,21 +0,0 @@ -#pragma warning disable CS1591 -using Newtonsoft.Json; - -namespace Discord.API.Rest -{ - [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class ModifyGuildIntegrationParams - { - [JsonProperty("expire_behavior")] - internal Optional _expireBehavior { get; set; } - public int ExpireBehavior { set { _expireBehavior = value; } } - - [JsonProperty("expire_grace_period")] - internal Optional _expireGracePeriod { get; set; } - public int ExpireGracePeriod { set { _expireGracePeriod = value; } } - - [JsonProperty("enable_emoticons")] - internal Optional _enableEmoticons { get; set; } - public bool EnableEmoticons { set { _enableEmoticons = value; } } - } -} diff --git a/src/Discord.Net/API/Rest/ModifyGuildMemberParams.cs b/src/Discord.Net/API/Rest/ModifyGuildMemberParams.cs deleted file mode 100644 index 4377efca2..000000000 --- a/src/Discord.Net/API/Rest/ModifyGuildMemberParams.cs +++ /dev/null @@ -1,33 +0,0 @@ -#pragma warning disable CS1591 -using Newtonsoft.Json; -using System.Collections.Generic; -using System.Linq; - -namespace Discord.API.Rest -{ - [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class ModifyGuildMemberParams - { - [JsonProperty("mute")] - internal Optional _mute { get; set; } - public bool Mute { set { _mute = value; } } - - [JsonProperty("deaf")] - internal Optional _deaf { get; set; } - public bool Deaf { set { _deaf = value; } } - - [JsonProperty("nick")] - internal Optional _nickname { get; set; } - public string Nickname { set { _nickname = value; } } - - [JsonProperty("roles")] - internal Optional _roleIds { get; set; } - public IEnumerable RoleIds { set { _roleIds = value.ToArray(); } } - public IEnumerable Roles { set { _roleIds = value.Select(x => x.Id).ToArray(); } } - - [JsonProperty("channel_id")] - internal Optional _channelId { get; set; } - public ulong VoiceChannelId { set { _channelId = value; } } - public IVoiceChannel VoiceChannel { set { _channelId = value.Id; } } - } -} diff --git a/src/Discord.Net/API/Rest/ModifyGuildParams.cs b/src/Discord.Net/API/Rest/ModifyGuildParams.cs deleted file mode 100644 index e7e8219bd..000000000 --- a/src/Discord.Net/API/Rest/ModifyGuildParams.cs +++ /dev/null @@ -1,52 +0,0 @@ -#pragma warning disable CS1591 -using Newtonsoft.Json; -using System.IO; - -namespace Discord.API.Rest -{ - [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class ModifyGuildParams - { - [JsonProperty("username")] - internal Optional _username { get; set; } - public string Username { set { _username = value; } } - - [JsonProperty("name")] - internal Optional _name { get; set; } - public string Name { set { _name = value; } } - - [JsonProperty("region")] - internal Optional _region { get; set; } - public IVoiceRegion Region { set { _region = Optional.Create(value); } } - - [JsonProperty("verification_level")] - internal Optional _verificationLevel { get; set; } - public VerificationLevel VerificationLevel { set { _verificationLevel = value; } } - - [JsonProperty("default_message_notifications")] - internal Optional _defaultMessageNotifications { get; set; } - public DefaultMessageNotifications DefaultMessageNotifications { set { _defaultMessageNotifications = value; } } - - [JsonProperty("afk_timeout")] - internal Optional _afkTimeout { get; set; } - public int AFKTimeout { set { _afkTimeout = value; } } - - [JsonProperty("icon")] - internal Optional _icon { get; set; } - public Stream Icon { set { _icon = value != null ? new Image(value) : (Image?)null; } } - - [JsonProperty("splash")] - internal Optional _splash { get; set; } - public Stream Splash { set { _splash = value != null ? new Image(value) : (Image?)null; } } - - [JsonProperty("afk_channel_id")] - internal Optional _afkChannelId { get; set; } - public ulong? AFKChannelId { set { _afkChannelId = value; } } - public IVoiceChannel AFKChannel { set { _afkChannelId = value?.Id; } } - - [JsonProperty("owner_id")] - internal Optional _ownerId { get; set; } - public ulong OwnerId { set { _ownerId = value; } } - public IGuildUser Owner { set { _ownerId = value.Id; } } - } -} diff --git a/src/Discord.Net/API/Rest/ModifyGuildRoleParams.cs b/src/Discord.Net/API/Rest/ModifyGuildRoleParams.cs deleted file mode 100644 index a9e7eeb3d..000000000 --- a/src/Discord.Net/API/Rest/ModifyGuildRoleParams.cs +++ /dev/null @@ -1,29 +0,0 @@ -#pragma warning disable CS1591 -using Newtonsoft.Json; - -namespace Discord.API.Rest -{ - [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - public class ModifyGuildRoleParams - { - [JsonProperty("name")] - internal Optional _name { get; set; } - public string Name { set { _name = value; } } - - [JsonProperty("permissions")] - internal Optional _permissions { get; set; } - public ulong Permissions { set { _permissions = value; } } - - [JsonProperty("position")] - internal Optional _position { get; set; } - public int Position { set { _position = value; } } - - [JsonProperty("color")] - internal Optional _color { get; set; } - public uint Color { set { _color = value; } } - - [JsonProperty("hoist")] - internal Optional _hoist { get; set; } - public bool Hoist { set { _hoist = value; } } - } -} diff --git a/src/Discord.Net/API/Rest/ModifyPresenceParams.cs b/src/Discord.Net/API/Rest/ModifyPresenceParams.cs deleted file mode 100644 index 00d64bfd6..000000000 --- a/src/Discord.Net/API/Rest/ModifyPresenceParams.cs +++ /dev/null @@ -1,12 +0,0 @@ -#pragma warning disable CS1591 -namespace Discord.API.Rest -{ - public class ModifyPresenceParams - { - internal Optional _status { get; set; } - public UserStatus Status { set { _status = value; } } - - internal Optional _game { get; set; } - public Discord.Game Game { set { _game = value; } } - } -} diff --git a/src/Discord.Net/API/Rest/UploadFileParams.cs b/src/Discord.Net/API/Rest/UploadFileParams.cs deleted file mode 100644 index b454068d7..000000000 --- a/src/Discord.Net/API/Rest/UploadFileParams.cs +++ /dev/null @@ -1,42 +0,0 @@ -#pragma warning disable CS1591 -using Discord.Net.Rest; -using System.Collections.Generic; -using System.IO; - -namespace Discord.API.Rest -{ - public class UploadFileParams - { - public Stream File { internal get; set; } - - internal Optional _filename { get; set; } - public string Filename { set { _filename = value; } } - - internal Optional _content { get; set; } - public string Content { set { _content = value; } } - - internal Optional _nonce { get; set; } - public string Nonce { set { _nonce = value; } } - - internal Optional _isTTS { get; set; } - public bool IsTTS { set { _isTTS = value; } } - - public UploadFileParams(Stream file) - { - File = file; - } - - internal IReadOnlyDictionary ToDictionary() - { - var d = new Dictionary(); - d["file"] = new MultipartFile(File, _filename.GetValueOrDefault("unknown.dat")); - if (_content.IsSpecified) - d["content"] = _content.Value; - if (_isTTS.IsSpecified) - d["tts"] = _isTTS.Value.ToString(); - if (_nonce.IsSpecified) - d["nonce"] = _nonce.Value; - return d; - } - } -} diff --git a/src/Discord.Net/API/Rpc/Application.cs b/src/Discord.Net/API/Rpc/Application.cs deleted file mode 100644 index 6cc12a28b..000000000 --- a/src/Discord.Net/API/Rpc/Application.cs +++ /dev/null @@ -1,19 +0,0 @@ -#pragma warning disable CS1591 -using Newtonsoft.Json; - -namespace Discord.API.Rpc -{ - public class Application - { - [JsonProperty("description")] - public string Description { get; set; } - [JsonProperty("icon")] - public string Icon { get; set; } - [JsonProperty("id")] - public ulong Id { get; set; } - [JsonProperty("rpc_origins")] - public string[] RpcOrigins { get; set; } - [JsonProperty("name")] - public string Name { get; set; } - } -} diff --git a/src/Discord.Net/API/Rpc/RpcChannel.cs b/src/Discord.Net/API/Rpc/RpcChannel.cs deleted file mode 100644 index 074672a96..000000000 --- a/src/Discord.Net/API/Rpc/RpcChannel.cs +++ /dev/null @@ -1,11 +0,0 @@ -#pragma warning disable CS1591 -using Newtonsoft.Json; - -namespace Discord.API.Rpc -{ - public class RpcChannel : Channel - { - [JsonProperty("voice_states")] - public VoiceState[] VoiceStates { get; set; } - } -} diff --git a/src/Discord.Net/API/Rpc/RpcGuild.cs b/src/Discord.Net/API/Rpc/RpcGuild.cs deleted file mode 100644 index b470de2ba..000000000 --- a/src/Discord.Net/API/Rpc/RpcGuild.cs +++ /dev/null @@ -1,13 +0,0 @@ -#pragma warning disable CS1591 -using Newtonsoft.Json; - -namespace Discord.API.Rpc -{ - public class RpcGuild : Guild - { - [JsonProperty("online")] - public int Online { get; set; } - [JsonProperty("members")] - public GuildMember[] Members { get; set; } - } -} diff --git a/src/Discord.Net/API/Rpc/VoiceStateEvent.cs b/src/Discord.Net/API/Rpc/VoiceStateEvent.cs deleted file mode 100644 index 445a02b80..000000000 --- a/src/Discord.Net/API/Rpc/VoiceStateEvent.cs +++ /dev/null @@ -1,7 +0,0 @@ -#pragma warning disable CS1591 -namespace Discord.API.Rpc -{ - public class VoiceStateEvent - { - } -} diff --git a/src/Discord.Net/Discord.Net.xproj b/src/Discord.Net/Discord.Net.xproj index 6759e09b4..079338b62 100644 --- a/src/Discord.Net/Discord.Net.xproj +++ b/src/Discord.Net/Discord.Net.xproj @@ -6,11 +6,11 @@ - 91e9e7bd-75c9-4e98-84aa-2c271922e5c2 - Discord + 496db20a-a455-4d01-b6bc-90fe6d7c6b81 + Discord.Net .\obj .\bin\ - v4.5.2 + v4.6.1 2.0 diff --git a/src/Discord.Net/Entities/Channels/IChannel.cs b/src/Discord.Net/Entities/Channels/IChannel.cs deleted file mode 100644 index 53c681b56..000000000 --- a/src/Discord.Net/Entities/Channels/IChannel.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Discord -{ - public interface IChannel : ISnowflakeEntity, IUpdateable - { - /// Gets a collection of all users in this channel. - Task> GetUsersAsync(); - /// Gets a user in this channel with the provided id. - Task GetUserAsync(ulong id); - } -} diff --git a/src/Discord.Net/Entities/Channels/IMessageChannel.cs b/src/Discord.Net/Entities/Channels/IMessageChannel.cs deleted file mode 100644 index a2288b0ee..000000000 --- a/src/Discord.Net/Entities/Channels/IMessageChannel.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; - -namespace Discord -{ - public interface IMessageChannel : IChannel - { - /// Gets all messages in this channel's cache. - IReadOnlyCollection CachedMessages { get; } - - /// Sends a message to this message channel. - Task SendMessageAsync(string text, bool isTTS = false); - /// Sends a file to this text channel, with an optional caption. - Task SendFileAsync(string filePath, string text = null, bool isTTS = false); - /// Sends a file to this text channel, with an optional caption. - Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false); - /// Gets a message from this message channel with the given id, or null if not found. - Task GetMessageAsync(ulong id); - /// Gets the message from this channel's cache with the given id, or null if not found. - IMessage GetCachedMessage(ulong id); - /// Gets the last N messages from this message channel. - Task> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch); - /// Gets a collection of messages in this channel. - Task> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch); - /// Gets a collection of pinned messages in this channel. - Task> GetPinnedMessagesAsync(); - /// Bulk deletes multiple messages. - Task DeleteMessagesAsync(IEnumerable messages); - - /// Broadcasts the "user is typing" message to all users in this channel, lasting 10 seconds. - Task TriggerTypingAsync(); - } -} diff --git a/src/Discord.Net/Entities/Guilds/Ban.cs b/src/Discord.Net/Entities/Guilds/Ban.cs deleted file mode 100644 index 5d9d3df07..000000000 --- a/src/Discord.Net/Entities/Guilds/Ban.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Diagnostics; - -namespace Discord -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public struct Ban - { - public IUser User { get; } - public string Reason { get; } - - public Ban(IUser user, string reason) - { - User = user; - Reason = reason; - } - - public override string ToString() => User.ToString(); - private string DebuggerDisplay => $"{User}: {Reason}"; - } -} diff --git a/src/Discord.Net/Entities/Guilds/Emoji.cs b/src/Discord.Net/Entities/Guilds/Emoji.cs deleted file mode 100644 index 19ee306b0..000000000 --- a/src/Discord.Net/Entities/Guilds/Emoji.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Immutable; -using System.Diagnostics; -using Model = Discord.API.Emoji; - -namespace Discord -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public struct Emoji - { - public ulong Id { get; } - public string Name { get; } - public bool IsManaged { get; } - public bool RequireColons { get; } - public IImmutableList RoleIds { get; } - - public Emoji(Model model) - { - Id = model.Id; - Name = model.Name; - IsManaged = model.Managed; - RequireColons = model.RequireColons; - RoleIds = ImmutableArray.Create(model.Roles); - } - - public override string ToString() => Name; - private string DebuggerDisplay => $"{Name} ({Id})"; - } -} diff --git a/src/Discord.Net/Entities/IEntity.cs b/src/Discord.Net/Entities/IEntity.cs deleted file mode 100644 index d6d97626d..000000000 --- a/src/Discord.Net/Entities/IEntity.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Discord -{ - public interface IEntity - where TId : IEquatable - { - /// Gets the unique identifier for this object. - TId Id { get; } - - //TODO: What do we do when an object is destroyed due to reconnect? This summary isn't correct. - /// Returns true if this object is getting live updates from the DiscordClient. - bool IsAttached { get;} - } -} diff --git a/src/Discord.Net/Entities/Messages/ChannelMentionHandling.cs b/src/Discord.Net/Entities/Messages/ChannelMentionHandling.cs deleted file mode 100644 index 39f9baa6a..000000000 --- a/src/Discord.Net/Entities/Messages/ChannelMentionHandling.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Discord -{ - public enum ChannelMentionHandling - { - Ignore = 0, - Remove, - Name - } -} diff --git a/src/Discord.Net/Entities/Messages/EmbedProvider.cs b/src/Discord.Net/Entities/Messages/EmbedProvider.cs deleted file mode 100644 index 1f1ef6d2d..000000000 --- a/src/Discord.Net/Entities/Messages/EmbedProvider.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Model = Discord.API.EmbedProvider; - -namespace Discord -{ - public struct EmbedProvider - { - public string Name { get; } - public string Url { get; } - - public EmbedProvider(string name, string url) - { - Name = name; - Url = url; - } - internal EmbedProvider(Model model) - : this(model.Name, model.Url) { } - } -} diff --git a/src/Discord.Net/Entities/Messages/EmbedThumbnail.cs b/src/Discord.Net/Entities/Messages/EmbedThumbnail.cs deleted file mode 100644 index 736d7d743..000000000 --- a/src/Discord.Net/Entities/Messages/EmbedThumbnail.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Model = Discord.API.EmbedThumbnail; - -namespace Discord -{ - public struct EmbedThumbnail - { - public string Url { get; } - public string ProxyUrl { get; } - public int? Height { get; } - public int? Width { get; } - - public EmbedThumbnail(string url, string proxyUrl, int? height, int? width) - { - Url = url; - ProxyUrl = proxyUrl; - Height = height; - Width = width; - } - - internal EmbedThumbnail(Model model) - : this( - model.Url, - model.ProxyUrl, - model.Height.IsSpecified ? model.Height.Value : (int?)null, - model.Width.IsSpecified ? model.Width.Value : (int?)null) - { - } - } -} diff --git a/src/Discord.Net/Entities/Messages/IMessage.cs b/src/Discord.Net/Entities/Messages/IMessage.cs deleted file mode 100644 index 0c83b1024..000000000 --- a/src/Discord.Net/Entities/Messages/IMessage.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Discord -{ - public interface IMessage : ISnowflakeEntity, IUpdateable - { - /// Returns true if this message was sent as a text-to-speech message. - bool IsTTS { get; } - /// Returns true if this message was added to its channel's pinned messages. - bool IsPinned { get; } - /// Returns the content for this message. - string Content { get; } - /// Gets the time this message was sent. - DateTimeOffset Timestamp { get; } - /// Gets the time of this message's last edit, if any. - DateTimeOffset? EditedTimestamp { get; } - - /// Gets the channel this message was sent to. - IMessageChannel Channel { get; } - /// Gets the author of this message. - IUser Author { get; } - - /// Returns a collection of all attachments included in this message. - IReadOnlyCollection Attachments { get; } - /// Returns a collection of all embeds included in this message. - IReadOnlyCollection Embeds { get; } - /// Returns a collection of channel ids mentioned in this message. - IReadOnlyCollection MentionedChannelIds { get; } - /// Returns a collection of roles mentioned in this message. - IReadOnlyCollection MentionedRoles { get; } - /// Returns a collection of users mentioned in this message. - IReadOnlyCollection MentionedUsers { get; } - } -} \ No newline at end of file diff --git a/src/Discord.Net/Entities/Messages/ISystemMessage.cs b/src/Discord.Net/Entities/Messages/ISystemMessage.cs deleted file mode 100644 index d2e23d147..000000000 --- a/src/Discord.Net/Entities/Messages/ISystemMessage.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Discord -{ - public interface ISystemMessage : IMessage - { - /// Gets the type of this system message. - MessageType Type { get; } - } -} diff --git a/src/Discord.Net/Entities/Messages/IUserMessage.cs b/src/Discord.Net/Entities/Messages/IUserMessage.cs deleted file mode 100644 index fd170dacb..000000000 --- a/src/Discord.Net/Entities/Messages/IUserMessage.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Discord.API.Rest; -using System; -using System.Threading.Tasks; - -namespace Discord -{ - public interface IUserMessage : IMessage, IDeletable - { - /// Modifies this message. - Task ModifyAsync(Action func); - /// Adds this message to its channel's pinned messages. - Task PinAsync(); - /// Removes this message from its channel's pinned messages. - Task UnpinAsync(); - - /// Transforms this message's text into a human readable form, resolving mentions to that object's name. - string Resolve(int startIndex, int length, - UserMentionHandling userHandling = UserMentionHandling.Name, - ChannelMentionHandling channelHandling = ChannelMentionHandling.Name, - RoleMentionHandling roleHandling = RoleMentionHandling.Name, - EveryoneMentionHandling everyoneHandling = EveryoneMentionHandling.Ignore); - /// Transforms this message's text into a human readable form, resolving mentions to that object's name. - string Resolve( - UserMentionHandling userHandling = UserMentionHandling.Name, - ChannelMentionHandling channelHandling = ChannelMentionHandling.Name, - RoleMentionHandling roleHandling = RoleMentionHandling.Name, - EveryoneMentionHandling everyoneHandling = EveryoneMentionHandling.Ignore); - } -} diff --git a/src/Discord.Net/Entities/Messages/RoleMentionHandling.cs b/src/Discord.Net/Entities/Messages/RoleMentionHandling.cs deleted file mode 100644 index 466cf1fd8..000000000 --- a/src/Discord.Net/Entities/Messages/RoleMentionHandling.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Discord -{ - public enum RoleMentionHandling - { - Ignore = 0, - Remove, - Name - } -} diff --git a/src/Discord.Net/Entities/Messages/UserMentionHandling.cs b/src/Discord.Net/Entities/Messages/UserMentionHandling.cs deleted file mode 100644 index b31a994a2..000000000 --- a/src/Discord.Net/Entities/Messages/UserMentionHandling.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Discord -{ - public enum UserMentionHandling - { - Ignore = 0, - Remove, - Name, - NameAndDiscriminator - } -} diff --git a/src/Discord.Net/Entities/UpdateSource.cs b/src/Discord.Net/Entities/UpdateSource.cs deleted file mode 100644 index 6c56416e7..000000000 --- a/src/Discord.Net/Entities/UpdateSource.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Discord -{ - internal enum UpdateSource - { - Creation, - Rest, - WebSocket - } -} diff --git a/src/Discord.Net/Entities/Users/IGroupUser.cs b/src/Discord.Net/Entities/Users/IGroupUser.cs deleted file mode 100644 index 8ed53616c..000000000 --- a/src/Discord.Net/Entities/Users/IGroupUser.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Threading.Tasks; - -namespace Discord -{ - public interface IGroupUser : IUser - { - /// Kicks this user from this group. - Task KickAsync(); - - /// Returns a private message channel to this user, creating one if it does not already exist. - Task CreateDMChannelAsync(); - } -} diff --git a/src/Discord.Net/Entities/Users/IGuildUser.cs b/src/Discord.Net/Entities/Users/IGuildUser.cs deleted file mode 100644 index 3115b246a..000000000 --- a/src/Discord.Net/Entities/Users/IGuildUser.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Discord.API.Rest; - -namespace Discord -{ - /// A Guild-User pairing. - public interface IGuildUser : IUpdateable, IUser, IVoiceState - { - /// Gets when this user joined this guild. - DateTimeOffset? JoinedAt { get; } - /// Gets the nickname for this user. - string Nickname { get; } - /// Gets the guild-level permissions granted to this user by their roles. - GuildPermissions GuildPermissions { get; } - - /// Gets the guild for this guild-user pair. - IGuild Guild { get; } - /// Returns a collection of the roles this user is a member of in this guild, including the guild's @everyone role. - IReadOnlyCollection Roles { get; } - - /// Gets the level permissions granted to this user to a given channel. - ChannelPermissions GetPermissions(IGuildChannel channel); - - /// Kicks this user from this guild. - Task KickAsync(); - /// Modifies this user's properties in this guild. - Task ModifyAsync(Action func); - - /// Returns a private message channel to this user, creating one if it does not already exist. - Task CreateDMChannelAsync(); - } -} diff --git a/src/Discord.Net/Logging/ILogManager.cs b/src/Discord.Net/Logging/ILogManager.cs deleted file mode 100644 index b244419b9..000000000 --- a/src/Discord.Net/Logging/ILogManager.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Discord.Logging -{ - public interface ILogManager - { - LogSeverity Level { get; } - - Task LogAsync(LogSeverity severity, string source, string message, Exception ex = null); - Task LogAsync(LogSeverity severity, string source, FormattableString message, Exception ex = null); - Task LogAsync(LogSeverity severity, string source, Exception ex); - - Task ErrorAsync(string source, string message, Exception ex = null); - Task ErrorAsync(string source, FormattableString message, Exception ex = null); - Task ErrorAsync(string source, Exception ex); - - Task WarningAsync(string source, string message, Exception ex = null); - Task WarningAsync(string source, FormattableString message, Exception ex = null); - Task WarningAsync(string source, Exception ex); - - Task InfoAsync(string source, string message, Exception ex = null); - Task InfoAsync(string source, FormattableString message, Exception ex = null); - Task InfoAsync(string source, Exception ex); - - Task VerboseAsync(string source, string message, Exception ex = null); - Task VerboseAsync(string source, FormattableString message, Exception ex = null); - Task VerboseAsync(string source, Exception ex); - - Task DebugAsync(string source, string message, Exception ex = null); - Task DebugAsync(string source, FormattableString message, Exception ex = null); - Task DebugAsync(string source, Exception ex); - - ILogger CreateLogger(string name); - } -} diff --git a/src/Discord.Net/Logging/ILogger.cs b/src/Discord.Net/Logging/ILogger.cs deleted file mode 100644 index 207c03dc7..000000000 --- a/src/Discord.Net/Logging/ILogger.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Discord.Logging -{ - public interface ILogger - { - LogSeverity Level { get; } - - Task LogAsync(LogSeverity severity, string message, Exception exception = null); - Task LogAsync(LogSeverity severity, FormattableString message, Exception exception = null); - Task LogAsync(LogSeverity severity, Exception exception); - - Task ErrorAsync(string message, Exception exception = null); - Task ErrorAsync(FormattableString message, Exception exception = null); - Task ErrorAsync(Exception exception); - - Task WarningAsync(string message, Exception exception = null); - Task WarningAsync(FormattableString message, Exception exception = null); - Task WarningAsync(Exception exception); - - Task InfoAsync(string message, Exception exception = null); - Task InfoAsync(FormattableString message, Exception exception = null); - Task InfoAsync(Exception exception); - - Task VerboseAsync(string message, Exception exception = null); - Task VerboseAsync(FormattableString message, Exception exception = null); - Task VerboseAsync(Exception exception); - - Task DebugAsync(string message, Exception exception = null); - Task DebugAsync(FormattableString message, Exception exception = null); - Task DebugAsync(Exception exception); - } -} diff --git a/src/Discord.Net/Net/Queue/Definitions/BucketDefinition.cs b/src/Discord.Net/Net/Queue/Definitions/BucketDefinition.cs deleted file mode 100644 index cfc53b0c8..000000000 --- a/src/Discord.Net/Net/Queue/Definitions/BucketDefinition.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Discord.Net.Queue -{ - public sealed class Bucket - { - /// Gets the unique identifier for this bucket. - public string Id { get; } - /// Gets the name of this bucket. - public string Name { get; } - /// Gets the amount of requests that may be sent per window. - public int WindowCount { get; } - /// Gets the length of this bucket's window, in seconds. - public int WindowSeconds { get; } - /// Gets the type of account this bucket affects. - public BucketTarget Target { get; } - /// Gets this bucket's parent. - public GlobalBucket? Parent { get; } - - internal Bucket(string id, int windowCount, int windowSeconds, BucketTarget target, GlobalBucket? parent = null) - : this(id, id, windowCount, windowSeconds, target, parent) { } - internal Bucket(string id, string name, int windowCount, int windowSeconds, BucketTarget target, GlobalBucket? parent = null) - { - Id = id; - Name = name; - WindowCount = windowCount; - WindowSeconds = windowSeconds; - Target = target; - Parent = parent; - } - } -} diff --git a/src/Discord.Net/Net/Queue/Definitions/BucketGroup.cs b/src/Discord.Net/Net/Queue/Definitions/BucketGroup.cs deleted file mode 100644 index e7b0a4181..000000000 --- a/src/Discord.Net/Net/Queue/Definitions/BucketGroup.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Discord.Net.Queue -{ - public enum BucketGroup - { - Global, - Guild, - Channel - } -} diff --git a/src/Discord.Net/Net/Queue/Definitions/BucketTarget.cs b/src/Discord.Net/Net/Queue/Definitions/BucketTarget.cs deleted file mode 100644 index 0e5a5d552..000000000 --- a/src/Discord.Net/Net/Queue/Definitions/BucketTarget.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Discord.Net.Queue -{ - public enum BucketTarget - { - Client, - Bot, - Both - } -} diff --git a/src/Discord.Net/Net/Queue/Definitions/ChannelBucket.cs b/src/Discord.Net/Net/Queue/Definitions/ChannelBucket.cs deleted file mode 100644 index 235e6dfdf..000000000 --- a/src/Discord.Net/Net/Queue/Definitions/ChannelBucket.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Discord.Net.Queue -{ - public enum ChannelBucket - { - SendEditMessage, - } -} diff --git a/src/Discord.Net/Net/Queue/Definitions/GlobalBucket.cs b/src/Discord.Net/Net/Queue/Definitions/GlobalBucket.cs deleted file mode 100644 index fe95ecb79..000000000 --- a/src/Discord.Net/Net/Queue/Definitions/GlobalBucket.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Discord.Net.Queue -{ - public enum GlobalBucket - { - GeneralRest, - DirectMessage, - SendEditMessage, - - GeneralGateway, - UpdateStatus, - - GeneralRpc - } -} diff --git a/src/Discord.Net/Net/Queue/Definitions/GuildBucket.cs b/src/Discord.Net/Net/Queue/Definitions/GuildBucket.cs deleted file mode 100644 index 4089fd1e7..000000000 --- a/src/Discord.Net/Net/Queue/Definitions/GuildBucket.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Discord.Net.Queue -{ - public enum GuildBucket - { - SendEditMessage, - DeleteMessage, - DeleteMessages, - ModifyMember, - Nickname - } -} diff --git a/src/Discord.Net/Net/Queue/IQueuedRequest.cs b/src/Discord.Net/Net/Queue/IQueuedRequest.cs deleted file mode 100644 index ad0c8fcb6..000000000 --- a/src/Discord.Net/Net/Queue/IQueuedRequest.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Discord.Net.Queue -{ - //TODO: Allow user-supplied canceltoken - //TODO: Allow specifying timeout via DiscordApiClient - internal interface IQueuedRequest - { - CancellationToken CancelToken { get; } - int? TimeoutTick { get; } - - Task SendAsync(); - } -} diff --git a/src/Discord.Net/Net/Queue/RequestQueue.cs b/src/Discord.Net/Net/Queue/RequestQueue.cs deleted file mode 100644 index 37e5f816c..000000000 --- a/src/Discord.Net/Net/Queue/RequestQueue.cs +++ /dev/null @@ -1,189 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Discord.Net.Queue -{ - public class RequestQueue - { - public event Func RateLimitTriggered; - - private readonly static ImmutableDictionary _globalLimits; - private readonly static ImmutableDictionary _guildLimits; - private readonly static ImmutableDictionary _channelLimits; - private readonly SemaphoreSlim _lock; - private readonly RequestQueueBucket[] _globalBuckets; - private readonly ConcurrentDictionary[] _guildBuckets; - private readonly ConcurrentDictionary[] _channelBuckets; - private CancellationTokenSource _clearToken; - private CancellationToken _parentToken; - private CancellationToken _cancelToken; - - static RequestQueue() - { - _globalLimits = new Dictionary - { - //REST - [GlobalBucket.GeneralRest] = new Bucket(null, "rest", 0, 0, BucketTarget.Both), //No Limit - //[GlobalBucket.Login] = new BucketDefinition(1, 1), - [GlobalBucket.DirectMessage] = new Bucket("bot:msg:dm", 5, 5, BucketTarget.Bot), - [GlobalBucket.SendEditMessage] = new Bucket("bot:msg:global", 50, 10, BucketTarget.Bot), - //[GlobalBucket.Username] = new Bucket("bot:msg:global", 2, 3600, BucketTarget.Both), - - //Gateway - [GlobalBucket.GeneralGateway] = new Bucket(null, "gateway", 120, 60, BucketTarget.Both), - [GlobalBucket.UpdateStatus] = new Bucket(null, "status", 5, 1, BucketTarget.Both, GlobalBucket.GeneralGateway), - - //Rpc - [GlobalBucket.GeneralRpc] = new Bucket(null, "rpc", 120, 60, BucketTarget.Both) - }.ToImmutableDictionary(); - - _guildLimits = new Dictionary - { - //REST - [GuildBucket.SendEditMessage] = new Bucket("bot:msg:server", 5, 5, BucketTarget.Bot, GlobalBucket.SendEditMessage), - [GuildBucket.DeleteMessage] = new Bucket("dmsg", 5, 1, BucketTarget.Bot), - [GuildBucket.DeleteMessages] = new Bucket("bdmsg", 1, 1, BucketTarget.Bot), - [GuildBucket.ModifyMember] = new Bucket("guild_member", 10, 10, BucketTarget.Bot), - [GuildBucket.Nickname] = new Bucket("guild_member_nick", 1, 1, BucketTarget.Bot) - }.ToImmutableDictionary(); - - //Client-Only - _channelLimits = new Dictionary - { - //REST - [ChannelBucket.SendEditMessage] = new Bucket("msg", 10, 10, BucketTarget.Client, GlobalBucket.SendEditMessage), - }.ToImmutableDictionary(); - } - - public static Bucket GetBucketInfo(GlobalBucket bucket) => _globalLimits[bucket]; - public static Bucket GetBucketInfo(GuildBucket bucket) => _guildLimits[bucket]; - public static Bucket GetBucketInfo(ChannelBucket bucket) => _channelLimits[bucket]; - - public RequestQueue() - { - _lock = new SemaphoreSlim(1, 1); - - _globalBuckets = new RequestQueueBucket[_globalLimits.Count]; - foreach (var pair in _globalLimits) - { - //var target = _globalLimits[pair.Key].Target; - //if (target == BucketTarget.Both || (target == BucketTarget.Bot && isBot) || (target == BucketTarget.Client && !isBot)) - _globalBuckets[(int)pair.Key] = CreateBucket(pair.Value); - } - - _guildBuckets = new ConcurrentDictionary[_guildLimits.Count]; - for (int i = 0; i < _guildLimits.Count; i++) - { - //var target = _guildLimits[(GuildBucket)i].Target; - //if (target == BucketTarget.Both || (target == BucketTarget.Bot && isBot) || (target == BucketTarget.Client && !isBot)) - _guildBuckets[i] = new ConcurrentDictionary(); - } - - _channelBuckets = new ConcurrentDictionary[_channelLimits.Count]; - for (int i = 0; i < _channelLimits.Count; i++) - { - //var target = _channelLimits[(GuildBucket)i].Target; - //if (target == BucketTarget.Both || (target == BucketTarget.Bot && isBot) || (target == BucketTarget.Client && !isBot)) - _channelBuckets[i] = new ConcurrentDictionary(); - } - - _clearToken = new CancellationTokenSource(); - _cancelToken = CancellationToken.None; - _parentToken = CancellationToken.None; - } - public async Task SetCancelTokenAsync(CancellationToken cancelToken) - { - await _lock.WaitAsync().ConfigureAwait(false); - try - { - _parentToken = cancelToken; - _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _clearToken.Token).Token; - } - finally { _lock.Release(); } - } - - internal async Task SendAsync(RestRequest request, BucketGroup group, int bucketId, ulong objId) - { - request.CancelToken = _cancelToken; - var bucket = GetBucket(group, bucketId, objId); - return await bucket.SendAsync(request).ConfigureAwait(false); - } - internal async Task SendAsync(WebSocketRequest request, BucketGroup group, int bucketId, ulong objId) - { - request.CancelToken = _cancelToken; - var bucket = GetBucket(group, bucketId, objId); - return await bucket.SendAsync(request).ConfigureAwait(false); - } - - private RequestQueueBucket CreateBucket(Bucket def) - { - var parent = def.Parent != null ? GetGlobalBucket(def.Parent.Value) : null; - return new RequestQueueBucket(this, def, parent); - } - - public void DestroyGuildBucket(GuildBucket type, ulong guildId) - { - //Assume this object is locked - RequestQueueBucket bucket; - _guildBuckets[(int)type].TryRemove(guildId, out bucket); - } - public void DestroyChannelBucket(ChannelBucket type, ulong channelId) - { - //Assume this object is locked - RequestQueueBucket bucket; - _channelBuckets[(int)type].TryRemove(channelId, out bucket); - } - - private RequestQueueBucket GetBucket(BucketGroup group, int bucketId, ulong objId) - { - switch (group) - { - case BucketGroup.Global: - return GetGlobalBucket((GlobalBucket)bucketId); - case BucketGroup.Guild: - return GetGuildBucket((GuildBucket)bucketId, objId); - case BucketGroup.Channel: - return GetChannelBucket((ChannelBucket)bucketId, objId); - default: - throw new ArgumentException($"Unknown bucket group: {group}", nameof(group)); - } - } - private RequestQueueBucket GetGlobalBucket(GlobalBucket type) - { - return _globalBuckets[(int)type]; - } - private RequestQueueBucket GetGuildBucket(GuildBucket type, ulong guildId) - { - return _guildBuckets[(int)type].GetOrAdd(guildId, _ => CreateBucket(_guildLimits[type])); - } - private RequestQueueBucket GetChannelBucket(ChannelBucket type, ulong channelId) - { - return _channelBuckets[(int)type].GetOrAdd(channelId, _ => CreateBucket(_channelLimits[type])); - } - - public async Task ClearAsync() - { - await _lock.WaitAsync().ConfigureAwait(false); - try - { - _clearToken?.Cancel(); - _clearToken = new CancellationTokenSource(); - if (_parentToken != null) - _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_clearToken.Token, _parentToken).Token; - else - _cancelToken = _clearToken.Token; - } - finally { _lock.Release(); } - } - - internal async Task RaiseRateLimitTriggered(string id, Bucket bucket, int millis) - { - await RateLimitTriggered.Invoke(id, bucket, millis).ConfigureAwait(false); - } - } -} diff --git a/src/Discord.Net/Net/Queue/RequestQueueBucket.cs b/src/Discord.Net/Net/Queue/RequestQueueBucket.cs deleted file mode 100644 index 2ec9a9e02..000000000 --- a/src/Discord.Net/Net/Queue/RequestQueueBucket.cs +++ /dev/null @@ -1,164 +0,0 @@ -#pragma warning disable CS4014 -using System; -using System.IO; -using System.Net; -using System.Threading; -using System.Threading.Tasks; - -namespace Discord.Net.Queue -{ - internal class RequestQueueBucket - { - private readonly RequestQueue _queue; - private readonly SemaphoreSlim _semaphore; - private readonly object _pauseLock; - private int _pauseEndTick; - private TaskCompletionSource _resumeNotifier; - - public Bucket Definition { get; } - public RequestQueueBucket Parent { get; } - public Task _resetTask { get; } - - public RequestQueueBucket(RequestQueue queue, Bucket definition, RequestQueueBucket parent = null) - { - _queue = queue; - Definition = definition; - if (definition.WindowCount != 0) - _semaphore = new SemaphoreSlim(definition.WindowCount, definition.WindowCount); - Parent = parent; - - _pauseLock = new object(); - _resumeNotifier = new TaskCompletionSource(); - _resumeNotifier.SetResult(0); - } - - public async Task SendAsync(IQueuedRequest request) - { - while (true) - { - try - { - return await SendAsyncInternal(request).ConfigureAwait(false); - } - catch (HttpRateLimitException ex) - { - //When a 429 occurs, we drop all our locks. - //This is generally safe though since 429s actually occuring should be very rare. - RequestQueueBucket bucket; - bool success = FindBucket(ex.BucketId, out bucket); - - await _queue.RaiseRateLimitTriggered(ex.BucketId, success ? bucket.Definition : null, ex.RetryAfterMilliseconds).ConfigureAwait(false); - - bucket.Pause(ex.RetryAfterMilliseconds); - } - } - } - private async Task SendAsyncInternal(IQueuedRequest request) - { - var endTick = request.TimeoutTick; - - //Wait until a spot is open in our bucket - if (_semaphore != null) - await EnterAsync(endTick).ConfigureAwait(false); - try - { - while (true) - { - //Get our 429 state - Task notifier; - int resumeTime; - - lock (_pauseLock) - { - notifier = _resumeNotifier.Task; - resumeTime = _pauseEndTick; - } - - //Are we paused due to a 429? - if (!notifier.IsCompleted) - { - //If the 429 ends after the maximum time for this request, timeout immediately - if (endTick.HasValue && endTick.Value < resumeTime) - throw new TimeoutException(); - - //Wait for the 429 to complete - await notifier.ConfigureAwait(false); - } - - try - { - //If there's a parent bucket, pass this request to them - if (Parent != null) - return await Parent.SendAsyncInternal(request).ConfigureAwait(false); - - //We have all our semaphores, send the request - return await request.SendAsync().ConfigureAwait(false); - } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.BadGateway) - { - continue; - } - } - } - finally - { - //Make sure we put this entry back after WindowMilliseconds - if (_semaphore != null) - QueueExitAsync(); - } - } - - private bool FindBucket(string id, out RequestQueueBucket bucket) - { - //Keep going up until we find a bucket with matching id or we're at the topmost bucket - if (Definition.Id == id) - { - bucket = this; - return true; - } - else if (Parent == null) - { - bucket = this; - return false; - } - else - return Parent.FindBucket(id, out bucket); - } - - private void Pause(int milliseconds) - { - lock (_pauseLock) - { - //If we aren't already waiting on a 429's time, create a new notifier task - if (_resumeNotifier.Task.IsCompleted) - { - _resumeNotifier = new TaskCompletionSource(); - _pauseEndTick = unchecked(Environment.TickCount + milliseconds); - QueueResumeAsync(_resumeNotifier, milliseconds); - } - } - } - private async Task QueueResumeAsync(TaskCompletionSource resumeNotifier, int millis) - { - await Task.Delay(millis).ConfigureAwait(false); - resumeNotifier.TrySetResultAsync(0); - } - - private async Task EnterAsync(int? endTick) - { - if (endTick.HasValue) - { - int millis = unchecked(endTick.Value - Environment.TickCount); - if (millis <= 0 || !await _semaphore.WaitAsync(millis).ConfigureAwait(false)) - throw new TimeoutException(); - } - else - await _semaphore.WaitAsync().ConfigureAwait(false); - } - private async Task QueueExitAsync() - { - await Task.Delay(Definition.WindowSeconds * 1000).ConfigureAwait(false); - _semaphore.Release(); - } - } -} diff --git a/src/Discord.Net/Net/Queue/RestRequest.cs b/src/Discord.Net/Net/Queue/RestRequest.cs deleted file mode 100644 index 59a106e96..000000000 --- a/src/Discord.Net/Net/Queue/RestRequest.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Discord.Net.Rest; -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Discord.Net.Queue -{ - internal class RestRequest : IQueuedRequest - { - public IRestClient Client { get; } - public string Method { get; } - public string Endpoint { get; } - public string Json { get; } - public bool HeaderOnly { get; } - public int? TimeoutTick { get; } - public IReadOnlyDictionary MultipartParams { get; } - public TaskCompletionSource Promise { get; } - public CancellationToken CancelToken { get; set; } - - public bool IsMultipart => MultipartParams != null; - - public RestRequest(IRestClient client, string method, string endpoint, string json, bool headerOnly, RequestOptions options) - : this(client, method, endpoint, headerOnly, options) - { - Json = json; - } - - public RestRequest(IRestClient client, string method, string endpoint, IReadOnlyDictionary multipartParams, bool headerOnly, RequestOptions options) - : this(client, method, endpoint, headerOnly, options) - { - MultipartParams = multipartParams; - } - - private RestRequest(IRestClient client, string method, string endpoint, bool headerOnly, RequestOptions options) - { - if (options == null) - options = RequestOptions.Default; - - Client = client; - Method = method; - Endpoint = endpoint; - Json = null; - MultipartParams = null; - HeaderOnly = headerOnly; - TimeoutTick = options.Timeout.HasValue ? (int?)unchecked(Environment.TickCount + options.Timeout.Value) : null; - Promise = new TaskCompletionSource(); - } - - public async Task SendAsync() - { - if (IsMultipart) - return await Client.SendAsync(Method, Endpoint, MultipartParams, HeaderOnly).ConfigureAwait(false); - else if (Json != null) - return await Client.SendAsync(Method, Endpoint, Json, HeaderOnly).ConfigureAwait(false); - else - return await Client.SendAsync(Method, Endpoint, HeaderOnly).ConfigureAwait(false); - } - } -} diff --git a/src/Discord.Net/Net/Queue/WebSocketRequest.cs b/src/Discord.Net/Net/Queue/WebSocketRequest.cs deleted file mode 100644 index a7cffbcf9..000000000 --- a/src/Discord.Net/Net/Queue/WebSocketRequest.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Discord.Net.WebSockets; -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Discord.Net.Queue -{ - internal class WebSocketRequest : IQueuedRequest - { - public IWebSocketClient Client { get; } - public byte[] Data { get; } - public int DataIndex { get; } - public int DataCount { get; } - public bool IsText { get; } - public int? TimeoutTick { get; } - public TaskCompletionSource Promise { get; } - public CancellationToken CancelToken { get; set; } - - public WebSocketRequest(IWebSocketClient client, byte[] data, bool isText, RequestOptions options) : this(client, data, 0, data.Length, isText, options) { } - public WebSocketRequest(IWebSocketClient client, byte[] data, int index, int count, bool isText, RequestOptions options) - { - if (options == null) - options = RequestOptions.Default; - - Client = client; - Data = data; - DataIndex = index; - DataCount = count; - IsText = isText; - TimeoutTick = options.Timeout.HasValue ? (int?)unchecked(Environment.TickCount + options.Timeout.Value) : null; - Promise = new TaskCompletionSource(); - } - - public async Task SendAsync() - { - await Client.SendAsync(Data, DataIndex, DataCount, IsText).ConfigureAwait(false); - return null; - } - } -} diff --git a/src/Discord.Net/Net/RateLimitException.cs b/src/Discord.Net/Net/RateLimitException.cs deleted file mode 100644 index ff594155a..000000000 --- a/src/Discord.Net/Net/RateLimitException.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Net; - -namespace Discord.Net -{ - public class HttpRateLimitException : HttpException - { - public string BucketId { get; } - public int RetryAfterMilliseconds { get; } - - public HttpRateLimitException(string bucketId, int retryAfterMilliseconds, string reason) - : base((HttpStatusCode)429, reason) - { - BucketId = bucketId; - RetryAfterMilliseconds = retryAfterMilliseconds; - } - } -} diff --git a/src/Discord.Net/Net/Rest/IRestClient.cs b/src/Discord.Net/Net/Rest/IRestClient.cs deleted file mode 100644 index 57b5f91ca..000000000 --- a/src/Discord.Net/Net/Rest/IRestClient.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Discord.Net.Rest -{ - //TODO: Add docstrings - public interface IRestClient - { - void SetHeader(string key, string value); - void SetCancelToken(CancellationToken cancelToken); - - Task SendAsync(string method, string endpoint, bool headerOnly = false); - Task SendAsync(string method, string endpoint, string json, bool headerOnly = false); - Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, bool headerOnly = false); - } -} diff --git a/src/Discord.Net/Rest/DiscordRestClient.cs b/src/Discord.Net/Rest/DiscordRestClient.cs deleted file mode 100644 index 11cf10747..000000000 --- a/src/Discord.Net/Rest/DiscordRestClient.cs +++ /dev/null @@ -1,344 +0,0 @@ -using Discord.API.Rest; -using Discord.Logging; -using Discord.Net; -using Discord.Net.Queue; -using System; -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.Rpc; -using Discord.WebSocket; - -namespace Discord.Rest -{ - public class DiscordRestClient : IDiscordClient - { - private readonly object _eventLock = new object(); - - public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } - private readonly AsyncEvent> _logEvent = new AsyncEvent>(); - - public event Func LoggedIn { add { _loggedInEvent.Add(value); } remove { _loggedInEvent.Remove(value); } } - private readonly AsyncEvent> _loggedInEvent = new AsyncEvent>(); - public event Func LoggedOut { add { _loggedOutEvent.Add(value); } remove { _loggedOutEvent.Remove(value); } } - private readonly AsyncEvent> _loggedOutEvent = new AsyncEvent>(); - - internal readonly ILogger _clientLogger, _restLogger, _queueLogger; - internal readonly SemaphoreSlim _connectionLock; - internal SelfUser _currentUser; - private bool _isFirstLogSub; - internal bool _isDisposed; - - public API.DiscordRestApiClient ApiClient { get; } - internal LogManager LogManager { get; } - public LoginState LoginState { get; private set; } - - /// Creates a new REST-only discord client. - public DiscordRestClient() : this(new DiscordRestConfig()) { } - public DiscordRestClient(DiscordRestConfig config) : this(config, CreateApiClient(config)) { } - /// Creates a new REST-only discord client. - 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; - - _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, requestQueue: new RequestQueue()); - - /// - public async Task LoginAsync(TokenType tokenType, string token, bool validateToken = true) - { - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - await LoginInternalAsync(tokenType, token, validateToken).ConfigureAwait(false); - } - finally { _connectionLock.Release(); } - } - private async Task LoginInternalAsync(TokenType tokenType, string token, bool validateToken) - { - 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); - if (validateToken) - await ValidateTokenAsync(tokenType, token).ConfigureAwait(false); - await OnLoginAsync(tokenType, token).ConfigureAwait(false); - - LoginState = LoginState.LoggedIn; - } - catch (Exception) - { - await LogoutInternalAsync().ConfigureAwait(false); - throw; - } - - await _loggedInEvent.InvokeAsync().ConfigureAwait(false); - } - protected virtual async Task ValidateTokenAsync(TokenType tokenType, string token) - { - try - { - var user = await GetCurrentUserAsync().ConfigureAwait(false); - if (user == null) //Is using a cached DiscordClient - user = new SelfUser(this, await ApiClient.GetMyUserAsync().ConfigureAwait(false)); - - if (user.IsBot && tokenType == TokenType.User) - throw new InvalidOperationException($"A bot token used provided with {nameof(TokenType)}.{nameof(TokenType.User)}"); - else if (!user.IsBot && tokenType == TokenType.Bot) //Discord currently sends a 401 in this case - throw new InvalidOperationException($"A user token used provided with {nameof(TokenType)}.{nameof(TokenType.Bot)}"); - } - catch (HttpException ex) - { - throw new ArgumentException("Token validation failed", nameof(token), ex); - } - } - protected virtual Task OnLoginAsync(TokenType tokenType, string token) => Task.CompletedTask; - - - /// - 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() => Task.CompletedTask; - - /// - public async Task GetApplicationInfoAsync() - { - var model = await ApiClient.GetMyApplicationAsync().ConfigureAwait(false); - return new Application(this, model); - } - - /// - public virtual async Task GetChannelAsync(ulong id) - { - var model = await ApiClient.GetChannelAsync(id).ConfigureAwait(false); - if (model != null) - { - if (model.GuildId.IsSpecified) - { - var guildModel = await ApiClient.GetGuildAsync(model.GuildId.Value).ConfigureAwait(false); - if (guildModel != null) - { - var guild = new Guild(this, guildModel); - return guild.ToChannel(model); - } - } - else if (model.Type == ChannelType.DM) - return new DMChannel(this, new User(model.Recipients.Value[0]), model); - else if (model.Type == ChannelType.Group) - { - var channel = new GroupChannel(this, model); - channel.UpdateUsers(model.Recipients.Value, UpdateSource.Creation); - return channel; - } - else - throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); - } - return null; - } - /// - public virtual async Task> GetPrivateChannelsAsync() - { - var models = await ApiClient.GetMyPrivateChannelsAsync().ConfigureAwait(false); - return models.Select(x => new DMChannel(this, new User(x.Recipients.Value[0]), x)).ToImmutableArray(); - } - - /// - public async Task> GetConnectionsAsync() - { - var models = await ApiClient.GetMyConnectionsAsync().ConfigureAwait(false); - return models.Select(x => new Connection(x)).ToImmutableArray(); - } - - /// - public virtual async Task GetInviteAsync(string inviteId) - { - var model = await ApiClient.GetInviteAsync(inviteId).ConfigureAwait(false); - if (model != null) - return new Invite(this, model); - return null; - } - - /// - public virtual async Task GetGuildAsync(ulong id) - { - var model = await ApiClient.GetGuildAsync(id).ConfigureAwait(false); - if (model != null) - return new Guild(this, model); - return null; - } - /// - public virtual async Task GetGuildEmbedAsync(ulong id) - { - var model = await ApiClient.GetGuildEmbedAsync(id).ConfigureAwait(false); - if (model != null) - return new GuildEmbed(model); - return null; - } - /// - public virtual async Task> GetGuildSummariesAsync() - { - var models = await ApiClient.GetMyGuildsAsync().ConfigureAwait(false); - return models.Select(x => new UserGuild(this, x)).ToImmutableArray(); - } - /// - public virtual async Task> GetGuildsAsync() - { - var summaryModels = await ApiClient.GetMyGuildsAsync().ConfigureAwait(false); - var guilds = ImmutableArray.CreateBuilder(summaryModels.Count); - foreach (var summaryModel in summaryModels) - { - var guildModel = await ApiClient.GetGuildAsync(summaryModel.Id).ConfigureAwait(false); - if (guildModel != null) - guilds.Add(new Guild(this, guildModel)); - } - return guilds.ToImmutable(); - } - /// - public virtual async Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null) - { - var args = new CreateGuildParams(); - var model = await ApiClient.CreateGuildAsync(args).ConfigureAwait(false); - return new Guild(this, model); - } - - /// - public virtual async Task GetUserAsync(ulong id) - { - var model = await ApiClient.GetUserAsync(id).ConfigureAwait(false); - if (model != null) - return new User(model); - return null; - } - /// - public virtual async Task GetUserAsync(string username, string discriminator) - { - var model = await ApiClient.GetUserAsync(username, discriminator).ConfigureAwait(false); - if (model != null) - return new User(model); - return null; - } - /// - public virtual async Task GetCurrentUserAsync() - { - var user = _currentUser; - if (user == null) - { - var model = await ApiClient.GetMyUserAsync().ConfigureAwait(false); - user = new SelfUser(this, model); - _currentUser = user; - } - return user; - } - - /// - public virtual async Task> QueryUsersAsync(string query, int limit) - { - var models = await ApiClient.QueryUsersAsync(query, limit).ConfigureAwait(false); - return models.Select(x => new User(x)).ToImmutableArray(); - } - - /// - public virtual async Task> GetVoiceRegionsAsync() - { - var models = await ApiClient.GetVoiceRegionsAsync().ConfigureAwait(false); - return models.Select(x => new VoiceRegion(x)).ToImmutableArray(); - } - /// - public virtual async Task GetVoiceRegionAsync(string id) - { - var models = await ApiClient.GetVoiceRegionsAsync().ConfigureAwait(false); - return models.Select(x => new VoiceRegion(x)).Where(x => x.Id == id).FirstOrDefault(); - } - - internal virtual void Dispose(bool disposing) - { - if (!_isDisposed) - { - ApiClient.Dispose(); - _isDisposed = true; - } - } - /// - 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); - else - await _clientLogger.InfoAsync($"DiscordClient 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(); - } - } - - ConnectionState IDiscordClient.ConnectionState => ConnectionState.Disconnected; - ILogManager IDiscordClient.LogManager => LogManager; - - Task IDiscordClient.ConnectAsync() { throw new NotSupportedException(); } - Task IDiscordClient.DisconnectAsync() { throw new NotSupportedException(); } - } -} diff --git a/src/Discord.Net/Rest/Entities/Channels/DMChannel.cs b/src/Discord.Net/Rest/Entities/Channels/DMChannel.cs deleted file mode 100644 index 6097a7f00..000000000 --- a/src/Discord.Net/Rest/Entities/Channels/DMChannel.cs +++ /dev/null @@ -1,139 +0,0 @@ -using Discord.API.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 Model = Discord.API.Channel; -using MessageModel = Discord.API.Message; - -namespace Discord.Rest -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - internal class DMChannel : SnowflakeEntity, IDMChannel - { - public override DiscordRestClient Discord { get; } - public IUser Recipient { get; private set; } - - public virtual IReadOnlyCollection CachedMessages => ImmutableArray.Create(); - IReadOnlyCollection IPrivateChannel.Recipients => ImmutableArray.Create(Recipient); - - public DMChannel(DiscordRestClient discord, IUser recipient, Model model) - : base(model.Id) - { - Discord = discord; - Recipient = recipient; - - Update(model, UpdateSource.Creation); - } - public void Update(Model model, UpdateSource source) - { - if (/*source == UpdateSource.Rest && */IsAttached) return; - - (Recipient as User).Update(model.Recipients.Value[0], source); - } - - 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 CloseAsync() - { - await Discord.ApiClient.DeleteChannelAsync(Id).ConfigureAwait(false); - } - - public virtual async Task GetUserAsync(ulong id) - { - var currentUser = await Discord.GetCurrentUserAsync().ConfigureAwait(false); - if (id == Recipient.Id) - return Recipient; - else if (id == currentUser.Id) - return currentUser; - else - return null; - } - public virtual async Task> GetUsersAsync() - { - var currentUser = await Discord.GetCurrentUserAsync().ConfigureAwait(false); - return ImmutableArray.Create(currentUser, Recipient); - } - - public async Task SendMessageAsync(string text, bool isTTS) - { - var args = new CreateMessageParams { Content = text, IsTTS = isTTS }; - var model = await Discord.ApiClient.CreateDMMessageAsync(Id, args).ConfigureAwait(false); - return CreateOutgoingMessage(model); - } - public async Task SendFileAsync(string filePath, string text, bool isTTS) - { - string filename = Path.GetFileName(filePath); - using (var file = File.OpenRead(filePath)) - { - var args = new UploadFileParams(file) { Filename = filename, Content = text, IsTTS = isTTS }; - var model = await Discord.ApiClient.UploadDMFileAsync(Id, args).ConfigureAwait(false); - return CreateOutgoingMessage(model); - } - } - public async Task SendFileAsync(Stream stream, string filename, string text, bool isTTS) - { - var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS }; - var model = await Discord.ApiClient.UploadDMFileAsync(Id, args).ConfigureAwait(false); - return CreateOutgoingMessage(model); - } - public virtual async Task GetMessageAsync(ulong id) - { - var model = await Discord.ApiClient.GetChannelMessageAsync(Id, id).ConfigureAwait(false); - if (model != null) - return CreateIncomingMessage(model); - return null; - } - public virtual async Task> GetMessagesAsync(int limit) - { - var args = new GetChannelMessagesParams { Limit = limit }; - var models = await Discord.ApiClient.GetChannelMessagesAsync(Id, args).ConfigureAwait(false); - return models.Select(x => CreateIncomingMessage(x)).ToImmutableArray(); - } - public virtual async Task> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit) - { - var args = new GetChannelMessagesParams { Limit = limit, RelativeMessageId = fromMessageId, RelativeDirection = dir }; - var models = await Discord.ApiClient.GetChannelMessagesAsync(Id, args).ConfigureAwait(false); - return models.Select(x => CreateIncomingMessage(x)).ToImmutableArray(); - } - public async Task DeleteMessagesAsync(IEnumerable messages) - { - await Discord.ApiClient.DeleteDMMessagesAsync(Id, new DeleteMessagesParams { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false); - } - public async Task> GetPinnedMessagesAsync() - { - var models = await Discord.ApiClient.GetPinsAsync(Id); - return models.Select(x => CreateIncomingMessage(x)).ToImmutableArray(); - } - - public async Task TriggerTypingAsync() - { - await Discord.ApiClient.TriggerTypingIndicatorAsync(Id).ConfigureAwait(false); - } - - private UserMessage CreateOutgoingMessage(MessageModel model) - { - return new UserMessage(this, new User(model.Author.Value), model); - } - private Message CreateIncomingMessage(MessageModel model) - { - if (model.Type == MessageType.Default) - return new UserMessage(this, new User(model.Author.Value), model); - else - return new SystemMessage(this, new User(model.Author.Value), model); - } - - public override string ToString() => '@' + Recipient.ToString(); - private string DebuggerDisplay => $"@{Recipient} ({Id}, DM)"; - - IMessage IMessageChannel.GetCachedMessage(ulong id) => null; - } -} diff --git a/src/Discord.Net/Rest/Entities/Channels/GroupChannel.cs b/src/Discord.Net/Rest/Entities/Channels/GroupChannel.cs deleted file mode 100644 index 3ed544087..000000000 --- a/src/Discord.Net/Rest/Entities/Channels/GroupChannel.cs +++ /dev/null @@ -1,164 +0,0 @@ -using Discord.API.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 Model = Discord.API.Channel; -using MessageModel = Discord.API.Message; - -namespace Discord.Rest -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - internal class GroupChannel : SnowflakeEntity, IGroupChannel - { - protected ConcurrentDictionary _users; - private string _iconId; - - public override DiscordRestClient Discord { get; } - public string Name { get; private set; } - - public IReadOnlyCollection Recipients => _users.ToReadOnlyCollection(); - public virtual IReadOnlyCollection CachedMessages => ImmutableArray.Create(); - public string IconUrl => API.CDN.GetChannelIconUrl(Id, _iconId); - - public GroupChannel(DiscordRestClient discord, Model model) - : base(model.Id) - { - Discord = discord; - - Update(model, UpdateSource.Creation); - } - public virtual void Update(Model model, UpdateSource source) - { - if (source == UpdateSource.Rest && IsAttached) return; - - if (model.Name.IsSpecified) - Name = model.Name.Value; - if (model.Icon.IsSpecified) - _iconId = model.Icon.Value; - - if (source != UpdateSource.Creation && model.Recipients.IsSpecified) - UpdateUsers(model.Recipients.Value, source); - } - - internal virtual void UpdateUsers(API.User[] models, UpdateSource source) - { - if (!IsAttached) - { - var users = new ConcurrentDictionary(1, (int)(models.Length * 1.05)); - for (int i = 0; i < models.Length; i++) - users[models[i].Id] = new GroupUser(this, new User(models[i])); - _users = users; - } - } - - 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 LeaveAsync() - { - await Discord.ApiClient.DeleteChannelAsync(Id).ConfigureAwait(false); - } - - public async Task AddUserAsync(IUser user) - { - await Discord.ApiClient.AddGroupRecipientAsync(Id, user.Id).ConfigureAwait(false); - } - public async Task GetUserAsync(ulong id) - { - GroupUser user; - if (_users.TryGetValue(id, out user)) - return user; - var currentUser = await Discord.GetCurrentUserAsync().ConfigureAwait(false); - if (id == currentUser.Id) - return currentUser; - return null; - } - public async Task> GetUsersAsync() - { - var currentUser = await Discord.GetCurrentUserAsync().ConfigureAwait(false); - return _users.Select(x => x.Value).Concat(ImmutableArray.Create(currentUser)).ToReadOnlyCollection(_users); - } - - public async Task SendMessageAsync(string text, bool isTTS) - { - var args = new CreateMessageParams { Content = text, IsTTS = isTTS }; - var model = await Discord.ApiClient.CreateDMMessageAsync(Id, args).ConfigureAwait(false); - return CreateOutgoingMessage(model); - } - public async Task SendFileAsync(string filePath, string text, bool isTTS) - { - string filename = Path.GetFileName(filePath); - using (var file = File.OpenRead(filePath)) - { - var args = new UploadFileParams(file) { Filename = filename, Content = text, IsTTS = isTTS }; - var model = await Discord.ApiClient.UploadDMFileAsync(Id, args).ConfigureAwait(false); - return CreateOutgoingMessage(model); - } - } - public async Task SendFileAsync(Stream stream, string filename, string text, bool isTTS) - { - var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS }; - var model = await Discord.ApiClient.UploadDMFileAsync(Id, args).ConfigureAwait(false); - return CreateOutgoingMessage(model); - } - public virtual async Task GetMessageAsync(ulong id) - { - var model = await Discord.ApiClient.GetChannelMessageAsync(Id, id).ConfigureAwait(false); - if (model != null) - return CreateIncomingMessage(model); - return null; - } - public virtual async Task> GetMessagesAsync(int limit) - { - var args = new GetChannelMessagesParams { Limit = limit }; - var models = await Discord.ApiClient.GetChannelMessagesAsync(Id, args).ConfigureAwait(false); - return models.Select(x => CreateIncomingMessage(x)).ToImmutableArray(); - } - public virtual async Task> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit) - { - var args = new GetChannelMessagesParams { Limit = limit, RelativeMessageId = fromMessageId, RelativeDirection = dir }; - var models = await Discord.ApiClient.GetChannelMessagesAsync(Id, args).ConfigureAwait(false); - return models.Select(x => CreateIncomingMessage(x)).ToImmutableArray(); - } - public async Task DeleteMessagesAsync(IEnumerable messages) - { - await Discord.ApiClient.DeleteDMMessagesAsync(Id, new DeleteMessagesParams { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false); - } - public async Task> GetPinnedMessagesAsync() - { - var models = await Discord.ApiClient.GetPinsAsync(Id); - return models.Select(x => CreateIncomingMessage(x)).ToImmutableArray(); - } - - public async Task TriggerTypingAsync() - { - await Discord.ApiClient.TriggerTypingIndicatorAsync(Id).ConfigureAwait(false); - } - - private UserMessage CreateOutgoingMessage(MessageModel model) - { - return new UserMessage(this, new User(model.Author.Value), model); - } - private Message CreateIncomingMessage(MessageModel model) - { - if (model.Type == MessageType.Default) - return new UserMessage(this, new User(model.Author.Value), model); - else - return new SystemMessage(this, new User(model.Author.Value), model); - } - - public override string ToString() => Name; - private string DebuggerDisplay => $"@{Name} ({Id}, Group)"; - - IMessage IMessageChannel.GetCachedMessage(ulong id) => null; - } -} diff --git a/src/Discord.Net/Rest/Entities/Channels/GuildChannel.cs b/src/Discord.Net/Rest/Entities/Channels/GuildChannel.cs deleted file mode 100644 index 6b5777570..000000000 --- a/src/Discord.Net/Rest/Entities/Channels/GuildChannel.cs +++ /dev/null @@ -1,157 +0,0 @@ -using Discord.API.Rest; -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; -using Model = Discord.API.Channel; - -namespace Discord.Rest -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - internal abstract class GuildChannel : SnowflakeEntity, IGuildChannel - { - private List _overwrites; //TODO: Is maintaining a list here too expensive? Is this threadsafe? - - public string Name { get; private set; } - public int Position { get; private set; } - - public Guild Guild { get; private set; } - - public override DiscordRestClient Discord => Guild.Discord; - - public GuildChannel(Guild guild, Model model) - : base(model.Id) - { - Guild = guild; - - Update(model, UpdateSource.Creation); - } - public virtual void Update(Model model, UpdateSource source) - { - if (source == UpdateSource.Rest && IsAttached) return; - - Name = model.Name.Value; - Position = model.Position.Value; - - var overwrites = model.PermissionOverwrites.Value; - var newOverwrites = new List(overwrites.Length); - for (int i = 0; i < overwrites.Length; i++) - newOverwrites.Add(new Overwrite(overwrites[i])); - _overwrites = newOverwrites; - } - - public async Task UpdateAsync() - { - if (IsAttached) throw new NotSupportedException(); - - var model = await Discord.ApiClient.GetChannelAsync(Id).ConfigureAwait(false); - Update(model, UpdateSource.Rest); - } - public async Task ModifyAsync(Action 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 GetUserAsync(ulong id); - public abstract Task> GetUsersAsync(); - - public async Task> GetInvitesAsync() - { - var models = await Discord.ApiClient.GetChannelInvitesAsync(Id).ConfigureAwait(false); - return models.Select(x => new InviteMetadata(Discord, x)).ToImmutableArray(); - } - public async Task CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary) - { - var args = new CreateChannelInviteParams - { - MaxAge = maxAge ?? 0, - MaxUses = maxUses ?? 0, - Temporary = isTemporary - }; - var model = await Discord.ApiClient.CreateChannelInviteAsync(Id, args).ConfigureAwait(false); - return new InviteMetadata(Discord, model); - } - - public OverwritePermissions? GetPermissionOverwrite(IUser user) - { - for (int i = 0; i < _overwrites.Count; i++) - { - if (_overwrites[i].TargetId == user.Id) - return _overwrites[i].Permissions; - } - return null; - } - public OverwritePermissions? GetPermissionOverwrite(IRole role) - { - for (int i = 0; i < _overwrites.Count; i++) - { - if (_overwrites[i].TargetId == role.Id) - return _overwrites[i].Permissions; - } - return null; - } - - public async Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions perms) - { - var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue, Type = "member" }; - await Discord.ApiClient.ModifyChannelPermissionsAsync(Id, user.Id, args).ConfigureAwait(false); - _overwrites.Add(new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = user.Id, TargetType = PermissionTarget.User })); - } - public async Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions perms) - { - var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue, Type = "role" }; - await Discord.ApiClient.ModifyChannelPermissionsAsync(Id, role.Id, args).ConfigureAwait(false); - _overwrites.Add(new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = role.Id, TargetType = PermissionTarget.Role })); - } - public async Task RemovePermissionOverwriteAsync(IUser user) - { - await Discord.ApiClient.DeleteChannelPermissionAsync(Id, user.Id).ConfigureAwait(false); - - for (int i = 0; i < _overwrites.Count; i++) - { - if (_overwrites[i].TargetId == user.Id) - { - _overwrites.RemoveAt(i); - return; - } - } - } - public async Task RemovePermissionOverwriteAsync(IRole role) - { - await Discord.ApiClient.DeleteChannelPermissionAsync(Id, role.Id).ConfigureAwait(false); - - for (int i = 0; i < _overwrites.Count; i++) - { - if (_overwrites[i].TargetId == role.Id) - { - _overwrites.RemoveAt(i); - return; - } - } - } - - public override string ToString() => Name; - private string DebuggerDisplay => $"{Name} ({Id})"; - - IGuild IGuildChannel.Guild => Guild; - IReadOnlyCollection IGuildChannel.PermissionOverwrites => _overwrites.AsReadOnly(); - - async Task IChannel.GetUserAsync(ulong id) => await GetUserAsync(id).ConfigureAwait(false); - async Task> IChannel.GetUsersAsync() => await GetUsersAsync().ConfigureAwait(false); - } -} diff --git a/src/Discord.Net/Rest/Entities/Channels/TextChannel.cs b/src/Discord.Net/Rest/Entities/Channels/TextChannel.cs deleted file mode 100644 index 5d97bca7c..000000000 --- a/src/Discord.Net/Rest/Entities/Channels/TextChannel.cs +++ /dev/null @@ -1,133 +0,0 @@ -using Discord.API.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 Model = Discord.API.Channel; -using MessageModel = Discord.API.Message; - -namespace Discord.Rest -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - internal class TextChannel : GuildChannel, ITextChannel - { - public string Topic { get; private set; } - - public string Mention => MentionUtils.Mention(this); - public virtual IReadOnlyCollection CachedMessages => ImmutableArray.Create(); - - public TextChannel(Guild guild, Model model) - : base(guild, model) - { - } - public override void Update(Model model, UpdateSource source) - { - if (source == UpdateSource.Rest && IsAttached) return; - - Topic = model.Topic.Value; - base.Update(model, source); - } - - public async Task ModifyAsync(Action func) - { - if (func == null) throw new NullReferenceException(nameof(func)); - - var args = new ModifyTextChannelParams(); - func(args); - - if (!args._name.IsSpecified) - args._name = Name; - - var model = await Discord.ApiClient.ModifyGuildChannelAsync(Id, args).ConfigureAwait(false); - Update(model, UpdateSource.Rest); - } - - public override async Task GetUserAsync(ulong id) - { - var user = await Guild.GetUserAsync(id).ConfigureAwait(false); - if (user != null && Permissions.GetValue(Permissions.ResolveChannel(user, this, user.GuildPermissions.RawValue), ChannelPermission.ReadMessages)) - return user; - return null; - } - public override async Task> GetUsersAsync() - { - var users = await Guild.GetUsersAsync().ConfigureAwait(false); - return users.Where(x => Permissions.GetValue(Permissions.ResolveChannel(x, this, x.GuildPermissions.RawValue), ChannelPermission.ReadMessages)).ToImmutableArray(); - } - - public async Task SendMessageAsync(string text, bool isTTS) - { - var args = new CreateMessageParams { Content = text, IsTTS = isTTS }; - var model = await Discord.ApiClient.CreateMessageAsync(Guild.Id, Id, args).ConfigureAwait(false); - return CreateOutgoingMessage(model); - } - public async Task SendFileAsync(string filePath, string text, bool isTTS) - { - string filename = Path.GetFileName(filePath); - using (var file = File.OpenRead(filePath)) - { - var args = new UploadFileParams(file) { Filename = filename, Content = text, IsTTS = isTTS }; - var model = await Discord.ApiClient.UploadFileAsync(Guild.Id, Id, args).ConfigureAwait(false); - return CreateOutgoingMessage(model); - } - } - public async Task SendFileAsync(Stream stream, string filename, string text, bool isTTS) - { - var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS }; - var model = await Discord.ApiClient.UploadFileAsync(Guild.Id, Id, args).ConfigureAwait(false); - return CreateOutgoingMessage(model); - } - public virtual async Task GetMessageAsync(ulong id) - { - var model = await Discord.ApiClient.GetChannelMessageAsync(Id, id).ConfigureAwait(false); - if (model != null) - return CreateIncomingMessage(model); - return null; - } - public virtual async Task> GetMessagesAsync(int limit) - { - var args = new GetChannelMessagesParams { Limit = limit }; - var models = await Discord.ApiClient.GetChannelMessagesAsync(Id, args).ConfigureAwait(false); - return models.Select(x => CreateIncomingMessage(x)).ToImmutableArray(); - } - public virtual async Task> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit) - { - var args = new GetChannelMessagesParams { Limit = limit, RelativeMessageId = fromMessageId, RelativeDirection = dir }; - var models = await Discord.ApiClient.GetChannelMessagesAsync(Id, args).ConfigureAwait(false); - return models.Select(x => CreateIncomingMessage(x)).ToImmutableArray(); - } - public async Task DeleteMessagesAsync(IEnumerable messages) - { - await Discord.ApiClient.DeleteMessagesAsync(Guild.Id, Id, new DeleteMessagesParams { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false); - } - public async Task> GetPinnedMessagesAsync() - { - var models = await Discord.ApiClient.GetPinsAsync(Id); - return models.Select(x => CreateIncomingMessage(x)).ToImmutableArray(); - } - - public async Task TriggerTypingAsync() - { - await Discord.ApiClient.TriggerTypingIndicatorAsync(Id).ConfigureAwait(false); - } - - private UserMessage CreateOutgoingMessage(MessageModel model) - { - return new UserMessage(this, new User(model.Author.Value), model); - } - private Message CreateIncomingMessage(MessageModel model) - { - if (model.Type == MessageType.Default) - return new UserMessage(this, new User(model.Author.Value), model); - else - return new SystemMessage(this, new User(model.Author.Value), model); - } - - private string DebuggerDisplay => $"{Name} ({Id}, Text)"; - - IMessage IMessageChannel.GetCachedMessage(ulong id) => null; - } -} diff --git a/src/Discord.Net/Rest/Entities/Channels/VoiceChannel.cs b/src/Discord.Net/Rest/Entities/Channels/VoiceChannel.cs deleted file mode 100644 index abe4fa56c..000000000 --- a/src/Discord.Net/Rest/Entities/Channels/VoiceChannel.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Discord.API.Rest; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Threading.Tasks; -using Model = Discord.API.Channel; -using Discord.Audio; - -namespace Discord.Rest -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - internal class VoiceChannel : GuildChannel, IVoiceChannel - { - public int Bitrate { get; private set; } - public int UserLimit { get; private set; } - - public VoiceChannel(Guild guild, Model model) - : base(guild, model) - { - } - public override void Update(Model model, UpdateSource source) - { - if (source == UpdateSource.Rest && IsAttached) return; - - base.Update(model, source); - Bitrate = model.Bitrate.Value; - UserLimit = model.UserLimit.Value; - } - - public async Task ModifyAsync(Action func) - { - if (func == null) throw new NullReferenceException(nameof(func)); - - var args = new ModifyVoiceChannelParams(); - func(args); - - if (!args._name.IsSpecified) - args._name = Name; - - var model = await Discord.ApiClient.ModifyGuildChannelAsync(Id, args).ConfigureAwait(false); - Update(model, UpdateSource.Rest); - } - - public override Task GetUserAsync(ulong id) - { - throw new NotSupportedException(); - } - public override Task> GetUsersAsync() - { - throw new NotSupportedException(); - } - - public virtual Task ConnectAsync() { throw new NotSupportedException(); } - - private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; - } -} diff --git a/src/Discord.Net/Rest/Entities/Entity.cs b/src/Discord.Net/Rest/Entities/Entity.cs deleted file mode 100644 index 6023626f1..000000000 --- a/src/Discord.Net/Rest/Entities/Entity.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using Discord.Rest; - -namespace Discord.Rest -{ - internal abstract class Entity : IEntity - where T : IEquatable - { - public T Id { get; } - - public abstract DiscordRestClient Discord { get; } - - internal virtual bool IsAttached => false; - bool IEntity.IsAttached => IsAttached; - - public Entity(T id) - { - Id = id; - } - } -} diff --git a/src/Discord.Net/Rest/Entities/Guilds/Guild.cs b/src/Discord.Net/Rest/Entities/Guilds/Guild.cs deleted file mode 100644 index 395504943..000000000 --- a/src/Discord.Net/Rest/Entities/Guilds/Guild.cs +++ /dev/null @@ -1,307 +0,0 @@ -using Discord.API.Rest; -using Discord.Audio; -using Discord.Rest; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; -using EmbedModel = Discord.API.GuildEmbed; -using Model = Discord.API.Guild; -using RoleModel = Discord.API.Role; - -namespace Discord.Rest -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - internal class Guild : SnowflakeEntity, IGuild - { - protected ConcurrentDictionary _roles; - protected string _iconId, _splashId; - - 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 override DiscordRestClient Discord { get; } - 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 ImmutableArray Emojis { get; protected set; } - public ImmutableArray Features { get; protected set; } - - public ulong DefaultChannelId => Id; - public string IconUrl => API.CDN.GetGuildIconUrl(Id, _iconId); - public string SplashUrl => API.CDN.GetGuildSplashUrl(Id, _splashId); - - public Role EveryoneRole => GetRole(Id); - public IReadOnlyCollection Roles => _roles.ToReadOnlyCollection(); - - public Guild(DiscordRestClient discord, Model model) - : base(model.Id) - { - Discord = discord; - - Update(model, UpdateSource.Creation); - } - public void Update(Model model, UpdateSource source) - { - if (source == UpdateSource.Rest && IsAttached) return; - - 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(model.Emojis.Length); - for (int i = 0; i < model.Emojis.Length; i++) - emojis.Add(new Emoji(model.Emojis[i])); - Emojis = emojis.ToImmutableArray(); - } - else - Emojis = ImmutableArray.Create(); - - if (model.Features != null) - Features = model.Features.ToImmutableArray(); - else - Features = ImmutableArray.Create(); - - var roles = new ConcurrentDictionary(1, model.Roles?.Length ?? 0); - if (model.Roles != null) - { - for (int i = 0; i < model.Roles.Length; i++) - roles[model.Roles[i].Id] = new Role(this, model.Roles[i]); - } - _roles = roles; - } - public void Update(EmbedModel model, UpdateSource source) - { - if (source == UpdateSource.Rest && IsAttached) return; - - IsEmbeddable = model.Enabled; - EmbedChannelId = model.ChannelId; - } - public void Update(IEnumerable models, UpdateSource source) - { - if (source == UpdateSource.Rest && IsAttached) return; - - Role role; - foreach (var model in models) - { - if (_roles.TryGetValue(model.Id, out role)) - role.Update(model, UpdateSource.Rest); - } - } - - public async Task UpdateAsync() - { - if (IsAttached) throw new NotSupportedException(); - - var response = await Discord.ApiClient.GetGuildAsync(Id).ConfigureAwait(false); - Update(response, UpdateSource.Rest); - } - public async Task ModifyAsync(Action func) - { - if (func == null) throw new NullReferenceException(nameof(func)); - - var args = new ModifyGuildParams(); - func(args); - - if (args._splash.IsSpecified && _splashId != null) - args._splash = new API.Image(_splashId); - if (args._icon.IsSpecified && _iconId != null) - args._icon = new API.Image(_iconId); - - var model = await Discord.ApiClient.ModifyGuildAsync(Id, args).ConfigureAwait(false); - Update(model, UpdateSource.Rest); - } - public async Task ModifyEmbedAsync(Action func) - { - if (func == null) throw new NullReferenceException(nameof(func)); - - var args = new ModifyGuildEmbedParams(); - func(args); - var model = await Discord.ApiClient.ModifyGuildEmbedAsync(Id, args).ConfigureAwait(false); - Update(model, UpdateSource.Rest); - } - public async Task ModifyChannelsAsync(IEnumerable args) - { - await Discord.ApiClient.ModifyGuildChannelsAsync(Id, args).ConfigureAwait(false); - } - public async Task ModifyRolesAsync(IEnumerable args) - { - var models = await Discord.ApiClient.ModifyGuildRolesAsync(Id, args).ConfigureAwait(false); - Update(models, UpdateSource.Rest); - } - public async Task LeaveAsync() - { - await Discord.ApiClient.LeaveGuildAsync(Id).ConfigureAwait(false); - } - public async Task DeleteAsync() - { - await Discord.ApiClient.DeleteGuildAsync(Id).ConfigureAwait(false); - } - - public async Task> GetBansAsync() - { - var models = await Discord.ApiClient.GetGuildBansAsync(Id).ConfigureAwait(false); - return models.Select(x => new Ban(new User(x.User), x.Reason)).ToImmutableArray(); - } - public Task AddBanAsync(IUser user, int pruneDays = 0) => AddBanAsync(user.Id, pruneDays); - public async Task AddBanAsync(ulong userId, int pruneDays = 0) - { - var args = new CreateGuildBanParams() { DeleteMessageDays = pruneDays }; - await Discord.ApiClient.CreateGuildBanAsync(Id, userId, args).ConfigureAwait(false); - } - public Task RemoveBanAsync(IUser user) => RemoveBanAsync(user.Id); - public async Task RemoveBanAsync(ulong userId) - { - await Discord.ApiClient.RemoveGuildBanAsync(Id, userId).ConfigureAwait(false); - } - - public virtual async Task GetChannelAsync(ulong id) - { - var model = await Discord.ApiClient.GetChannelAsync(Id, id).ConfigureAwait(false); - if (model != null) - return ToChannel(model); - return null; - } - public virtual async Task> GetChannelsAsync() - { - var models = await Discord.ApiClient.GetGuildChannelsAsync(Id).ConfigureAwait(false); - return models.Select(x => ToChannel(x)).ToImmutableArray(); - } - public async Task CreateTextChannelAsync(string name) - { - if (name == null) throw new ArgumentNullException(nameof(name)); - - var args = new CreateGuildChannelParams() { Name = name, Type = ChannelType.Text }; - var model = await Discord.ApiClient.CreateGuildChannelAsync(Id, args).ConfigureAwait(false); - return new TextChannel(this, model); - } - public async Task CreateVoiceChannelAsync(string name) - { - if (name == null) throw new ArgumentNullException(nameof(name)); - - var args = new CreateGuildChannelParams { Name = name, Type = ChannelType.Voice }; - var model = await Discord.ApiClient.CreateGuildChannelAsync(Id, args).ConfigureAwait(false); - return new VoiceChannel(this, model); - } - - public async Task> GetIntegrationsAsync() - { - var models = await Discord.ApiClient.GetGuildIntegrationsAsync(Id).ConfigureAwait(false); - return models.Select(x => new GuildIntegration(this, x)).ToImmutableArray(); - } - public async Task CreateIntegrationAsync(ulong id, string type) - { - var args = new CreateGuildIntegrationParams { Id = id, Type = type }; - var model = await Discord.ApiClient.CreateGuildIntegrationAsync(Id, args).ConfigureAwait(false); - return new GuildIntegration(this, model); - } - - public async Task> GetInvitesAsync() - { - var models = await Discord.ApiClient.GetGuildInvitesAsync(Id).ConfigureAwait(false); - return models.Select(x => new InviteMetadata(Discord, x)).ToImmutableArray(); - } - - public Role GetRole(ulong id) - { - Role result = null; - if (_roles?.TryGetValue(id, out result) == true) - return result; - return null; - } - public async Task CreateRoleAsync(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false) - { - if (name == null) throw new ArgumentNullException(nameof(name)); - - var model = await Discord.ApiClient.CreateGuildRoleAsync(Id).ConfigureAwait(false); - var role = new Role(this, model); - - await role.ModifyAsync(x => - { - x.Name = name; - x.Permissions = (permissions ?? role.Permissions).RawValue; - x.Color = (color ?? Color.Default).RawValue; - x.Hoist = isHoisted; - }).ConfigureAwait(false); - - return role; - } - - public virtual async Task GetUserAsync(ulong id) - { - var model = await Discord.ApiClient.GetGuildMemberAsync(Id, id).ConfigureAwait(false); - if (model != null) - return new GuildUser(this, new User(model.User), model); - return null; - } - public virtual async Task GetCurrentUserAsync() - { - var currentUser = await Discord.GetCurrentUserAsync().ConfigureAwait(false); - return await GetUserAsync(currentUser.Id).ConfigureAwait(false); - } - public virtual async Task> GetUsersAsync() - { - var args = new GetGuildMembersParams(); - var models = await Discord.ApiClient.GetGuildMembersAsync(Id, args).ConfigureAwait(false); - return models.Select(x => new GuildUser(this, new User(x.User), x)).ToImmutableArray(); - } - public async Task PruneUsersAsync(int days = 30, bool simulate = false) - { - var args = new GuildPruneParams() { Days = days }; - GetGuildPruneCountResponse model; - if (simulate) - model = await Discord.ApiClient.GetGuildPruneCountAsync(Id, args).ConfigureAwait(false); - else - model = await Discord.ApiClient.BeginGuildPruneAsync(Id, args).ConfigureAwait(false); - return model.Pruned; - } - public virtual Task DownloadUsersAsync() - { - throw new NotSupportedException(); - } - - internal GuildChannel ToChannel(API.Channel model) - { - switch (model.Type) - { - case ChannelType.Text: - return new TextChannel(this, model); - case ChannelType.Voice: - return new VoiceChannel(this, model); - default: - throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); - } - } - - public override string ToString() => Name; - - private string DebuggerDisplay => $"{Name} ({Id})"; - - bool IGuild.Available => false; - IRole IGuild.EveryoneRole => EveryoneRole; - IReadOnlyCollection IGuild.Emojis => Emojis; - IReadOnlyCollection IGuild.Features => Features; - IAudioClient IGuild.AudioClient => null; - - IRole IGuild.GetRole(ulong id) => GetRole(id); - } -} diff --git a/src/Discord.Net/Rest/Entities/Guilds/VoiceRegion.cs b/src/Discord.Net/Rest/Entities/Guilds/VoiceRegion.cs deleted file mode 100644 index 8f8bbfc53..000000000 --- a/src/Discord.Net/Rest/Entities/Guilds/VoiceRegion.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Diagnostics; -using Model = Discord.API.VoiceRegion; - -namespace Discord.Rest -{ - [DebuggerDisplay("{DebuggerDisplay,nq}")] - internal class VoiceRegion : IVoiceRegion - { - public string Id { get; } - public string Name { get; } - public bool IsVip { get; } - public bool IsOptimal { get; } - public string SampleHostname { get; } - public int SamplePort { get; } - - public VoiceRegion(Model model) - { - Id = model.Id; - Name = model.Name; - IsVip = model.IsVip; - IsOptimal = model.IsOptimal; - SampleHostname = model.SampleHostname; - SamplePort = model.SamplePort; - } - - public override string ToString() => Name; - private string DebuggerDisplay => $"{Name} ({Id}{(IsVip ? ", VIP" : "")}{(IsOptimal ? ", Optimal" : "")})"; - } -} diff --git a/src/Discord.Net/Rest/Entities/Invites/Invite.cs b/src/Discord.Net/Rest/Entities/Invites/Invite.cs deleted file mode 100644 index c69fcbad4..000000000 --- a/src/Discord.Net/Rest/Entities/Invites/Invite.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Discord.Rest; -using System.Diagnostics; -using System.Threading.Tasks; -using Model = Discord.API.Invite; - -namespace Discord.Rest -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - internal class Invite : Entity, IInvite - { - public string ChannelName { get; private set; } - public string GuildName { get; private set; } - - public ulong ChannelId { get; private set; } - public ulong GuildId { get; private set; } - public override DiscordRestClient Discord { get; } - - public string Code => Id; - public string Url => $"{DiscordConfig.InviteUrl}/{Code}"; - - public Invite(DiscordRestClient discord, Model model) - : base(model.Code) - { - Discord = discord; - - Update(model, UpdateSource.Creation); - } - public void Update(Model model, UpdateSource source) - { - if (source == UpdateSource.Rest && IsAttached) return; - - GuildId = model.Guild.Id; - ChannelId = model.Channel.Id; - GuildName = model.Guild.Name; - ChannelName = model.Channel.Name; - } - - public async Task AcceptAsync() - { - await Discord.ApiClient.AcceptInviteAsync(Code).ConfigureAwait(false); - } - public async Task DeleteAsync() - { - await Discord.ApiClient.DeleteInviteAsync(Code).ConfigureAwait(false); - } - - public override string ToString() => Url; - private string DebuggerDisplay => $"{Url} ({GuildName} / {ChannelName})"; - } -} diff --git a/src/Discord.Net/Rest/Entities/Messages/Attachment.cs b/src/Discord.Net/Rest/Entities/Messages/Attachment.cs deleted file mode 100644 index b5a94689e..000000000 --- a/src/Discord.Net/Rest/Entities/Messages/Attachment.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Model = Discord.API.Attachment; - -namespace Discord.Rest -{ - internal class Attachment : IAttachment - { - public ulong Id { get; } - public string Filename { get; } - public string Url { get; } - public string ProxyUrl { get; } - public int Size { get; } - public int? Height { get; } - public int? Width { get; } - - public Attachment(Model model) - { - Id = model.Id; - Filename = model.Filename; - Size = model.Size; - Url = model.Url; - ProxyUrl = model.ProxyUrl; - Height = model.Height.IsSpecified ? model.Height.Value : (int?)null; - Width = model.Width.IsSpecified ? model.Width.Value : (int?)null; - } - } -} diff --git a/src/Discord.Net/Rest/Entities/Messages/Embed.cs b/src/Discord.Net/Rest/Entities/Messages/Embed.cs deleted file mode 100644 index 77c8e28b7..000000000 --- a/src/Discord.Net/Rest/Entities/Messages/Embed.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Model = Discord.API.Embed; - -namespace Discord.Rest -{ - internal class Embed : IEmbed - { - public string Description { get; } - public string Url { get; } - public string Title { get; } - public string Type { get; } - public EmbedProvider Provider { get; } - public EmbedThumbnail Thumbnail { get; } - - public Embed(Model model) - { - Url = model.Url; - Type = model.Type; - Title = model.Title; - Description = model.Description; - - if (model.Provider.IsSpecified) - Provider = new EmbedProvider(model.Provider.Value); - if (model.Thumbnail.IsSpecified) - Thumbnail = new EmbedThumbnail(model.Thumbnail.Value); - } - } -} diff --git a/src/Discord.Net/Rest/Entities/Messages/Message.cs b/src/Discord.Net/Rest/Entities/Messages/Message.cs deleted file mode 100644 index 1b5c025e9..000000000 --- a/src/Discord.Net/Rest/Entities/Messages/Message.cs +++ /dev/null @@ -1,100 +0,0 @@ -using Discord.API.Rest; -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Threading.Tasks; -using Model = Discord.API.Message; - -namespace Discord.Rest -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - internal abstract class Message : SnowflakeEntity, IMessage - { - private long _timestampTicks; - - public IMessageChannel Channel { get; } - public IUser Author { get; } - - public string Content { get; private set; } - - public override DiscordRestClient Discord => (Channel as Entity).Discord; - - public virtual bool IsTTS => false; - public virtual bool IsPinned => false; - public virtual DateTimeOffset? EditedTimestamp => null; - - public virtual IReadOnlyCollection Attachments => ImmutableArray.Create(); - public virtual IReadOnlyCollection Embeds => ImmutableArray.Create(); - public virtual IReadOnlyCollection MentionedChannelIds => ImmutableArray.Create(); - public virtual IReadOnlyCollection MentionedRoles => ImmutableArray.Create(); - public virtual IReadOnlyCollection MentionedUsers => ImmutableArray.Create(); - - public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); - - public Message(IMessageChannel channel, IUser author, Model model) - : base(model.Id) - { - Channel = channel; - Author = author; - - Update(model, UpdateSource.Creation); - } - public virtual void Update(Model model, UpdateSource source) - { - if (source == UpdateSource.Rest && IsAttached) return; - - var guildChannel = Channel as GuildChannel; - var guild = guildChannel?.Guild; - - if (model.Timestamp.IsSpecified) - _timestampTicks = model.Timestamp.Value.UtcTicks; - - if (model.Content.IsSpecified) - Content = model.Content.Value; - } - - public async Task UpdateAsync() - { - if (IsAttached) throw new NotSupportedException(); - - var model = await Discord.ApiClient.GetChannelMessageAsync(Channel.Id, Id).ConfigureAwait(false); - Update(model, UpdateSource.Rest); - } - public async Task ModifyAsync(Action func) - { - if (func == null) throw new NullReferenceException(nameof(func)); - - var args = new ModifyMessageParams(); - func(args); - var guildChannel = Channel as GuildChannel; - - Model model; - if (guildChannel != null) - model = await Discord.ApiClient.ModifyMessageAsync(guildChannel.Guild.Id, Channel.Id, Id, args).ConfigureAwait(false); - else - model = await Discord.ApiClient.ModifyDMMessageAsync(Channel.Id, Id, args).ConfigureAwait(false); - - Update(model, UpdateSource.Rest); - } - public async Task DeleteAsync() - { - var guildChannel = Channel as GuildChannel; - if (guildChannel != null) - await Discord.ApiClient.DeleteMessageAsync(guildChannel.Id, Channel.Id, Id).ConfigureAwait(false); - else - await Discord.ApiClient.DeleteDMMessageAsync(Channel.Id, Id).ConfigureAwait(false); - } - public async Task PinAsync() - { - await Discord.ApiClient.AddPinAsync(Channel.Id, Id).ConfigureAwait(false); - } - public async Task UnpinAsync() - { - await Discord.ApiClient.RemovePinAsync(Channel.Id, Id).ConfigureAwait(false); - } - - public override string ToString() => Content; - private string DebuggerDisplay => $"{Author}: {Content}{(Attachments.Count > 0 ? $" [{Attachments.Count} Attachments]" : "")}"; - } -} diff --git a/src/Discord.Net/Rest/Entities/Messages/SystemMessage.cs b/src/Discord.Net/Rest/Entities/Messages/SystemMessage.cs deleted file mode 100644 index a715c6493..000000000 --- a/src/Discord.Net/Rest/Entities/Messages/SystemMessage.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Diagnostics; -using Model = Discord.API.Message; - -namespace Discord.Rest -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - internal class SystemMessage : Message, ISystemMessage - { - public MessageType Type { get; } - - public override DiscordRestClient Discord => (Channel as Entity).Discord; - - public SystemMessage(IMessageChannel channel, IUser author, Model model) - : base(channel, author, model) - { - Type = model.Type; - } - - public override string ToString() => Content; - private string DebuggerDisplay => $"[{Type}] {Author}{(!string.IsNullOrEmpty(Content) ? $": ({Content})" : "")}"; - } -} diff --git a/src/Discord.Net/Rest/Entities/Messages/UserMessage.cs b/src/Discord.Net/Rest/Entities/Messages/UserMessage.cs deleted file mode 100644 index 06c904232..000000000 --- a/src/Discord.Net/Rest/Entities/Messages/UserMessage.cs +++ /dev/null @@ -1,175 +0,0 @@ -using Discord.API.Rest; -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Threading.Tasks; -using Model = Discord.API.Message; - -namespace Discord.Rest -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - internal class UserMessage : Message, IUserMessage - { - private bool _isMentioningEveryone, _isTTS, _isPinned; - private long? _editedTimestampTicks; - private IReadOnlyCollection _attachments; - private IReadOnlyCollection _embeds; - private IReadOnlyCollection _mentionedChannelIds; - private IReadOnlyCollection _mentionedRoles; - private IReadOnlyCollection _mentionedUsers; - - public override DiscordRestClient Discord => (Channel as Entity).Discord; - public override bool IsTTS => _isTTS; - public override bool IsPinned => _isPinned; - public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks); - - public override IReadOnlyCollection Attachments => _attachments; - public override IReadOnlyCollection Embeds => _embeds; - public override IReadOnlyCollection MentionedChannelIds => _mentionedChannelIds; - public override IReadOnlyCollection MentionedRoles => _mentionedRoles; - public override IReadOnlyCollection MentionedUsers => _mentionedUsers; - - public UserMessage(IMessageChannel channel, IUser author, Model model) - : base(channel, author, model) - { - _mentionedChannelIds = ImmutableArray.Create(); - _mentionedRoles = ImmutableArray.Create(); - _mentionedUsers = ImmutableArray.Create(); - - Update(model, UpdateSource.Creation); - } - public override void Update(Model model, UpdateSource source) - { - if (source == UpdateSource.Rest && IsAttached) return; - - var guildChannel = Channel as GuildChannel; - var guild = guildChannel?.Guild; - - 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 = new Attachment[value.Length]; - for (int i = 0; i < attachments.Length; i++) - attachments[i] = new Attachment(value[i]); - _attachments = ImmutableArray.Create(attachments); - } - else - _attachments = ImmutableArray.Create(); - } - - if (model.Embeds.IsSpecified) - { - var value = model.Embeds.Value; - if (value.Length > 0) - { - var embeds = new Embed[value.Length]; - for (int i = 0; i < embeds.Length; i++) - embeds[i] = new Embed(value[i]); - _embeds = ImmutableArray.Create(embeds); - } - else - _embeds = ImmutableArray.Create(); - } - - ImmutableArray mentions = ImmutableArray.Create(); - if (model.Mentions.IsSpecified) - { - var value = model.Mentions.Value; - if (value.Length > 0) - { - var newMentions = new IUser[value.Length]; - for (int i = 0; i < value.Length; i++) - newMentions[i] = new User(value[i]); - mentions = ImmutableArray.Create(newMentions); - } - } - - if (model.Content.IsSpecified) - { - var text = model.Content.Value; - - if (guildChannel != null) - { - _mentionedUsers = MentionUtils.GetUserMentions(text, Channel, mentions); - _mentionedChannelIds = MentionUtils.GetChannelMentions(text, guildChannel.Guild); - _mentionedRoles = MentionUtils.GetRoleMentions(text, guildChannel.Guild); - } - model.Content = text; - } - - base.Update(model, source); - } - - public async Task UpdateAsync() - { - if (IsAttached) throw new NotSupportedException(); - - var model = await Discord.ApiClient.GetChannelMessageAsync(Channel.Id, Id).ConfigureAwait(false); - Update(model, UpdateSource.Rest); - } - public async Task ModifyAsync(Action func) - { - if (func == null) throw new NullReferenceException(nameof(func)); - - var args = new ModifyMessageParams(); - func(args); - var guildChannel = Channel as GuildChannel; - - Model model; - if (guildChannel != null) - model = await Discord.ApiClient.ModifyMessageAsync(guildChannel.Guild.Id, Channel.Id, Id, args).ConfigureAwait(false); - else - model = await Discord.ApiClient.ModifyDMMessageAsync(Channel.Id, Id, args).ConfigureAwait(false); - - Update(model, UpdateSource.Rest); - } - public async Task DeleteAsync() - { - var guildChannel = Channel as GuildChannel; - if (guildChannel != null) - await Discord.ApiClient.DeleteMessageAsync(guildChannel.Id, Channel.Id, Id).ConfigureAwait(false); - else - await Discord.ApiClient.DeleteDMMessageAsync(Channel.Id, Id).ConfigureAwait(false); - } - public async Task PinAsync() - { - await Discord.ApiClient.AddPinAsync(Channel.Id, Id).ConfigureAwait(false); - } - public async Task UnpinAsync() - { - await Discord.ApiClient.RemovePinAsync(Channel.Id, Id).ConfigureAwait(false); - } - - public string Resolve(int startIndex, int length, UserMentionHandling userHandling, ChannelMentionHandling channelHandling, - RoleMentionHandling roleHandling, EveryoneMentionHandling everyoneHandling) - => Resolve(Content.Substring(startIndex, length), userHandling, channelHandling, roleHandling, everyoneHandling); - public string Resolve(UserMentionHandling userHandling, ChannelMentionHandling channelHandling, - RoleMentionHandling roleHandling, EveryoneMentionHandling everyoneHandling) - => Resolve(Content, userHandling, channelHandling, roleHandling, everyoneHandling); - - private string Resolve(string text, UserMentionHandling userHandling, ChannelMentionHandling channelHandling, - RoleMentionHandling roleHandling, EveryoneMentionHandling everyoneHandling) - { - text = MentionUtils.ResolveUserMentions(text, Channel, MentionedUsers, userHandling); - text = MentionUtils.ResolveChannelMentions(text, (Channel as IGuildChannel)?.Guild, channelHandling); - text = MentionUtils.ResolveRoleMentions(text, MentionedRoles, roleHandling); - text = MentionUtils.ResolveEveryoneMentions(text, everyoneHandling); - return text; - } - - public override string ToString() => Content; - private string DebuggerDisplay => $"{Author}: {Content}{(Attachments.Count > 0 ? $" [{Attachments.Count} Attachments]" : "")}"; - } -} diff --git a/src/Discord.Net/Rest/Entities/Roles/Role.cs b/src/Discord.Net/Rest/Entities/Roles/Role.cs deleted file mode 100644 index dcb198cd4..000000000 --- a/src/Discord.Net/Rest/Entities/Roles/Role.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Discord.API.Rest; -using Discord.Rest; -using System; -using System.Diagnostics; -using System.Threading.Tasks; -using Model = Discord.API.Role; - -namespace Discord.Rest -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - internal class Role : SnowflakeEntity, IRole, IMentionable - { - public Guild Guild { get; } - - public Color Color { get; private set; } - public bool IsHoisted { get; private set; } - public bool IsManaged { get; private set; } - public string Name { get; private set; } - public GuildPermissions Permissions { get; private set; } - public int Position { get; private set; } - - public bool IsEveryone => Id == Guild.Id; - public string Mention => MentionUtils.Mention(this); - public override DiscordRestClient Discord => Guild.Discord; - - public Role(Guild guild, Model model) - : base(model.Id) - { - Guild = guild; - - Update(model, UpdateSource.Creation); - } - public void Update(Model model, UpdateSource source) - { - if (source == UpdateSource.Rest && IsAttached) return; - - Name = model.Name; - IsHoisted = model.Hoist; - IsManaged = model.Managed; - Position = model.Position; - Color = new Color(model.Color); - Permissions = new GuildPermissions(model.Permissions); - } - - public async Task ModifyAsync(Action func) - { - if (func == null) throw new NullReferenceException(nameof(func)); - - var args = new ModifyGuildRoleParams(); - func(args); - var response = await Discord.ApiClient.ModifyGuildRoleAsync(Guild.Id, Id, args).ConfigureAwait(false); - - Update(response, UpdateSource.Rest); - } - public async Task DeleteAsync() - { - await Discord.ApiClient.DeleteGuildRoleAsync(Guild.Id, Id).ConfigureAwait(false); - } - - public Role Clone() => MemberwiseClone() as Role; - - public override string ToString() => Name; - private string DebuggerDisplay => $"{Name} ({Id})"; - - ulong IRole.GuildId => Guild.Id; - } -} diff --git a/src/Discord.Net/Rest/Entities/SnowflakeEntity.cs b/src/Discord.Net/Rest/Entities/SnowflakeEntity.cs deleted file mode 100644 index f126c8ff5..000000000 --- a/src/Discord.Net/Rest/Entities/SnowflakeEntity.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Discord.Rest -{ - internal abstract class SnowflakeEntity : Entity, ISnowflakeEntity - { - //TODO: C#7 Candidate for Extension Property. Lets us remove this class. - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); - - public SnowflakeEntity(ulong id) - : base(id) - { - } - } -} diff --git a/src/Discord.Net/Rest/Entities/Users/Connection.cs b/src/Discord.Net/Rest/Entities/Users/Connection.cs deleted file mode 100644 index 622bc8730..000000000 --- a/src/Discord.Net/Rest/Entities/Users/Connection.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics; -using Model = Discord.API.Connection; - -namespace Discord.Rest -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - internal class Connection : IConnection - { - public string Id { get; } - public string Type { get; } - public string Name { get; } - public bool IsRevoked { get; } - - public IReadOnlyCollection IntegrationIds { get; } - - public Connection(Model model) - { - Id = model.Id; - Type = model.Type; - Name = model.Name; - IsRevoked = model.Revoked; - - IntegrationIds = model.Integrations; - } - - public override string ToString() => Name; - private string DebuggerDisplay => $"{Name} ({Id}, Type = {Type}{(IsRevoked ? ", Revoked" : "")})"; - } -} diff --git a/src/Discord.Net/Rest/Entities/Users/GroupUser.cs b/src/Discord.Net/Rest/Entities/Users/GroupUser.cs deleted file mode 100644 index 5a0df5cc2..000000000 --- a/src/Discord.Net/Rest/Entities/Users/GroupUser.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Discord.API.Rest; -using Discord.Rest; -using System; -using System.Threading.Tasks; - -namespace Discord.Rest -{ - internal class GroupUser : IGroupUser - { - internal virtual bool IsAttached => false; - bool IEntity.IsAttached => IsAttached; - - public GroupChannel Channel { get; private set; } - public User User { 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 Username => User.Username; - public string Mention => MentionUtils.Mention(this, false); - - public virtual UserStatus Status => UserStatus.Unknown; - public virtual Game Game => null; - - public DiscordRestClient Discord => Channel.Discord; - - public GroupUser(GroupChannel channel, User user) - { - Channel = channel; - User = user; - } - - public async Task KickAsync() - { - await Discord.ApiClient.RemoveGroupRecipientAsync(Channel.Id, Id).ConfigureAwait(false); - } - - public async Task CreateDMChannelAsync() - { - var args = new CreateDMChannelParams { Recipient = this }; - var model = await Discord.ApiClient.CreateDMChannelAsync(args).ConfigureAwait(false); - - return new DMChannel(Discord, new User(model.Recipients.Value[0]), model); - } - } -} diff --git a/src/Discord.Net/Rest/Entities/Users/GuildUser.cs b/src/Discord.Net/Rest/Entities/Users/GuildUser.cs deleted file mode 100644 index 1c8b2cfc1..000000000 --- a/src/Discord.Net/Rest/Entities/Users/GuildUser.cs +++ /dev/null @@ -1,164 +0,0 @@ -using Discord.API.Rest; -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.GuildMember; -using PresenceModel = Discord.API.Presence; - -namespace Discord.Rest -{ - [DebuggerDisplay("{DebuggerDisplay,nq}")] - internal class GuildUser : IGuildUser, ISnowflakeEntity - { - internal virtual bool IsAttached => false; - bool IEntity.IsAttached => IsAttached; - - private long? _joinedAtTicks; - - public string Nickname { get; private set; } - public GuildPermissions GuildPermissions { get; private set; } - - public Guild Guild { get; private set; } - public User User { get; private set; } - public ImmutableArray Roles { 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, Nickname != null); - public string Username => User.Username; - - public virtual UserStatus Status => UserStatus.Unknown; - public virtual Game Game => null; - - public DiscordRestClient Discord => Guild.Discord; - public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); - - public GuildUser(Guild guild, User user) - { - Guild = guild; - User = user; - Roles = ImmutableArray.Create(); - } - public GuildUser(Guild guild, User user, Model model) - : this(guild, user) - { - Update(model, UpdateSource.Creation); - } - public GuildUser(Guild guild, User user, PresenceModel model) - : this(guild, user) - { - Update(model, UpdateSource.Creation); - } - public void Update(Model model, UpdateSource source) - { - if (source == UpdateSource.Rest && IsAttached) return; - - //if (model.JoinedAt.IsSpecified) - _joinedAtTicks = model.JoinedAt.UtcTicks; - if (model.Nick.IsSpecified) - Nickname = model.Nick.Value; - - //if (model.Roles.IsSpecified) - UpdateRoles(model.Roles); - } - public virtual void Update(PresenceModel model, UpdateSource source) - { - if (source == UpdateSource.Rest && IsAttached) return; - - if (model.Roles.IsSpecified) - UpdateRoles(model.Roles.Value); - if (model.Nick.IsSpecified) - Nickname = model.Nick.Value; - } - private void Update(ModifyGuildMemberParams args, UpdateSource source) - { - if (source == UpdateSource.Rest && IsAttached) return; - - if (args._roleIds.IsSpecified) - Roles = args._roleIds.Value.Select(x => Guild.GetRole(x)).Where(x => x != null).ToImmutableArray(); - if (args._nickname.IsSpecified) - Nickname = args._nickname.Value ?? ""; - } - private void UpdateRoles(ulong[] roleIds) - { - var roles = ImmutableArray.CreateBuilder(roleIds.Length + 1); - roles.Add(Guild.EveryoneRole); - for (int i = 0; i < roleIds.Length; i++) - { - var role = Guild.GetRole(roleIds[i]); - if (role != null) - roles.Add(role); - } - Roles = roles.ToImmutable(); - GuildPermissions = new GuildPermissions(Permissions.ResolveGuild(this)); - } - - public async Task UpdateAsync() - { - if (IsAttached) throw new NotSupportedException(); - - var model = await Discord.ApiClient.GetGuildMemberAsync(Guild.Id, Id).ConfigureAwait(false); - Update(model, UpdateSource.Rest); - } - public async Task ModifyAsync(Action func) - { - if (func == null) throw new NullReferenceException(nameof(func)); - - var args = new ModifyGuildMemberParams(); - func(args); - - bool isCurrentUser = (await Discord.GetCurrentUserAsync().ConfigureAwait(false)).Id == Id; - if (isCurrentUser && args._nickname.IsSpecified) - { - var nickArgs = new ModifyCurrentUserNickParams { Nickname = args._nickname.Value ?? "" }; - await Discord.ApiClient.ModifyMyNickAsync(Guild.Id, nickArgs).ConfigureAwait(false); - args._nickname = Optional.Create(); //Remove - } - - if (!isCurrentUser || args._deaf.IsSpecified || args._mute.IsSpecified || args._roleIds.IsSpecified) - { - await Discord.ApiClient.ModifyGuildMemberAsync(Guild.Id, Id, args).ConfigureAwait(false); - Update(args, UpdateSource.Rest); - } - } - public async Task KickAsync() - { - await Discord.ApiClient.RemoveGuildMemberAsync(Guild.Id, Id).ConfigureAwait(false); - } - - public override string ToString() => $"{Username}#{Discriminator}"; - private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id})"; - - public ChannelPermissions GetPermissions(IGuildChannel channel) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - return new ChannelPermissions(Permissions.ResolveChannel(this, channel, GuildPermissions.RawValue)); - } - - public async Task CreateDMChannelAsync() - { - var args = new CreateDMChannelParams { Recipient = this }; - var model = await Discord.ApiClient.CreateDMChannelAsync(args).ConfigureAwait(false); - - return new DMChannel(Discord, new User(model.Recipients.Value[0]), model); - } - - IGuild IGuildUser.Guild => Guild; - IReadOnlyCollection IGuildUser.Roles => Roles; - 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; - } -} diff --git a/src/Discord.Net/Rest/Entities/Users/SelfUser.cs b/src/Discord.Net/Rest/Entities/Users/SelfUser.cs deleted file mode 100644 index ee32fa243..000000000 --- a/src/Discord.Net/Rest/Entities/Users/SelfUser.cs +++ /dev/null @@ -1,68 +0,0 @@ -using Discord.API.Rest; -using Discord.Rest; -using System; -using System.Threading.Tasks; -using Model = Discord.API.User; - -namespace Discord.Rest -{ - internal class SelfUser : User, ISelfUser - { - protected long _idleSince; - protected UserStatus _status; - protected Game _game; - - public string Email { get; private set; } - public bool IsVerified { get; private set; } - public bool IsMfaEnabled { get; private set; } - - public override UserStatus Status => _status; - public override Game Game => _game; - - public override DiscordRestClient Discord { get; } - - public SelfUser(DiscordRestClient discord, Model model) - : base(model) - { - Discord = discord; - } - public override void Update(Model model, UpdateSource source) - { - if (source == UpdateSource.Rest && IsAttached) return; - - base.Update(model, source); - - if (model.Email.IsSpecified) - Email = model.Email.Value; - if (model.Verified.IsSpecified) - IsVerified = model.Verified.Value; - if (model.MfaEnabled.IsSpecified) - IsMfaEnabled = model.MfaEnabled.Value; - } - - public async Task UpdateAsync() - { - if (IsAttached) throw new NotSupportedException(); - - var model = await Discord.ApiClient.GetMyUserAsync().ConfigureAwait(false); - Update(model, UpdateSource.Rest); - } - public async Task ModifyAsync(Action func) - { - if (func == null) throw new NullReferenceException(nameof(func)); - - var args = new ModifyCurrentUserParams(); - func(args); - - if (!args._username.IsSpecified) - args._username = Username; - if (!args._avatar.IsSpecified && _avatarId != null) - args._avatar = new API.Image(_avatarId); - - var model = await Discord.ApiClient.ModifySelfAsync(args).ConfigureAwait(false); - Update(model, UpdateSource.Rest); - } - - Task ISelfUser.ModifyStatusAsync(Action func) { throw new NotSupportedException(); } - } -} diff --git a/src/Discord.Net/Rest/Entities/Users/User.cs b/src/Discord.Net/Rest/Entities/Users/User.cs deleted file mode 100644 index 8f0877b59..000000000 --- a/src/Discord.Net/Rest/Entities/Users/User.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Discord.Rest; -using System; -using System.Diagnostics; -using Model = Discord.API.User; - -namespace Discord.Rest -{ - [DebuggerDisplay("{DebuggerDisplay,nq}")] - internal class User : SnowflakeEntity, IUser - { - protected string _avatarId; - - public bool IsBot { get; private set; } - public string Username { get; private set; } - public ushort DiscriminatorValue { get; private set; } - - public override DiscordRestClient Discord { get { throw new NotSupportedException(); } } - - public string AvatarUrl => API.CDN.GetUserAvatarUrl(Id, _avatarId); - public string Discriminator => DiscriminatorValue.ToString("D4"); - public string Mention => MentionUtils.Mention(this); - public virtual Game Game => null; - public virtual UserStatus Status => UserStatus.Unknown; - - public User(Model model) - : base(model.Id) - { - Update(model, UpdateSource.Creation); - } - public virtual void Update(Model model, UpdateSource source) - { - if (source == UpdateSource.Rest && IsAttached) return; - - if (model.Avatar.IsSpecified) - _avatarId = model.Avatar.Value; - if (model.Discriminator.IsSpecified) - DiscriminatorValue = ushort.Parse(model.Discriminator.Value); - if (model.Bot.IsSpecified) - IsBot = model.Bot.Value; - if (model.Username.IsSpecified) - Username = model.Username.Value; - } - - public override string ToString() => $"{Username}#{Discriminator}"; - private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id})"; - } -} diff --git a/src/Discord.Net/Rpc/DiscordRpcClient.Events.cs b/src/Discord.Net/Rpc/DiscordRpcClient.Events.cs deleted file mode 100644 index 25989d01a..000000000 --- a/src/Discord.Net/Rpc/DiscordRpcClient.Events.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Discord.Rpc -{ - public partial class DiscordRpcClient - { - //General - public event Func Connected - { - add { _connectedEvent.Add(value); } - remove { _connectedEvent.Remove(value); } - } - private readonly AsyncEvent> _connectedEvent = new AsyncEvent>(); - public event Func Disconnected - { - add { _disconnectedEvent.Add(value); } - remove { _disconnectedEvent.Remove(value); } - } - private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); - public event Func Ready - { - add { _readyEvent.Add(value); } - remove { _readyEvent.Remove(value); } - } - private readonly AsyncEvent> _readyEvent = new AsyncEvent>(); - - //Guild - public event Func GuildUpdated - { - add { _guildUpdatedEvent.Add(value); } - remove { _guildUpdatedEvent.Remove(value); } - } - private readonly AsyncEvent> _guildUpdatedEvent = new AsyncEvent>(); - - //Voice - public event Func VoiceStateUpdated - { - add { _voiceStateUpdatedEvent.Add(value); } - remove { _voiceStateUpdatedEvent.Remove(value); } - } - private readonly AsyncEvent> _voiceStateUpdatedEvent = new AsyncEvent>(); - - //Messages - public event Func MessageReceived - { - add { _messageReceivedEvent.Add(value); } - remove { _messageReceivedEvent.Remove(value); } - } - private readonly AsyncEvent> _messageReceivedEvent = new AsyncEvent>(); - public event Func MessageUpdated - { - add { _messageUpdatedEvent.Add(value); } - remove { _messageUpdatedEvent.Remove(value); } - } - private readonly AsyncEvent> _messageUpdatedEvent = new AsyncEvent>(); - public event Func MessageDeleted - { - add { _messageDeletedEvent.Add(value); } - remove { _messageDeletedEvent.Remove(value); } - } - private readonly AsyncEvent> _messageDeletedEvent = new AsyncEvent>(); - } -} diff --git a/src/Discord.Net/Rpc/Entities/IRemoteUserGuild.cs b/src/Discord.Net/Rpc/Entities/IRemoteUserGuild.cs deleted file mode 100644 index 5f92edb9b..000000000 --- a/src/Discord.Net/Rpc/Entities/IRemoteUserGuild.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Discord.Rpc -{ - /*public interface IRemoteUserGuild : ISnowflakeEntity - { - /// Gets the name of this guild. - string Name { get; } - }*/ -} diff --git a/src/Discord.Net/Rpc/Entities/RemoteUserGuild.cs b/src/Discord.Net/Rpc/Entities/RemoteUserGuild.cs deleted file mode 100644 index e0337237a..000000000 --- a/src/Discord.Net/Rpc/Entities/RemoteUserGuild.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Discord.Rest; -using System; -using Model = Discord.API.Rpc.RpcUserGuild; - -namespace Discord.Rpc -{ - /*internal class RemoteUserGuild : IRemoteUserGuild, ISnowflakeEntity - { - public ulong Id { get; } - public DiscordRestClient Discord { get; } - public string Name { get; private set; } - - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); - - public RemoteUserGuild(DiscordRestClient discord, Model model) - { - Id = model.Id; - Discord = discord; - Update(model, UpdateSource.Creation); - } - public void Update(Model model, UpdateSource source) - { - if (source == UpdateSource.Rest) return; - - Name = model.Name; - } - - bool IEntity.IsAttached => false; - }*/ -} diff --git a/src/Discord.Net/Rpc/Entities/RpcMessage.cs b/src/Discord.Net/Rpc/Entities/RpcMessage.cs deleted file mode 100644 index 86d67bfa4..000000000 --- a/src/Discord.Net/Rpc/Entities/RpcMessage.cs +++ /dev/null @@ -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; - } - } -} diff --git a/src/Discord.Net/Utilities/DateTimeUtils.cs b/src/Discord.Net/Utilities/DateTimeUtils.cs deleted file mode 100644 index b3496520c..000000000 --- a/src/Discord.Net/Utilities/DateTimeUtils.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Discord -{ - internal static class DateTimeUtils - { - public static DateTimeOffset FromSnowflake(ulong value) - => DateTimeOffset.FromUnixTimeMilliseconds((long)((value >> 22) + 1420070400000UL)); - - public static DateTimeOffset FromTicks(long ticks) - => new DateTimeOffset(ticks, TimeSpan.Zero); - public static DateTimeOffset? FromTicks(long? ticks) - => ticks != null ? new DateTimeOffset(ticks.Value, TimeSpan.Zero) : (DateTimeOffset?)null; - } -} diff --git a/src/Discord.Net/Utilities/MentionUtils.cs b/src/Discord.Net/Utilities/MentionUtils.cs deleted file mode 100644 index dc2aeb6c2..000000000 --- a/src/Discord.Net/Utilities/MentionUtils.cs +++ /dev/null @@ -1,282 +0,0 @@ -using Discord.WebSocket; -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Globalization; -using System.Linq; -using System.Text.RegularExpressions; - -namespace Discord -{ - public static class MentionUtils - { - private static readonly Regex _userRegex = new Regex(@"<@!?([0-9]+)>", RegexOptions.Compiled); - private static readonly Regex _channelRegex = new Regex(@"<#([0-9]+)>", RegexOptions.Compiled); - private static readonly Regex _roleRegex = new Regex(@"<@&([0-9]+)>", RegexOptions.Compiled); - - //If the system can't be positive a user doesn't have a nickname, assume useNickname = true (source: Jake) - internal static string Mention(IUser user, bool useNickname = true) => useNickname ? $"<@!{user.Id}>" : $"<@{user.Id}>"; - public static string MentionUser(ulong id) => $"<@!{id}>"; - - internal static string Mention(IChannel channel) => $"<#{channel.Id}>"; - public static string MentionChannel(ulong id) => $"<#{id}>"; - - internal static string Mention(IRole role) => $"<@&{role.Id}>"; - public static string MentionRole(ulong id) => $"<@&{id}>"; - - /// Parses a provided user mention string. - public static ulong ParseUser(string mentionText) - { - ulong id; - if (TryParseUser(mentionText, out id)) - return id; - throw new ArgumentException("Invalid mention format", nameof(mentionText)); - } - /// Tries to parse a provided user mention string. - public static bool TryParseUser(string mentionText, out ulong userId) - { - mentionText = mentionText.Trim(); - if (mentionText.Length >= 3 && mentionText[0] == '<' && mentionText[1] == '@' && mentionText[mentionText.Length - 1] == '>') - { - if (mentionText.Length >= 4 && mentionText[2] == '!') - mentionText = mentionText.Substring(3, mentionText.Length - 4); //<@!123> - else - mentionText = mentionText.Substring(2, mentionText.Length - 3); //<@123> - - if (ulong.TryParse(mentionText, NumberStyles.None, CultureInfo.InvariantCulture, out userId)) - return true; - } - userId = 0; - return false; - } - - /// Parses a provided channel mention string. - public static ulong ParseChannel(string mentionText) - { - ulong id; - if (TryParseChannel(mentionText, out id)) - return id; - throw new ArgumentException("Invalid mention format", nameof(mentionText)); - } - /// Tries to parse a provided channel mention string. - public static bool TryParseChannel(string mentionText, out ulong channelId) - { - mentionText = mentionText.Trim(); - if (mentionText.Length >= 3 && mentionText[0] == '<' && mentionText[1] == '#' && mentionText[mentionText.Length - 1] == '>') - { - mentionText = mentionText.Substring(2, mentionText.Length - 3); //<#123> - - if (ulong.TryParse(mentionText, NumberStyles.None, CultureInfo.InvariantCulture, out channelId)) - return true; - } - channelId = 0; - return false; - } - - /// Parses a provided role mention string. - public static ulong ParseRole(string mentionText) - { - ulong id; - if (TryParseRole(mentionText, out id)) - return id; - throw new ArgumentException("Invalid mention format", nameof(mentionText)); - } - /// Tries to parse a provided role mention string. - public static bool TryParseRole(string mentionText, out ulong roleId) - { - mentionText = mentionText.Trim(); - if (mentionText.Length >= 4 && mentionText[0] == '<' && mentionText[1] == '@' && mentionText[2] == '&' && mentionText[mentionText.Length - 1] == '>') - { - mentionText = mentionText.Substring(3, mentionText.Length - 4); //<@&123> - - if (ulong.TryParse(mentionText, NumberStyles.None, CultureInfo.InvariantCulture, out roleId)) - return true; - } - roleId = 0; - return false; - } - - internal static ImmutableArray GetUserMentions(string text, IMessageChannel channel, IReadOnlyCollection mentionedUsers) - { - var matches = _userRegex.Matches(text); - var builder = ImmutableArray.CreateBuilder(matches.Count); - foreach (var match in matches.OfType()) - { - ulong id; - if (ulong.TryParse(match.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) - { - IUser user = null; - - //Verify this user was actually mentioned - foreach (var userMention in mentionedUsers) - { - if (userMention.Id == id) - { - if (channel.IsAttached) //Waiting this sync is safe because it's using a cache - user = channel.GetUserAsync(id).GetAwaiter().GetResult() as IUser; - if (user == null) //User not found, fallback to basic mention info - user = userMention; - break; - } - } - - if (user != null) - builder.Add(user); - } - } - return builder.ToImmutable(); - } - internal static ImmutableArray GetChannelMentions(string text, IGuild guild) - { - var matches = _channelRegex.Matches(text); - var builder = ImmutableArray.CreateBuilder(matches.Count); - foreach (var match in matches.OfType()) - { - ulong id; - if (ulong.TryParse(match.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) - { - /*var channel = guild.GetChannelAsync(id).GetAwaiter().GetResult(); - if (channel != null) - builder.Add(channel);*/ - builder.Add(id); - } - } - return builder.ToImmutable(); - } - internal static ImmutableArray GetRoleMentions(string text, IGuild guild) - { - var matches = _roleRegex.Matches(text); - var builder = ImmutableArray.CreateBuilder(matches.Count); - foreach (var match in matches.OfType()) - { - ulong id; - if (ulong.TryParse(match.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) - { - var role = guild.GetRole(id); - if (role != null) - builder.Add(role); - } - } - return builder.ToImmutable(); - } - - internal static string ResolveUserMentions(string text, IMessageChannel channel, IReadOnlyCollection mentions, UserMentionHandling mode) - { - if (mode == UserMentionHandling.Ignore) return text; - - return _userRegex.Replace(text, new MatchEvaluator(e => - { - ulong id; - if (ulong.TryParse(e.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) - { - IUser user = null; - foreach (var mention in mentions) - { - if (mention.Id == id) - { - user = mention; - break; - } - } - if (user != null) - { - string name = user.Username; - - var guildUser = user as IGuildUser; - if (e.Value[2] == '!') - { - if (guildUser != null && guildUser.Nickname != null) - name = guildUser.Nickname; - } - - switch (mode) - { - case UserMentionHandling.Remove: - default: - return ""; - case UserMentionHandling.Name: - return $"@{name}"; - case UserMentionHandling.NameAndDiscriminator: - return $"@{name}#{user.Discriminator}"; - } - } - } - return e.Value; - })); - } - internal static string ResolveChannelMentions(string text, IGuild guild, ChannelMentionHandling mode) - { - if (mode == ChannelMentionHandling.Ignore) return text; - - return _channelRegex.Replace(text, new MatchEvaluator(e => - { - ulong id; - if (ulong.TryParse(e.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) - { - switch (mode) - { - case ChannelMentionHandling.Remove: - return ""; - case ChannelMentionHandling.Name: - if (guild != null && guild.IsAttached) //It's too expensive to do a channel lookup in REST mode - { - IGuildChannel channel = null; - channel = guild.GetChannel(id); - if (channel != null) - return $"#{channel.Name}"; - else - return $"#deleted-channel"; - } - break; - } - } - return e.Value; - })); - } - internal static string ResolveRoleMentions(string text, IReadOnlyCollection mentions, RoleMentionHandling mode) - { - if (mode == RoleMentionHandling.Ignore) return text; - - return _roleRegex.Replace(text, new MatchEvaluator(e => - { - ulong id; - if (ulong.TryParse(e.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) - { - switch (mode) - { - case RoleMentionHandling.Remove: - return ""; - case RoleMentionHandling.Name: - IRole role = null; - foreach (var mention in mentions) - { - if (mention.Id == id) - { - role = mention; - break; - } - } - if (role != null) - return $"@{role.Name}"; - else - return $"@deleted-role"; - } - } - return e.Value; - })); - } - internal static string ResolveEveryoneMentions(string text, EveryoneMentionHandling mode) - { - if (mode == EveryoneMentionHandling.Ignore) return text; - - switch (mode) - { - case EveryoneMentionHandling.Sanitize: - return text.Replace("@everyone", "@\x200beveryone").Replace("@here", "@\x200bhere"); - case EveryoneMentionHandling.Remove: - default: - return text.Replace("@everyone", "").Replace("@here", ""); - } - } - } -} diff --git a/src/Discord.Net/WebSocket/DiscordSocketClient.Events.cs b/src/Discord.Net/WebSocket/DiscordSocketClient.Events.cs deleted file mode 100644 index 04aadaa3f..000000000 --- a/src/Discord.Net/WebSocket/DiscordSocketClient.Events.cs +++ /dev/null @@ -1,203 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Discord.WebSocket -{ - //TODO: Add event docstrings - public partial class DiscordSocketClient - { - //General - public event Func Connected - { - add { _connectedEvent.Add(value); } - remove { _connectedEvent.Remove(value); } - } - private readonly AsyncEvent> _connectedEvent = new AsyncEvent>(); - public event Func Disconnected - { - add { _disconnectedEvent.Add(value); } - remove { _disconnectedEvent.Remove(value); } - } - private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); - public event Func Ready - { - add { _readyEvent.Add(value); } - remove { _readyEvent.Remove(value); } - } - private readonly AsyncEvent> _readyEvent = new AsyncEvent>(); - public event Func LatencyUpdated - { - add { _latencyUpdatedEvent.Add(value); } - remove { _latencyUpdatedEvent.Remove(value); } - } - private readonly AsyncEvent> _latencyUpdatedEvent = new AsyncEvent>(); - - //Channels - public event Func ChannelCreated - { - add { _channelCreatedEvent.Add(value); } - remove { _channelCreatedEvent.Remove(value); } - } - private readonly AsyncEvent> _channelCreatedEvent = new AsyncEvent>(); - public event Func ChannelDestroyed - { - add { _channelDestroyedEvent.Add(value); } - remove { _channelDestroyedEvent.Remove(value); } - } - private readonly AsyncEvent> _channelDestroyedEvent = new AsyncEvent>(); - public event Func ChannelUpdated - { - add { _channelUpdatedEvent.Add(value); } - remove { _channelUpdatedEvent.Remove(value); } - } - private readonly AsyncEvent> _channelUpdatedEvent = new AsyncEvent>(); - - //Messages - public event Func MessageReceived - { - add { _messageReceivedEvent.Add(value); } - remove { _messageReceivedEvent.Remove(value); } - } - private readonly AsyncEvent> _messageReceivedEvent = new AsyncEvent>(); - public event Func, Task> MessageDeleted - { - add { _messageDeletedEvent.Add(value); } - remove { _messageDeletedEvent.Remove(value); } - } - private readonly AsyncEvent, Task>> _messageDeletedEvent = new AsyncEvent, Task>>(); - public event Func, IMessage, Task> MessageUpdated - { - add { _messageUpdatedEvent.Add(value); } - remove { _messageUpdatedEvent.Remove(value); } - } - private readonly AsyncEvent, IMessage, Task>> _messageUpdatedEvent = new AsyncEvent, IMessage, Task>>(); - - //Roles - public event Func RoleCreated - { - add { _roleCreatedEvent.Add(value); } - remove { _roleCreatedEvent.Remove(value); } - } - private readonly AsyncEvent> _roleCreatedEvent = new AsyncEvent>(); - public event Func RoleDeleted - { - add { _roleDeletedEvent.Add(value); } - remove { _roleDeletedEvent.Remove(value); } - } - private readonly AsyncEvent> _roleDeletedEvent = new AsyncEvent>(); - public event Func RoleUpdated - { - add { _roleUpdatedEvent.Add(value); } - remove { _roleUpdatedEvent.Remove(value); } - } - private readonly AsyncEvent> _roleUpdatedEvent = new AsyncEvent>(); - - //Guilds - public event Func JoinedGuild - { - add { _joinedGuildEvent.Add(value); } - remove { _joinedGuildEvent.Remove(value); } - } - private AsyncEvent> _joinedGuildEvent = new AsyncEvent>(); - public event Func LeftGuild - { - add { _leftGuildEvent.Add(value); } - remove { _leftGuildEvent.Remove(value); } - } - private AsyncEvent> _leftGuildEvent = new AsyncEvent>(); - public event Func GuildAvailable - { - add { _guildAvailableEvent.Add(value); } - remove { _guildAvailableEvent.Remove(value); } - } - private AsyncEvent> _guildAvailableEvent = new AsyncEvent>(); - public event Func GuildUnavailable - { - add { _guildUnavailableEvent.Add(value); } - remove { _guildUnavailableEvent.Remove(value); } - } - private AsyncEvent> _guildUnavailableEvent = new AsyncEvent>(); - public event Func GuildMembersDownloaded - { - add { _guildMembersDownloadedEvent.Add(value); } - remove { _guildMembersDownloadedEvent.Remove(value); } - } - private AsyncEvent> _guildMembersDownloadedEvent = new AsyncEvent>(); - public event Func GuildUpdated - { - add { _guildUpdatedEvent.Add(value); } - remove { _guildUpdatedEvent.Remove(value); } - } - private AsyncEvent> _guildUpdatedEvent = new AsyncEvent>(); - - //Users - public event Func UserJoined - { - add { _userJoinedEvent.Add(value); } - remove { _userJoinedEvent.Remove(value); } - } - private readonly AsyncEvent> _userJoinedEvent = new AsyncEvent>(); - public event Func UserLeft - { - add { _userLeftEvent.Add(value); } - remove { _userLeftEvent.Remove(value); } - } - private readonly AsyncEvent> _userLeftEvent = new AsyncEvent>(); - public event Func UserBanned - { - add { _userBannedEvent.Add(value); } - remove { _userBannedEvent.Remove(value); } - } - private readonly AsyncEvent> _userBannedEvent = new AsyncEvent>(); - public event Func UserUnbanned - { - add { _userUnbannedEvent.Add(value); } - remove { _userUnbannedEvent.Remove(value); } - } - private readonly AsyncEvent> _userUnbannedEvent = new AsyncEvent>(); - public event Func UserUpdated - { - add { _userUpdatedEvent.Add(value); } - remove { _userUpdatedEvent.Remove(value); } - } - private readonly AsyncEvent> _userUpdatedEvent = new AsyncEvent>(); - public event Func UserPresenceUpdated - { - add { _userPresenceUpdatedEvent.Add(value); } - remove { _userPresenceUpdatedEvent.Remove(value); } - } - private readonly AsyncEvent> _userPresenceUpdatedEvent = new AsyncEvent>(); - public event Func UserVoiceStateUpdated - { - add { _userVoiceStateUpdatedEvent.Add(value); } - remove { _userVoiceStateUpdatedEvent.Remove(value); } - } - private readonly AsyncEvent> _userVoiceStateUpdatedEvent = new AsyncEvent>(); - public event Func CurrentUserUpdated - { - add { _selfUpdatedEvent.Add(value); } - remove { _selfUpdatedEvent.Remove(value); } - } - private readonly AsyncEvent> _selfUpdatedEvent = new AsyncEvent>(); - public event Func UserIsTyping - { - add { _userIsTypingEvent.Add(value); } - remove { _userIsTypingEvent.Remove(value); } - } - private readonly AsyncEvent> _userIsTypingEvent = new AsyncEvent>(); - public event Func RecipientAdded - { - add { _recipientAddedEvent.Add(value); } - remove { _recipientAddedEvent.Remove(value); } - } - private readonly AsyncEvent> _recipientAddedEvent = new AsyncEvent>(); - public event Func RecipientRemoved - { - add { _recipientRemovedEvent.Add(value); } - remove { _recipientRemovedEvent.Remove(value); } - } - private readonly AsyncEvent> _recipientRemovedEvent = new AsyncEvent>(); - - //TODO: Add PresenceUpdated? VoiceStateUpdated?, VoiceConnected, VoiceDisconnected; - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Channels/ISocketChannel.cs b/src/Discord.Net/WebSocket/Entities/Channels/ISocketChannel.cs deleted file mode 100644 index d74f35faa..000000000 --- a/src/Discord.Net/WebSocket/Entities/Channels/ISocketChannel.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Model = Discord.API.Channel; - -namespace Discord.WebSocket -{ - internal interface ISocketChannel : IChannel - { - void Update(Model model, UpdateSource source); - - ISocketChannel Clone(); - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Channels/ISocketGuildChannel.cs b/src/Discord.Net/WebSocket/Entities/Channels/ISocketGuildChannel.cs deleted file mode 100644 index f70a56a53..000000000 --- a/src/Discord.Net/WebSocket/Entities/Channels/ISocketGuildChannel.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Discord.WebSocket -{ - internal interface ISocketGuildChannel : ISocketChannel, IGuildChannel - { - new SocketGuild Guild { get; } - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Channels/ISocketMessageChannel.cs b/src/Discord.Net/WebSocket/Entities/Channels/ISocketMessageChannel.cs deleted file mode 100644 index 80706970f..000000000 --- a/src/Discord.Net/WebSocket/Entities/Channels/ISocketMessageChannel.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; -using MessageModel = Discord.API.Message; - -namespace Discord.WebSocket -{ - internal interface ISocketMessageChannel : ISocketChannel, IMessageChannel - { - IReadOnlyCollection Users { get; } - - ISocketMessage CreateMessage(ISocketUser author, MessageModel model); - ISocketMessage AddMessage(ISocketUser author, MessageModel model); - ISocketMessage GetMessage(ulong id); - ISocketMessage RemoveMessage(ulong id); - - ISocketUser GetUser(ulong id, bool skipCheck = false); - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Channels/ISocketPrivateChannel.cs b/src/Discord.Net/WebSocket/Entities/Channels/ISocketPrivateChannel.cs deleted file mode 100644 index e38107da6..000000000 --- a/src/Discord.Net/WebSocket/Entities/Channels/ISocketPrivateChannel.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; - -namespace Discord.WebSocket -{ - internal interface ISocketPrivateChannel : ISocketChannel, IPrivateChannel - { - new IReadOnlyCollection Recipients { get; } - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Channels/MessageManager.cs b/src/Discord.Net/WebSocket/Entities/Channels/MessageManager.cs deleted file mode 100644 index 713c5b635..000000000 --- a/src/Discord.Net/WebSocket/Entities/Channels/MessageManager.cs +++ /dev/null @@ -1,92 +0,0 @@ -using Discord.API.Rest; -using Discord.Rest; -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading.Tasks; -using Model = Discord.API.Message; - -namespace Discord.WebSocket -{ - internal class MessageManager - { - private readonly DiscordSocketClient _discord; - private readonly ISocketMessageChannel _channel; - - public virtual IReadOnlyCollection Messages - => ImmutableArray.Create(); - - public MessageManager(DiscordSocketClient discord, ISocketMessageChannel channel) - { - _discord = discord; - _channel = channel; - } - - public virtual void Add(ISocketMessage message) { } - public virtual ISocketMessage Remove(ulong id) => null; - public virtual ISocketMessage Get(ulong id) => null; - - public virtual IImmutableList GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) - => ImmutableArray.Create(); - - public virtual async Task DownloadAsync(ulong id) - { - var model = await _discord.ApiClient.GetChannelMessageAsync(_channel.Id, id).ConfigureAwait(false); - if (model != null) - return Create(new User(model.Author.Value), model); - return null; - } - public async Task> DownloadAsync(ulong? fromId, Direction dir, int limit) - { - //TODO: Test heavily, especially the ordering of messages - if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); - if (limit == 0) return ImmutableArray.Empty; - - var cachedMessages = GetMany(fromId, dir, limit); - if (cachedMessages.Count == limit) - return cachedMessages; - else if (cachedMessages.Count > limit) - return cachedMessages.Skip(cachedMessages.Count - limit).ToImmutableArray(); - else - { - var args = new GetChannelMessagesParams - { - Limit = limit - cachedMessages.Count, - RelativeDirection = dir - }; - if (cachedMessages.Count == 0) - { - if (fromId != null) - args.RelativeMessageId = fromId.Value; - } - else - args.RelativeMessageId = dir == Direction.Before ? cachedMessages[0].Id : cachedMessages[cachedMessages.Count - 1].Id; - var downloadedMessages = await _discord.ApiClient.GetChannelMessagesAsync(_channel.Id, args).ConfigureAwait(false); - - var guild = (_channel as ISocketGuildChannel)?.Guild; - return cachedMessages.Concat(downloadedMessages.Select(x => - { - IUser user = _channel.GetUser(x.Author.Value.Id, true); - if (user == null) - { - var newUser = new User(x.Author.Value); - if (guild != null) - user = new GuildUser(guild, newUser); - else - user = newUser; - } - return Create(user, x); - })).ToImmutableArray(); - } - } - - public ISocketMessage Create(IUser author, Model model) - { - if (model.Type == MessageType.Default) - return new SocketUserMessage(_channel, author, model); - else - return new SocketSystemMessage(_channel, author, model); - } - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Channels/SocketDMChannel.cs b/src/Discord.Net/WebSocket/Entities/Channels/SocketDMChannel.cs deleted file mode 100644 index 958c71bba..000000000 --- a/src/Discord.Net/WebSocket/Entities/Channels/SocketDMChannel.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Discord.Rest; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Threading.Tasks; -using MessageModel = Discord.API.Message; -using Model = Discord.API.Channel; - -namespace Discord.WebSocket -{ - internal class SocketDMChannel : DMChannel, IDMChannel, ISocketChannel, ISocketMessageChannel, ISocketPrivateChannel - { - internal override bool IsAttached => true; - - private readonly MessageManager _messages; - - public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; - public new SocketDMUser Recipient => base.Recipient as SocketDMUser; - public IReadOnlyCollection Users => ImmutableArray.Create(Discord.CurrentUser, Recipient); - IReadOnlyCollection ISocketPrivateChannel.Recipients => ImmutableArray.Create(Recipient); - - public SocketDMChannel(DiscordSocketClient discord, SocketDMUser recipient, Model model) - : base(discord, recipient, model) - { - if (Discord.MessageCacheSize > 0) - _messages = new MessageCache(Discord, this); - else - _messages = new MessageManager(Discord, this); - } - - public override Task GetUserAsync(ulong id) => Task.FromResult(GetUser(id)); - public override Task> GetUsersAsync() => Task.FromResult>(Users); - public ISocketUser GetUser(ulong id) - { - var currentUser = Discord.CurrentUser; - if (id == Recipient.Id) - return Recipient; - else if (id == currentUser.Id) - return currentUser; - else - return null; - } - - public override async Task GetMessageAsync(ulong id) - { - return await _messages.DownloadAsync(id).ConfigureAwait(false); - } - public override async Task> GetMessagesAsync(int limit) - { - return await _messages.DownloadAsync(null, Direction.Before, limit).ConfigureAwait(false); - } - public override async Task> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit) - { - return await _messages.DownloadAsync(fromMessageId, dir, limit).ConfigureAwait(false); - } - public ISocketMessage CreateMessage(ISocketUser author, MessageModel model) - { - return _messages.Create(author, model); - } - public ISocketMessage AddMessage(ISocketUser author, MessageModel model) - { - var msg = _messages.Create(author, model); - _messages.Add(msg); - return msg; - } - public ISocketMessage GetMessage(ulong id) - { - return _messages.Get(id); - } - public ISocketMessage RemoveMessage(ulong id) - { - return _messages.Remove(id); - } - - public SocketDMChannel Clone() => MemberwiseClone() as SocketDMChannel; - - IMessage IMessageChannel.GetCachedMessage(ulong id) => GetMessage(id); - ISocketUser ISocketMessageChannel.GetUser(ulong id, bool skipCheck) => GetUser(id); - ISocketChannel ISocketChannel.Clone() => Clone(); - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net/WebSocket/Entities/Channels/SocketGroupChannel.cs deleted file mode 100644 index 8d456bb55..000000000 --- a/src/Discord.Net/WebSocket/Entities/Channels/SocketGroupChannel.cs +++ /dev/null @@ -1,144 +0,0 @@ -using Discord.Rest; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading.Tasks; -using MessageModel = Discord.API.Message; -using Model = Discord.API.Channel; -using UserModel = Discord.API.User; -using VoiceStateModel = Discord.API.VoiceState; - -namespace Discord.WebSocket -{ - internal class SocketGroupChannel : GroupChannel, IGroupChannel, ISocketChannel, ISocketMessageChannel, ISocketPrivateChannel - { - internal override bool IsAttached => true; - - private readonly MessageManager _messages; - private ConcurrentDictionary _voiceStates; - - public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; - public IReadOnlyCollection Users - => _users.Select(x => x.Value as ISocketUser).Concat(ImmutableArray.Create(Discord.CurrentUser)).ToReadOnlyCollection(() => _users.Count + 1); - public new IReadOnlyCollection Recipients => _users.Select(x => x.Value as ISocketUser).ToReadOnlyCollection(_users); - - public SocketGroupChannel(DiscordSocketClient discord, Model model) - : base(discord, model) - { - if (Discord.MessageCacheSize > 0) - _messages = new MessageCache(Discord, this); - else - _messages = new MessageManager(Discord, this); - _voiceStates = new ConcurrentDictionary(1, 5); - } - public override void Update(Model model, UpdateSource source) - { - if (source == UpdateSource.Rest && IsAttached) return; - - base.Update(model, source); - } - - internal void UpdateUsers(UserModel[] models, UpdateSource source, DataStore dataStore) - { - var users = new ConcurrentDictionary(1, models.Length); - for (int i = 0; i < models.Length; i++) - { - var globalUser = Discord.GetOrAddUser(models[i], dataStore); - users[models[i].Id] = new SocketGroupUser(this, globalUser); - } - _users = users; - } - internal override void UpdateUsers(UserModel[] models, UpdateSource source) - => UpdateUsers(models, source, Discord.DataStore); - - public SocketGroupUser AddUser(UserModel model, DataStore dataStore) - { - GroupUser user; - if (_users.TryGetValue(model.Id, out user)) - return user as SocketGroupUser; - else - { - var globalUser = Discord.GetOrAddUser(model, dataStore); - var privateUser = new SocketGroupUser(this, globalUser); - _users[privateUser.Id] = privateUser; - return privateUser; - } - } - public ISocketUser GetUser(ulong id) - { - GroupUser user; - if (_users.TryGetValue(id, out user)) - return user as SocketGroupUser; - if (id == Discord.CurrentUser.Id) - return Discord.CurrentUser; - return null; - } - public SocketGroupUser RemoveUser(ulong id) - { - GroupUser user; - if (_users.TryRemove(id, out user)) - return user as SocketGroupUser; - return null; - } - - public VoiceState AddOrUpdateVoiceState(VoiceStateModel model, DataStore dataStore, ConcurrentDictionary voiceStates = null) - { - var voiceChannel = dataStore.GetChannel(model.ChannelId.Value) as SocketVoiceChannel; - var voiceState = new VoiceState(voiceChannel, model); - (voiceStates ?? _voiceStates)[model.UserId] = voiceState; - return voiceState; - } - public VoiceState? GetVoiceState(ulong id) - { - VoiceState voiceState; - if (_voiceStates.TryGetValue(id, out voiceState)) - return voiceState; - return null; - } - public VoiceState? RemoveVoiceState(ulong id) - { - VoiceState voiceState; - if (_voiceStates.TryRemove(id, out voiceState)) - return voiceState; - return null; - } - - public override async Task GetMessageAsync(ulong id) - { - return await _messages.DownloadAsync(id).ConfigureAwait(false); - } - public override async Task> GetMessagesAsync(int limit) - { - return await _messages.DownloadAsync(null, Direction.Before, limit).ConfigureAwait(false); - } - public override async Task> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit) - { - return await _messages.DownloadAsync(fromMessageId, dir, limit).ConfigureAwait(false); - } - public ISocketMessage CreateMessage(ISocketUser author, MessageModel model) - { - return _messages.Create(author, model); - } - public ISocketMessage AddMessage(ISocketUser author, MessageModel model) - { - var msg = _messages.Create(author, model); - _messages.Add(msg); - return msg; - } - public ISocketMessage GetMessage(ulong id) - { - return _messages.Get(id); - } - public ISocketMessage RemoveMessage(ulong id) - { - return _messages.Remove(id); - } - - public SocketDMChannel Clone() => MemberwiseClone() as SocketDMChannel; - - IMessage IMessageChannel.GetCachedMessage(ulong id) => GetMessage(id); - ISocketUser ISocketMessageChannel.GetUser(ulong id, bool skipCheck) => GetUser(id); - ISocketChannel ISocketChannel.Clone() => Clone(); - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net/WebSocket/Entities/Channels/SocketTextChannel.cs deleted file mode 100644 index 72f5717c3..000000000 --- a/src/Discord.Net/WebSocket/Entities/Channels/SocketTextChannel.cs +++ /dev/null @@ -1,88 +0,0 @@ -using Discord.Rest; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading.Tasks; -using MessageModel = Discord.API.Message; -using Model = Discord.API.Channel; - -namespace Discord.WebSocket -{ - internal class SocketTextChannel : TextChannel, ISocketGuildChannel, ISocketMessageChannel - { - internal override bool IsAttached => true; - - private readonly MessageManager _messages; - - public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; - public new SocketGuild Guild => base.Guild as SocketGuild; - - public IReadOnlyCollection Members - => Guild.Members.Where(x => Permissions.GetValue(Permissions.ResolveChannel(x, this, x.GuildPermissions.RawValue), ChannelPermission.ReadMessages)).ToImmutableArray(); - - public SocketTextChannel(SocketGuild guild, Model model) - : base(guild, model) - { - if (Discord.MessageCacheSize > 0) - _messages = new MessageCache(Discord, this); - else - _messages = new MessageManager(Discord, this); - } - - public override Task GetUserAsync(ulong id) => Task.FromResult(GetUser(id)); - public override Task> GetUsersAsync() => Task.FromResult>(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 GetMessageAsync(ulong id) - { - return await _messages.DownloadAsync(id).ConfigureAwait(false); - } - public override async Task> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch) - { - return await _messages.DownloadAsync(null, Direction.Before, limit).ConfigureAwait(false); - } - public override async Task> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) - { - return await _messages.DownloadAsync(fromMessageId, dir, limit).ConfigureAwait(false); - } - - public ISocketMessage CreateMessage(ISocketUser author, MessageModel model) - { - return _messages.Create(author, model); - } - public ISocketMessage AddMessage(ISocketUser author, MessageModel model) - { - var msg = _messages.Create(author, model); - _messages.Add(msg); - return msg; - } - public ISocketMessage GetMessage(ulong id) - { - return _messages.Get(id); - } - public ISocketMessage RemoveMessage(ulong id) - { - return _messages.Remove(id); - } - - public SocketTextChannel Clone() => MemberwiseClone() as SocketTextChannel; - - IReadOnlyCollection ISocketMessageChannel.Users => Members; - - IMessage IMessageChannel.GetCachedMessage(ulong id) => GetMessage(id); - ISocketUser ISocketMessageChannel.GetUser(ulong id, bool skipCheck) => GetUser(id, skipCheck); - ISocketChannel ISocketChannel.Clone() => Clone(); - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Channels/SocketVoiceChannel.cs b/src/Discord.Net/WebSocket/Entities/Channels/SocketVoiceChannel.cs deleted file mode 100644 index 4f6438254..000000000 --- a/src/Discord.Net/WebSocket/Entities/Channels/SocketVoiceChannel.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Discord.Audio; -using Discord.Rest; -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading.Tasks; -using Model = Discord.API.Channel; - -namespace Discord.WebSocket -{ - internal class SocketVoiceChannel : VoiceChannel, ISocketGuildChannel - { - internal override bool IsAttached => true; - - public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; - public new SocketGuild Guild => base.Guild as SocketGuild; - - public IReadOnlyCollection Members - => Guild.VoiceStates.Where(x => x.Value.VoiceChannel.Id == Id).Select(x => Guild.GetUser(x.Key)).ToImmutableArray(); - - public SocketVoiceChannel(SocketGuild guild, Model model) - : base(guild, model) - { - } - - public override Task GetUserAsync(ulong id) - => Task.FromResult(GetUser(id)); - public override Task> GetUsersAsync() - => Task.FromResult(Members); - public IGuildUser GetUser(ulong id) - { - var user = Guild.GetUser(id); - if (user != null && user.VoiceChannel.Id == Id) - return user; - return null; - } - - public override async Task ConnectAsync() - { - var audioMode = Discord.AudioMode; - if (audioMode == AudioMode.Disabled) - throw new InvalidOperationException($"Audio is not enabled on this client, {nameof(DiscordSocketConfig.AudioMode)} in {nameof(DiscordSocketConfig)} must be set."); - - return await Guild.ConnectAudioAsync(Id, - (audioMode & AudioMode.Incoming) == 0, - (audioMode & AudioMode.Outgoing) == 0).ConfigureAwait(false); - } - - public SocketVoiceChannel Clone() => MemberwiseClone() as SocketVoiceChannel; - - ISocketChannel ISocketChannel.Clone() => Clone(); - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net/WebSocket/Entities/Guilds/SocketGuild.cs deleted file mode 100644 index 75f8fded2..000000000 --- a/src/Discord.Net/WebSocket/Entities/Guilds/SocketGuild.cs +++ /dev/null @@ -1,420 +0,0 @@ -using Discord.Audio; -using Discord.Rest; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ChannelModel = Discord.API.Channel; -using EmojiUpdateModel = Discord.API.Gateway.GuildEmojiUpdateEvent; -using ExtendedModel = Discord.API.Gateway.ExtendedGuild; -using GuildSyncModel = Discord.API.Gateway.GuildSyncEvent; -using MemberModel = Discord.API.GuildMember; -using Model = Discord.API.Guild; -using PresenceModel = Discord.API.Presence; -using RoleModel = Discord.API.Role; -using VoiceStateModel = Discord.API.VoiceState; - -namespace Discord.WebSocket -{ - internal class SocketGuild : Guild, IGuild, IUserGuild - { - internal override bool IsAttached => true; - - private readonly SemaphoreSlim _audioLock; - private TaskCompletionSource _syncPromise, _downloaderPromise; - private TaskCompletionSource _audioConnectPromise; - private ConcurrentHashSet _channels; - private ConcurrentDictionary _members; - private ConcurrentDictionary _voiceStates; - internal bool _available; - - public bool Available => _available && Discord.ConnectionState == ConnectionState.Connected; - public int MemberCount { get; set; } - public int DownloadedMemberCount { get; private set; } - public AudioClient AudioClient { get; private set; } - - public bool HasAllMembers => _downloaderPromise.Task.IsCompleted; - public bool IsSynced => _syncPromise.Task.IsCompleted; - public Task SyncPromise => _syncPromise.Task; - public Task DownloaderPromise => _downloaderPromise.Task; - - public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; - public SocketGuildUser CurrentUser => GetUser(Discord.CurrentUser.Id); - public IReadOnlyCollection 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 Members => _members.ToReadOnlyCollection(); - public IEnumerable> VoiceStates => _voiceStates; - - public SocketGuild(DiscordSocketClient discord, ExtendedModel model, DataStore dataStore) : base(discord, model) - { - _audioLock = new SemaphoreSlim(1, 1); - _syncPromise = new TaskCompletionSource(); - _downloaderPromise = new TaskCompletionSource(); - Update(model, UpdateSource.Creation, dataStore); - } - - public void Update(ExtendedModel model, UpdateSource source, DataStore dataStore) - { - if (source == UpdateSource.Rest && IsAttached) return; - - _available = !(model.Unavailable ?? false); - if (!_available) - { - if (_channels == null) - _channels = new ConcurrentHashSet(); - if (_members == null) - _members = new ConcurrentDictionary(); - if (_roles == null) - _roles = new ConcurrentDictionary(); - if (Emojis == null) - Emojis = ImmutableArray.Create(); - if (Features == null) - Features = ImmutableArray.Create(); - return; - } - - base.Update(model as Model, source); - - var channels = new ConcurrentHashSet(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(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(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, UpdateSource source, DataStore dataStore) - { - if (source == UpdateSource.Rest && IsAttached) return; - - var members = new ConcurrentDictionary(1, (int)(model.Presences.Length * 1.05)); - { - DownloadedMemberCount = 0; - for (int i = 0; i < model.Members.Length; i++) - AddOrUpdateUser(model.Members[i], dataStore, members); - var _ = _syncPromise.TrySetResultAsync(true); - if (!model.Large) - _ = _downloaderPromise.TrySetResultAsync(true); - - for (int i = 0; i < model.Presences.Length; i++) - AddOrUpdateUser(model.Presences[i], dataStore, members); - } - _members = members; - } - - public void Update(EmojiUpdateModel model, UpdateSource source) - { - if (source == UpdateSource.Rest && IsAttached) return; - - var emojis = ImmutableArray.CreateBuilder(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 GetChannelAsync(ulong id) => Task.FromResult(GetChannel(id)); - public override Task> GetChannelsAsync() => Task.FromResult>(Channels); - public ISocketGuildChannel AddChannel(ChannelModel model, DataStore dataStore, ConcurrentHashSet 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 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 GetUserAsync(ulong id) => Task.FromResult(GetUser(id)); - public override Task GetCurrentUserAsync() - => Task.FromResult(CurrentUser); - public override Task> GetUsersAsync() - => Task.FromResult>(Members); - public SocketGuildUser AddOrUpdateUser(MemberModel model, DataStore dataStore, ConcurrentDictionary 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 members = null) - { - members = members ?? _members; - - SocketGuildUser member; - if (members.TryGetValue(model.User.Id, out member)) - member.Update(model, UpdateSource.WebSocket); - else - { - var user = Discord.GetOrAddUser(model.User, dataStore); - member = new SocketGuildUser(this, user, model); - members[user.Id] = member; - DownloadedMemberCount++; - } - return member; - } - public SocketGuildUser GetUser(ulong id) - { - SocketGuildUser member; - if (_members.TryGetValue(id, out member)) - return member; - return null; - } - public SocketGuildUser RemoveUser(ulong id) - { - SocketGuildUser member; - if (_members.TryRemove(id, out member)) - { - DownloadedMemberCount--; - return member; - } - member.User.RemoveRef(Discord); - return null; - } - public override async Task DownloadUsersAsync() - { - await Discord.DownloadUsersAsync(new [] { this }); - } - public void CompleteDownloadMembers() - { - _downloaderPromise.TrySetResultAsync(true); - } - - public VoiceState AddOrUpdateVoiceState(VoiceStateModel model, DataStore dataStore, ConcurrentDictionary voiceStates = null) - { - var voiceChannel = dataStore.GetChannel(model.ChannelId.Value) as SocketVoiceChannel; - var voiceState = new VoiceState(voiceChannel, model); - (voiceStates ?? _voiceStates)[model.UserId] = voiceState; - return voiceState; - } - public VoiceState? GetVoiceState(ulong id) - { - VoiceState voiceState; - if (_voiceStates.TryGetValue(id, out voiceState)) - return voiceState; - return null; - } - public VoiceState? RemoveVoiceState(ulong id) - { - VoiceState voiceState; - if (_voiceStates.TryRemove(id, out voiceState)) - return voiceState; - return null; - } - - public async Task ConnectAudioAsync(ulong channelId, bool selfDeaf, bool selfMute) - { - try - { - TaskCompletionSource promise; - - await _audioLock.WaitAsync().ConfigureAwait(false); - try - { - await DisconnectAudioInternalAsync().ConfigureAwait(false); - promise = new TaskCompletionSource(); - _audioConnectPromise = promise; - await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, channelId, selfDeaf, selfMute).ConfigureAwait(false); - } - finally - { - _audioLock.Release(); - } - - var timeoutTask = Task.Delay(15000); - if (await Task.WhenAny(promise.Task, timeoutTask) == timeoutTask) - throw new TimeoutException(); - return await promise.Task.ConfigureAwait(false); - } - catch (Exception) - { - await DisconnectAudioInternalAsync().ConfigureAwait(false); - throw; - } - } - public async Task DisconnectAudioAsync(AudioClient client = null) - { - await _audioLock.WaitAsync().ConfigureAwait(false); - try - { - await DisconnectAudioInternalAsync(client).ConfigureAwait(false); - } - finally - { - _audioLock.Release(); - } - } - private async Task DisconnectAudioInternalAsync(AudioClient client = null) - { - var oldClient = AudioClient; - if (oldClient != null) - { - if (client == null || oldClient == client) - { - _audioConnectPromise?.TrySetCanceledAsync(); //Cancel any previous audio connection - _audioConnectPromise = null; - } - if (oldClient == client) - { - AudioClient = null; - await oldClient.DisconnectAsync().ConfigureAwait(false); - } - } - } - public async Task FinishConnectAudio(int id, string url, string token) - { - var voiceState = GetVoiceState(CurrentUser.Id).Value; - - await _audioLock.WaitAsync().ConfigureAwait(false); - try - { - if (AudioClient == null) - { - var audioClient = new AudioClient(this, id); - audioClient.Disconnected += async ex => - { - await _audioLock.WaitAsync().ConfigureAwait(false); - try - { - if (AudioClient == audioClient) //Only reconnect if we're still assigned as this guild's audio client - { - if (ex != null) - { - //Reconnect if we still have channel info. - //TODO: Is this threadsafe? Could channel data be deleted before we access it? - var voiceState2 = GetVoiceState(CurrentUser.Id); - if (voiceState2.HasValue) - { - var voiceChannelId = voiceState2.Value.VoiceChannel?.Id; - if (voiceChannelId != null) - await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, voiceChannelId, voiceState2.Value.IsSelfDeafened, voiceState2.Value.IsSelfMuted); - } - } - else - { - try { AudioClient.Dispose(); } catch { } - AudioClient = null; - } - } - } - finally - { - _audioLock.Release(); - } - }; - AudioClient = audioClient; - } - await AudioClient.ConnectAsync(url, CurrentUser.Id, voiceState.VoiceSessionId, token).ConfigureAwait(false); - await _audioConnectPromise.TrySetResultAsync(AudioClient).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - await DisconnectAudioAsync(); - } - catch (Exception e) - { - await _audioConnectPromise.SetExceptionAsync(e).ConfigureAwait(false); - await DisconnectAudioAsync(); - } - finally - { - _audioLock.Release(); - } - } - public async Task FinishJoinAudioChannel() - { - await _audioLock.WaitAsync().ConfigureAwait(false); - try - { - if (AudioClient != null) - await _audioConnectPromise.TrySetResultAsync(AudioClient).ConfigureAwait(false); - } - finally - { - _audioLock.Release(); - } - } - - public SocketGuild Clone() => MemberwiseClone() as SocketGuild; - - new internal ISocketGuildChannel ToChannel(ChannelModel model) - { - switch (model.Type) - { - case ChannelType.Text: - return new SocketTextChannel(this, model); - case ChannelType.Voice: - return new SocketVoiceChannel(this, model); - default: - throw new InvalidOperationException($"Unexpected channel type: {model.Type}"); - } - } - - bool IUserGuild.IsOwner => OwnerId == Discord.CurrentUser.Id; - GuildPermissions IUserGuild.Permissions => CurrentUser.GuildPermissions; - IAudioClient IGuild.AudioClient => AudioClient; - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Messages/ISocketMessage.cs b/src/Discord.Net/WebSocket/Entities/Messages/ISocketMessage.cs deleted file mode 100644 index 818d62ec0..000000000 --- a/src/Discord.Net/WebSocket/Entities/Messages/ISocketMessage.cs +++ /dev/null @@ -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, UpdateSource source); - ISocketMessage Clone(); - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Messages/SocketSystemMessage.cs b/src/Discord.Net/WebSocket/Entities/Messages/SocketSystemMessage.cs deleted file mode 100644 index bcd95ddf2..000000000 --- a/src/Discord.Net/WebSocket/Entities/Messages/SocketSystemMessage.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Discord.Rest; -using Model = Discord.API.Message; - -namespace Discord.WebSocket -{ - internal class SocketSystemMessage : SystemMessage, ISocketMessage - { - internal override bool IsAttached => true; - - public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; - public new ISocketMessageChannel Channel => base.Channel as ISocketMessageChannel; - - public SocketSystemMessage(ISocketMessageChannel channel, IUser author, Model model) - : base(channel, author, model) - { - } - - public ISocketMessage Clone() => MemberwiseClone() as ISocketMessage; - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net/WebSocket/Entities/Messages/SocketUserMessage.cs deleted file mode 100644 index 31ef2082a..000000000 --- a/src/Discord.Net/WebSocket/Entities/Messages/SocketUserMessage.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Discord.Rest; -using Model = Discord.API.Message; - -namespace Discord.WebSocket -{ - internal class SocketUserMessage : UserMessage, ISocketMessage - { - internal override bool IsAttached => true; - - public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; - public new ISocketMessageChannel Channel => base.Channel as ISocketMessageChannel; - - public SocketUserMessage(ISocketMessageChannel channel, IUser author, Model model) - : base(channel, author, model) - { - } - - public ISocketMessage Clone() => MemberwiseClone() as ISocketMessage; - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Users/ISocketUser.cs b/src/Discord.Net/WebSocket/Entities/Users/ISocketUser.cs deleted file mode 100644 index bf152eae2..000000000 --- a/src/Discord.Net/WebSocket/Entities/Users/ISocketUser.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Discord.WebSocket -{ - internal interface ISocketUser : IUser, IEntity - { - SocketGlobalUser User { get; } - - ISocketUser Clone(); - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Users/Presence.cs b/src/Discord.Net/WebSocket/Entities/Users/Presence.cs deleted file mode 100644 index dad784870..000000000 --- a/src/Discord.Net/WebSocket/Entities/Users/Presence.cs +++ /dev/null @@ -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; - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Users/SocketDMUser.cs b/src/Discord.Net/WebSocket/Entities/Users/SocketDMUser.cs deleted file mode 100644 index b996ce94c..000000000 --- a/src/Discord.Net/WebSocket/Entities/Users/SocketDMUser.cs +++ /dev/null @@ -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.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, UpdateSource source) - { - 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})"; - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Users/SocketGlobalUser.cs b/src/Discord.Net/WebSocket/Entities/Users/SocketGlobalUser.cs deleted file mode 100644 index 3b23f7985..000000000 --- a/src/Discord.Net/WebSocket/Entities/Users/SocketGlobalUser.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Discord.Rest; -using System; -using Model = Discord.API.User; -using PresenceModel = Discord.API.Presence; - -namespace Discord.WebSocket -{ - internal class SocketGlobalUser : User, ISocketUser - { - internal override bool IsAttached => true; - private 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) - { - } - - 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, UpdateSource source) - { - lock (_lockObj) - base.Update(model, source); - } - public void Update(PresenceModel model, UpdateSource source) - { - //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(); - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Users/SocketGroupUser.cs b/src/Discord.Net/WebSocket/Entities/Users/SocketGroupUser.cs deleted file mode 100644 index f19dc6b9d..000000000 --- a/src/Discord.Net/WebSocket/Entities/Users/SocketGroupUser.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Discord.Rest; -using System.Diagnostics; - -namespace Discord.WebSocket -{ - [DebuggerDisplay("{DebuggerDisplay,nq}")] - internal class SocketGroupUser : GroupUser, ISocketUser - { - internal override bool IsAttached => true; - - public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; - public new SocketGroupChannel Channel => base.Channel as SocketGroupChannel; - public new SocketGlobalUser User => base.User as SocketGlobalUser; - public Presence Presence => User.Presence; //{ get; private set; } - - public override Game Game => Presence.Game; - public override UserStatus Status => Presence.Status; - - public VoiceState? VoiceState => Channel.GetVoiceState(Id); - public bool IsSelfDeafened => VoiceState?.IsSelfDeafened ?? false; - public bool IsSelfMuted => VoiceState?.IsSelfMuted ?? false; - public bool IsSuppressed => VoiceState?.IsSuppressed ?? false; - public SocketVoiceChannel VoiceChannel => VoiceState?.VoiceChannel; - - public SocketGroupUser(SocketGroupChannel channel, SocketGlobalUser user) - : base(channel, user) - { - } - - public SocketGroupUser Clone() => MemberwiseClone() as SocketGroupUser; - ISocketUser ISocketUser.Clone() => Clone(); - - public override string ToString() => $"{Username}#{Discriminator}"; - private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id})"; - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net/WebSocket/Entities/Users/SocketGuildUser.cs deleted file mode 100644 index ab01956cc..000000000 --- a/src/Discord.Net/WebSocket/Entities/Users/SocketGuildUser.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Discord.Rest; -using Model = Discord.API.GuildMember; -using PresenceModel = Discord.API.Presence; - -namespace Discord.WebSocket -{ - internal class SocketGuildUser : GuildUser, ISocketUser, IVoiceState - { - internal override bool IsAttached => true; - - public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; - public new SocketGuild Guild => base.Guild as SocketGuild; - public new SocketGlobalUser User => base.User as SocketGlobalUser; - public Presence Presence => User.Presence; //{ get; private set; } - - public override Game Game => Presence.Game; - public override UserStatus Status => Presence.Status; - - public VoiceState? VoiceState => Guild.GetVoiceState(Id); - public bool IsSelfDeafened => VoiceState?.IsSelfDeafened ?? false; - public bool IsSelfMuted => VoiceState?.IsSelfMuted ?? false; - public bool IsSuppressed => VoiceState?.IsSuppressed ?? false; - public SocketVoiceChannel VoiceChannel => VoiceState?.VoiceChannel; - public bool IsDeafened => VoiceState?.IsDeafened ?? false; - public bool IsMuted => VoiceState?.IsMuted ?? false; - public string VoiceSessionId => VoiceState?.VoiceSessionId ?? ""; - - public SocketGuildUser(SocketGuild guild, SocketGlobalUser user, Model model) - : base(guild, user, model) - { - //Presence = new Presence(null, UserStatus.Offline); - } - public SocketGuildUser(SocketGuild guild, SocketGlobalUser user, PresenceModel model) - : base(guild, user, model) - { - } - - public override void Update(PresenceModel model, UpdateSource source) - { - base.Update(model, source); - - var game = model.Game != null ? new Game(model.Game) : null; - //Presence = new Presence(game, model.Status); - - User.Update(model, source); - } - - IVoiceChannel IVoiceState.VoiceChannel => VoiceState?.VoiceChannel; - - public SocketGuildUser Clone() => MemberwiseClone() as SocketGuildUser; - ISocketUser ISocketUser.Clone() => Clone(); - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Users/SocketSelfUser.cs b/src/Discord.Net/WebSocket/Entities/Users/SocketSelfUser.cs deleted file mode 100644 index e0acfbfc1..000000000 --- a/src/Discord.Net/WebSocket/Entities/Users/SocketSelfUser.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Discord.API.Rest; -using Discord.Rest; -using System; -using System.Threading.Tasks; -using Model = Discord.API.User; - -namespace Discord.WebSocket -{ - internal class SocketSelfUser : SelfUser, ISocketUser, ISelfUser - { - internal override bool IsAttached => true; - - public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; - SocketGlobalUser ISocketUser.User { get { throw new NotSupportedException(); } } - - public SocketSelfUser(DiscordSocketClient discord, Model model) - : base(discord, model) - { - } - - public async Task ModifyStatusAsync(Action func) - { - if (func == null) throw new NullReferenceException(nameof(func)); - - var args = new ModifyPresenceParams(); - func(args); - - var game = args._game.GetValueOrDefault(_game); - var status = args._status.GetValueOrDefault(_status); - - long idleSince = _idleSince; - if (status == UserStatus.Idle && _status != UserStatus.Idle) - idleSince = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - var apiGame = game != null ? new API.Game { Name = game.Name, StreamType = game.StreamType, StreamUrl = game.StreamUrl } : null; - - await Discord.ApiClient.SendStatusUpdateAsync(status == UserStatus.Idle ? _idleSince : (long?)null, apiGame).ConfigureAwait(false); - - //Save values - _idleSince = idleSince; - _game = game; - _status = status; - } - - public SocketSelfUser Clone() => MemberwiseClone() as SocketSelfUser; - ISocketUser ISocketUser.Clone() => Clone(); - } -} diff --git a/src/Discord.Net/WebSocket/Extensions/ChannelExtensions.cs b/src/Discord.Net/WebSocket/Extensions/ChannelExtensions.cs deleted file mode 100644 index ab9911cac..000000000 --- a/src/Discord.Net/WebSocket/Extensions/ChannelExtensions.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Discord.WebSocket -{ - public static class ChannelExtensions - { - public static IUser GetUser(this IDMChannel channel, ulong id) - => GetSocketDMChannel(channel).GetUser(id); - - public static IReadOnlyCollection GetUsers(this IDMChannel channel) - => GetSocketDMChannel(channel).Users; - - public static IUser GetUser(this IGroupChannel channel, ulong id) - => GetSocketGroupChannel(channel).GetUser(id); - - public static IReadOnlyCollection GetUsers(this IGroupChannel channel) - => GetSocketGroupChannel(channel).Users; - - public static IGuildUser GetUser(this ITextChannel channel, ulong id) - => GetSocketTextChannel(channel).GetUser(id); - - public static IReadOnlyCollection GetUsers(this ITextChannel channel) - => GetSocketTextChannel(channel).Members; - - public static IGuildUser GetUser(this IVoiceChannel channel, ulong id) - => GetSocketVoiceChannel(channel).GetUser(id); - - public static IReadOnlyCollection GetUsers(this IVoiceChannel channel) - => GetSocketVoiceChannel(channel).Members; - - internal static SocketDMChannel GetSocketDMChannel(IDMChannel channel) - { - Preconditions.NotNull(channel, nameof(channel)); - var socketChannel = channel as SocketDMChannel; - if (socketChannel == null) - throw new InvalidOperationException("This extension method is only valid on WebSocket Entities"); - return socketChannel; - } - internal static SocketGroupChannel GetSocketGroupChannel(IGroupChannel channel) - { - Preconditions.NotNull(channel, nameof(channel)); - var socketChannel = channel as SocketGroupChannel; - if (socketChannel == null) - throw new InvalidOperationException("This extension method is only valid on WebSocket Entities"); - return socketChannel; - } - internal static SocketTextChannel GetSocketTextChannel(ITextChannel channel) - { - Preconditions.NotNull(channel, nameof(channel)); - var socketChannel = channel as SocketTextChannel; - if (socketChannel == null) - throw new InvalidOperationException("This extension method is only valid on WebSocket Entities"); - return socketChannel; - } - internal static SocketVoiceChannel GetSocketVoiceChannel(IVoiceChannel channel) - { - Preconditions.NotNull(channel, nameof(channel)); - var socketChannel = channel as SocketVoiceChannel; - if (socketChannel == null) - throw new InvalidOperationException("This extension method is only valid on WebSocket Entities"); - return socketChannel; - } - } -} diff --git a/src/Discord.Net/WebSocket/Extensions/DiscordClientExtensions.cs b/src/Discord.Net/WebSocket/Extensions/DiscordClientExtensions.cs deleted file mode 100644 index 8a5cfc9bd..000000000 --- a/src/Discord.Net/WebSocket/Extensions/DiscordClientExtensions.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace Discord.WebSocket -{ - public static class DiscordClientExtensions - { - public static IPrivateChannel GetPrivateChannel(this DiscordSocketClient client, ulong id) - => client.GetChannel(id) as IPrivateChannel; - - public static IDMChannel GetDMChannel(this DiscordSocketClient client, ulong id) - => client.GetPrivateChannelAsync(id) as IDMChannel; - public static IEnumerable GetDMChannels(this DiscordSocketClient client) - => client.GetPrivateChannels().Select(x => x as IDMChannel).Where(x => x != null); - - public static IGroupChannel GetGroupChannel(this DiscordSocketClient client, ulong id) - => client.GetPrivateChannel(id) as IGroupChannel; - public static IEnumerable GetGroupChannels(this DiscordSocketClient client) - => client.GetPrivateChannels().Select(x => x as IGroupChannel).Where(x => x != null); - - public static IVoiceRegion GetVoiceRegion(this DiscordSocketClient client, string id) - => client.VoiceRegions.FirstOrDefault(r => r.Id == id); - public static IReadOnlyCollection GetVoiceRegions(this DiscordSocketClient client) => - client.VoiceRegions; - public static IVoiceRegion GetOptimalVoiceRegion(this DiscordSocketClient client) - => client.VoiceRegions.FirstOrDefault(x => x.IsOptimal); - - public static IGuild GetGuild(this DiscordSocketClient client, ulong id) => - client.DataStore.GetGuild(id); - public static GuildEmbed? GetGuildEmbed(this DiscordSocketClient client, ulong id) - { - var guild = client.DataStore.GetGuild(id); - if (guild != null) - return new GuildEmbed(guild.IsEmbeddable, guild.EmbedChannelId); - return null; - } - public static IReadOnlyCollection GetGuilds(this DiscordSocketClient client) => - client.Guilds; - - public static IChannel GetChannel(this DiscordSocketClient client, ulong id) => - client.DataStore.GetChannel(id); - public static IReadOnlyCollection GetPrivateChannels(this DiscordSocketClient client) => - client.DataStore.PrivateChannels; - - public static IUser GetUser(this DiscordSocketClient client, ulong id) => - client.DataStore.GetUser(id); - public static IUser GetUser(this DiscordSocketClient client, string username, string discriminator) => - client.DataStore.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault(); - public static ISelfUser GetCurrentUser(this DiscordSocketClient client) => - client.CurrentUser; - - } -} diff --git a/src/Discord.Net/WebSocket/Extensions/GuildExtensions.cs b/src/Discord.Net/WebSocket/Extensions/GuildExtensions.cs deleted file mode 100644 index a2c5c7882..000000000 --- a/src/Discord.Net/WebSocket/Extensions/GuildExtensions.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Discord.WebSocket -{ - // Todo: Docstrings - public static class GuildExtensions - { - // Channels - public static IGuildChannel GetChannel(this IGuild guild, ulong id) => - GetSocketGuild(guild).GetChannel(id); - public static IReadOnlyCollection GetChannels(this IGuild guild) => - GetSocketGuild(guild).Channels; - - public static ITextChannel GetTextChannel(this IGuild guild, ulong id) => - GetSocketGuild(guild).GetChannel(id) as ITextChannel; - public static IEnumerable GetTextChannels(this IGuild guild) => - GetSocketGuild(guild).Channels.Select(c => c as ITextChannel).Where(c => c != null); - - - public static IVoiceChannel GetVoiceChannel(this IGuild guild, ulong id) => - GetSocketGuild(guild).GetChannel(id) as IVoiceChannel; - public static IEnumerable GetVoiceChannels(this IGuild guild) => - GetSocketGuild(guild).Channels.Select(c => c as IVoiceChannel).Where(c => c != null); - - // Users - public static IGuildUser GetCurrentUser(this IGuild guild) => - GetSocketGuild(guild).CurrentUser; - public static IGuildUser GetUser(this IGuild guild, ulong id) => - GetSocketGuild(guild).GetUser(id); - - public static IReadOnlyCollection GetUsers(this IGuild guild) => - GetSocketGuild(guild).Members; - - public static int GetUserCount(this IGuild guild) => - GetSocketGuild(guild).MemberCount; - public static int GetCachedUserCount(this IGuild guild) => - GetSocketGuild(guild).DownloadedMemberCount; - - //Helpers - internal static SocketGuild GetSocketGuild(IGuild guild) - { - Preconditions.NotNull(guild, nameof(guild)); - var socketGuild = guild as SocketGuild; - if (socketGuild == null) - throw new InvalidOperationException("This extension method is only valid on WebSocket Entities"); - return socketGuild; - } - } -} diff --git a/src/Discord.Net/project.json b/src/Discord.Net/project.json index 16fdb8aef..f7cb1aa13 100644 --- a/src/Discord.Net/project.json +++ b/src/Discord.Net/project.json @@ -1,6 +1,6 @@ { - "version": "1.0.0-beta-*", - "description": "An unofficial .Net API wrapper for the Discord service.", + "version": "1.0.0-beta2-*", + "description": "An aynchronous API wrapper for Discord using .NET. This package includes all of the optional Discord.Net components", "authors": [ "RogueException" ], "packOptions": { @@ -13,38 +13,22 @@ } }, - "buildOptions": { - "allowUnsafe": true, - "warningsAsErrors": false, - "xmlDoc": true - }, - - "configurations": { - "Release": { - "buildOptions": { - "define": [ "RELEASE" ], - "nowarn": [ "CS1573", "CS1591" ], - "optimize": true - } - } - }, - "dependencies": { - "Microsoft.Win32.Primitives": "4.0.1", - "Newtonsoft.Json": "8.0.3", - "System.Collections.Concurrent": "4.0.12", - "System.Collections.Immutable": "1.2.0", - "System.IO.Compression": "4.1.0", - "System.IO.FileSystem": "4.0.1", - "System.Net.Http": "4.1.0", - "System.Net.NameResolution": "4.0.0", - "System.Net.Sockets": "4.1.0", - "System.Net.WebSockets.Client": "4.0.0", - "System.Reflection.Extensions": "4.0.1", - "System.Runtime.InteropServices": "4.1.0", - "System.Runtime.InteropServices.RuntimeInformation": "4.0.0", - "System.Runtime.Serialization.Primitives": "4.1.1", - "System.Text.RegularExpressions": "4.1.0" + "Discord.Net.Core": { + "target": "project" + }, + "Discord.Net.Rest": { + "target": "project" + }, + "Discord.Net.WebSocket": { + "target": "project" + }, + "Discord.Net.Rpc": { + "target": "project" + }, + "Discord.Net.Commands": { + "target": "project" + } }, "frameworks": { @@ -56,4 +40,4 @@ ] } } -} +} \ No newline at end of file