diff --git a/src/stream-sniper/stream-sniper.sln b/src/stream-sniper/stream-sniper.sln new file mode 100644 index 0000000..1ed8aa5 --- /dev/null +++ b/src/stream-sniper/stream-sniper.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30204.135 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "stream-sniper", "stream-sniper\stream-sniper.csproj", "{D6DCDFC5-4DB3-4340-94F7-A5EC846E9195}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D6DCDFC5-4DB3-4340-94F7-A5EC846E9195}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D6DCDFC5-4DB3-4340-94F7-A5EC846E9195}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D6DCDFC5-4DB3-4340-94F7-A5EC846E9195}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D6DCDFC5-4DB3-4340-94F7-A5EC846E9195}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {02E9EA2E-1B01-4F93-8D52-142CA749AC0E} + EndGlobalSection +EndGlobal diff --git a/src/stream-sniper/stream-sniper/App.config b/src/stream-sniper/stream-sniper/App.config new file mode 100644 index 0000000..716fabe --- /dev/null +++ b/src/stream-sniper/stream-sniper/App.config @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/stream-sniper/stream-sniper/App.xaml b/src/stream-sniper/stream-sniper/App.xaml new file mode 100644 index 0000000..f645256 --- /dev/null +++ b/src/stream-sniper/stream-sniper/App.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/src/stream-sniper/stream-sniper/App.xaml.cs b/src/stream-sniper/stream-sniper/App.xaml.cs new file mode 100644 index 0000000..fccbe85 --- /dev/null +++ b/src/stream-sniper/stream-sniper/App.xaml.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; + +namespace stream_sniper +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/AliasAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/AliasAttribute.cs new file mode 100644 index 0000000..16eb3ba --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/AliasAttribute.cs @@ -0,0 +1,41 @@ +using System; + +namespace Discord.Commands +{ + /// + /// Marks the aliases for a command. + /// + /// + /// This attribute allows a command to have one or multiple aliases. In other words, the base command can have + /// multiple aliases when triggering the command itself, giving the end-user more freedom of choices when giving + /// hot-words to trigger the desired command. See the example for a better illustration. + /// + /// + /// In the following example, the command can be triggered with the base name, "stats", or either "stat" or + /// "info". + /// + /// [Command("stats")] + /// [Alias("stat", "info")] + /// public async Task GetStatsAsync(IUser user) + /// { + /// // ...pull stats + /// } + /// + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class AliasAttribute : Attribute + { + /// + /// Gets the aliases which have been defined for the command. + /// + public string[] Aliases { get; } + + /// + /// Creates a new with the given aliases. + /// + public AliasAttribute(params string[] aliases) + { + Aliases = aliases; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/CommandAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/CommandAttribute.cs new file mode 100644 index 0000000..d4d9ee3 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/CommandAttribute.cs @@ -0,0 +1,41 @@ +using System; + +namespace Discord.Commands +{ + /// + /// Marks the execution information for a command. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class CommandAttribute : Attribute + { + /// + /// Gets the text that has been set to be recognized as a command. + /// + public string Text { get; } + /// + /// Specifies the of the command. This affects how the command is executed. + /// + public RunMode RunMode { get; set; } = RunMode.Default; + public bool? IgnoreExtraArgs { get; } + + /// + public CommandAttribute() + { + Text = null; + } + + /// + /// Initializes a new attribute with the specified name. + /// + /// The name of the command. + public CommandAttribute(string text) + { + Text = text; + } + public CommandAttribute(string text, bool ignoreExtraArgs) + { + Text = text; + IgnoreExtraArgs = ignoreExtraArgs; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs new file mode 100644 index 0000000..7dbe1a4 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs @@ -0,0 +1,17 @@ +using System; + +namespace Discord.Commands +{ + /// + /// Prevents the marked module from being loaded automatically. + /// + /// + /// This attribute tells to ignore the marked module from being loaded + /// automatically (e.g. the method). If a non-public module marked + /// with this attribute is attempted to be loaded manually, the loading process will also fail. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public class DontAutoLoadAttribute : Attribute + { + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/DontInjectAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/DontInjectAttribute.cs new file mode 100644 index 0000000..72ca92f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/DontInjectAttribute.cs @@ -0,0 +1,31 @@ +using System; + +namespace Discord.Commands +{ + /// + /// Prevents the marked property from being injected into a module. + /// + /// + /// This attribute prevents the marked member from being injected into its parent module. Useful when you have a + /// public property that you do not wish to invoke the library's dependency injection service. + /// + /// + /// In the following example, DatabaseService will not be automatically injected into the module and will + /// not throw an error message if the dependency fails to be resolved. + /// + /// public class MyModule : ModuleBase + /// { + /// [DontInject] + /// public DatabaseService DatabaseService; + /// public MyModule() + /// { + /// DatabaseService = DatabaseFactory.Generate(); + /// } + /// } + /// + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public class DontInjectAttribute : Attribute + { + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/GroupAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/GroupAttribute.cs new file mode 100644 index 0000000..e1e38cf --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/GroupAttribute.cs @@ -0,0 +1,30 @@ +using System; + +namespace Discord.Commands +{ + /// + /// Marks the module as a command group. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public class GroupAttribute : Attribute + { + /// + /// Gets the prefix set for the module. + /// + public string Prefix { get; } + + /// + public GroupAttribute() + { + Prefix = null; + } + /// + /// Initializes a new with the provided prefix. + /// + /// The prefix of the module group. + public GroupAttribute(string prefix) + { + Prefix = prefix; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/NameAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/NameAttribute.cs new file mode 100644 index 0000000..a6e1f2e --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/NameAttribute.cs @@ -0,0 +1,26 @@ +using System; + +namespace Discord.Commands +{ + // Override public name of command/module + /// + /// Marks the public name of a command, module, or parameter. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] + public class NameAttribute : Attribute + { + /// + /// Gets the name of the command. + /// + public string Text { get; } + + /// + /// Marks the public name of a command, module, or parameter with the provided name. + /// + /// The public name of the object. + public NameAttribute(string text) + { + Text = text; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/NamedArgumentTypeAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/NamedArgumentTypeAttribute.cs new file mode 100644 index 0000000..e857172 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/NamedArgumentTypeAttribute.cs @@ -0,0 +1,11 @@ +using System; + +namespace Discord.Commands +{ + /// + /// Instructs the command system to treat command parameters of this type + /// as a collection of named arguments matching to its properties. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public sealed class NamedArgumentTypeAttribute : Attribute { } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs new file mode 100644 index 0000000..a44dcb6 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs @@ -0,0 +1,50 @@ +using System; +using System.Reflection; + +namespace Discord.Commands +{ + /// + /// Marks the to be read by the specified . + /// + /// + /// This attribute will override the to be used when parsing for the + /// desired type in the command. This is useful when one wishes to use a particular + /// without affecting other commands that are using the same target + /// type. + /// + /// If the given type reader does not inherit from , an + /// will be thrown. + /// + /// + /// + /// In this example, the will be read by a custom + /// , FriendlyTimeSpanTypeReader, instead of the + /// shipped by Discord.Net. + /// + /// [Command("time")] + /// public Task GetTimeAsync([OverrideTypeReader(typeof(FriendlyTimeSpanTypeReader))]TimeSpan time) + /// => ReplyAsync(time); + /// + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public sealed class OverrideTypeReaderAttribute : Attribute + { + private static readonly TypeInfo TypeReaderTypeInfo = typeof(TypeReader).GetTypeInfo(); + + /// + /// Gets the specified of the parameter. + /// + public Type TypeReader { get; } + + /// + /// The to be used with the parameter. + /// The given does not inherit from . + public OverrideTypeReaderAttribute(Type overridenTypeReader) + { + if (!TypeReaderTypeInfo.IsAssignableFrom(overridenTypeReader.GetTypeInfo())) + throw new ArgumentException($"{nameof(overridenTypeReader)} must inherit from {nameof(TypeReader)}."); + + TypeReader = overridenTypeReader; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs new file mode 100644 index 0000000..8ee46f9 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs @@ -0,0 +1,22 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + /// + /// Requires the parameter to pass the specified precondition before execution can begin. + /// + /// + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true, Inherited = true)] + public abstract class ParameterPreconditionAttribute : Attribute + { + /// + /// Checks whether the condition is met before execution of the command. + /// + /// The context of the command. + /// The parameter of the command being checked against. + /// The raw value of the parameter. + /// The service collection used for dependency injection. + public abstract Task CheckPermissionsAsync(ICommandContext context, ParameterInfo parameter, object value, IServiceProvider services); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/PreconditionAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/PreconditionAttribute.cs new file mode 100644 index 0000000..37a08ba --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/PreconditionAttribute.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + /// + /// Requires the module or class to pass the specified precondition before execution can begin. + /// + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = true)] + public abstract class PreconditionAttribute : Attribute + { + /// + /// Specifies a group that this precondition belongs to. + /// + /// + /// of the same group require only one of the preconditions to pass in order to + /// be successful (A || B). Specifying = null or not at all will + /// require *all* preconditions to pass, just like normal (A && B). + /// + public string Group { get; set; } = null; + + /// + /// When overridden in a derived class, uses the supplied string + /// as the error message if the precondition doesn't pass. + /// Setting this for a class that doesn't override + /// this property is a no-op. + /// + public virtual string ErrorMessage { get { return null; } set { } } + + /// + /// Checks if the has the sufficient permission to be executed. + /// + /// The context of the command. + /// The command being executed. + /// The service collection used for dependency injection. + public abstract Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs new file mode 100644 index 0000000..5b3b5bd --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs @@ -0,0 +1,86 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + /// + /// Requires the bot to have a specific permission in the channel a command is invoked in. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] + public class RequireBotPermissionAttribute : PreconditionAttribute + { + /// + /// Gets the specified of the precondition. + /// + public GuildPermission? GuildPermission { get; } + /// + /// Gets the specified of the precondition. + /// + public ChannelPermission? ChannelPermission { get; } + /// + public override string ErrorMessage { get; set; } + /// + /// Gets or sets the error message if the precondition + /// fails due to being run outside of a Guild channel. + /// + public string NotAGuildErrorMessage { get; set; } + + /// + /// Requires the bot account to have a specific . + /// + /// + /// This precondition will always fail if the command is being invoked in a . + /// + /// + /// The that the bot must have. Multiple permissions can be specified + /// by ORing the permissions together. + /// + public RequireBotPermissionAttribute(GuildPermission permission) + { + GuildPermission = permission; + ChannelPermission = null; + } + /// + /// Requires that the bot account to have a specific . + /// + /// + /// The that the bot must have. Multiple permissions can be + /// specified by ORing the permissions together. + /// + public RequireBotPermissionAttribute(ChannelPermission permission) + { + ChannelPermission = permission; + GuildPermission = null; + } + + /// + public override async Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) + { + IGuildUser guildUser = null; + if (context.Guild != null) + guildUser = await context.Guild.GetCurrentUserAsync().ConfigureAwait(false); + + if (GuildPermission.HasValue) + { + if (guildUser == null) + return PreconditionResult.FromError(NotAGuildErrorMessage ?? "Command must be used in a guild channel."); + if (!guildUser.GuildPermissions.Has(GuildPermission.Value)) + return PreconditionResult.FromError(ErrorMessage ?? $"Bot requires guild permission {GuildPermission.Value}."); + } + + if (ChannelPermission.HasValue) + { + ChannelPermissions perms; + if (context.Channel is IGuildChannel guildChannel) + perms = guildUser.GetPermissions(guildChannel); + else + perms = ChannelPermissions.All(context.Channel); + + if (!perms.Has(ChannelPermission.Value)) + return PreconditionResult.FromError(ErrorMessage ?? $"Bot requires channel permission {ChannelPermission.Value}."); + } + + return PreconditionResult.FromSuccess(); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs new file mode 100644 index 0000000..a27469c --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs @@ -0,0 +1,74 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + /// + /// Defines the type of command context (i.e. where the command is being executed). + /// + [Flags] + public enum ContextType + { + /// + /// Specifies the command to be executed within a guild. + /// + Guild = 0x01, + /// + /// Specifies the command to be executed within a DM. + /// + DM = 0x02, + /// + /// Specifies the command to be executed within a group. + /// + Group = 0x04 + } + + /// + /// Requires the command to be invoked in a specified context (e.g. in guild, DM). + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] + public class RequireContextAttribute : PreconditionAttribute + { + /// + /// Gets the context required to execute the command. + /// + public ContextType Contexts { get; } + /// + public override string ErrorMessage { get; set; } + + /// Requires the command to be invoked in the specified context. + /// The type of context the command can be invoked in. Multiple contexts can be specified by ORing the contexts together. + /// + /// + /// [Command("secret")] + /// [RequireContext(ContextType.DM | ContextType.Group)] + /// public Task PrivateOnlyAsync() + /// { + /// return ReplyAsync("shh, this command is a secret"); + /// } + /// + /// + public RequireContextAttribute(ContextType contexts) + { + Contexts = contexts; + } + + /// + public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) + { + bool isValid = false; + + if ((Contexts & ContextType.Guild) != 0) + isValid = context.Channel is IGuildChannel; + if ((Contexts & ContextType.DM) != 0) + isValid = isValid || context.Channel is IDMChannel; + if ((Contexts & ContextType.Group) != 0) + isValid = isValid || context.Channel is IGroupChannel; + + if (isValid) + return Task.FromResult(PreconditionResult.FromSuccess()); + else + return Task.FromResult(PreconditionResult.FromError(ErrorMessage ?? $"Invalid context for command; accepted contexts: {Contexts}.")); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs new file mode 100644 index 0000000..2a9647c --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + /// + /// Requires the command to be invoked in a channel marked NSFW. + /// + /// + /// The precondition will restrict the access of the command or module to be accessed within a guild channel + /// that has been marked as mature or NSFW. If the channel is not of type or the + /// channel is not marked as NSFW, the precondition will fail with an erroneous . + /// + /// + /// The following example restricts the command too-cool to an NSFW-enabled channel only. + /// + /// public class DankModule : ModuleBase + /// { + /// [Command("cool")] + /// public Task CoolAsync() + /// => ReplyAsync("I'm cool for everyone."); + /// + /// [RequireNsfw] + /// [Command("too-cool")] + /// public Task TooCoolAsync() + /// => ReplyAsync("You can only see this if you're cool enough."); + /// } + /// + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] + public class RequireNsfwAttribute : PreconditionAttribute + { + /// + public override string ErrorMessage { get; set; } + + /// + public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) + { + if (context.Channel is ITextChannel text && text.IsNsfw) + return Task.FromResult(PreconditionResult.FromSuccess()); + else + return Task.FromResult(PreconditionResult.FromError(ErrorMessage ?? "This command may only be invoked in an NSFW channel.")); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs new file mode 100644 index 0000000..c08e1e9 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs @@ -0,0 +1,55 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + /// + /// Requires the command to be invoked by the owner of the bot. + /// + /// + /// This precondition will restrict the access of the command or module to the owner of the Discord application. + /// If the precondition fails to be met, an erroneous will be returned with the + /// message "Command can only be run by the owner of the bot." + /// + /// This precondition will only work if the account has a of + /// ;otherwise, this precondition will always fail. + /// + /// + /// + /// The following example restricts the command to a set of sensitive commands that only the owner of the bot + /// application should be able to access. + /// + /// [RequireOwner] + /// [Group("admin")] + /// public class AdminModule : ModuleBase + /// { + /// [Command("exit")] + /// public async Task ExitAsync() + /// { + /// Environment.Exit(0); + /// } + /// } + /// + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] + public class RequireOwnerAttribute : PreconditionAttribute + { + /// + public override string ErrorMessage { get; set; } + + /// + public override async Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) + { + switch (context.Client.TokenType) + { + case TokenType.Bot: + var application = await context.Client.GetApplicationInfoAsync().ConfigureAwait(false); + if (context.User.Id != application.Owner.Id) + return PreconditionResult.FromError(ErrorMessage ?? "Command can only be run by the owner of the bot."); + return PreconditionResult.FromSuccess(); + default: + return PreconditionResult.FromError($"{nameof(RequireOwnerAttribute)} is not supported by this {nameof(TokenType)}."); + } + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs new file mode 100644 index 0000000..2908a18 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs @@ -0,0 +1,84 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + /// + /// Requires the user invoking the command to have a specified permission. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] + public class RequireUserPermissionAttribute : PreconditionAttribute + { + /// + /// Gets the specified of the precondition. + /// + public GuildPermission? GuildPermission { get; } + /// + /// Gets the specified of the precondition. + /// + public ChannelPermission? ChannelPermission { get; } + /// + public override string ErrorMessage { get; set; } + /// + /// Gets or sets the error message if the precondition + /// fails due to being run outside of a Guild channel. + /// + public string NotAGuildErrorMessage { get; set; } + + /// + /// Requires that the user invoking the command to have a specific . + /// + /// + /// This precondition will always fail if the command is being invoked in a . + /// + /// + /// The that the user must have. Multiple permissions can be + /// specified by ORing the permissions together. + /// + public RequireUserPermissionAttribute(GuildPermission permission) + { + GuildPermission = permission; + ChannelPermission = null; + } + /// + /// Requires that the user invoking the command to have a specific . + /// + /// + /// The that the user must have. Multiple permissions can be + /// specified by ORing the permissions together. + /// + public RequireUserPermissionAttribute(ChannelPermission permission) + { + ChannelPermission = permission; + GuildPermission = null; + } + + /// + public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) + { + var guildUser = context.User as IGuildUser; + + if (GuildPermission.HasValue) + { + if (guildUser == null) + return Task.FromResult(PreconditionResult.FromError(NotAGuildErrorMessage ?? "Command must be used in a guild channel.")); + if (!guildUser.GuildPermissions.Has(GuildPermission.Value)) + return Task.FromResult(PreconditionResult.FromError(ErrorMessage ?? $"User requires guild permission {GuildPermission.Value}.")); + } + + if (ChannelPermission.HasValue) + { + ChannelPermissions perms; + if (context.Channel is IGuildChannel guildChannel) + perms = guildUser.GetPermissions(guildChannel); + else + perms = ChannelPermissions.All(context.Channel); + + if (!perms.Has(ChannelPermission.Value)) + return Task.FromResult(PreconditionResult.FromError(ErrorMessage ?? $"User requires channel permission {ChannelPermission.Value}.")); + } + + return Task.FromResult(PreconditionResult.FromSuccess()); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/PriorityAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/PriorityAttribute.cs new file mode 100644 index 0000000..75ffd25 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/PriorityAttribute.cs @@ -0,0 +1,24 @@ +using System; + +namespace Discord.Commands +{ + /// + /// Sets priority of commands. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class PriorityAttribute : Attribute + { + /// + /// Gets the priority which has been set for the command. + /// + public int Priority { get; } + + /// + /// Initializes a new attribute with the given priority. + /// + public PriorityAttribute(int priority) + { + Priority = priority; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/RemainderAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/RemainderAttribute.cs new file mode 100644 index 0000000..33e07f0 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/RemainderAttribute.cs @@ -0,0 +1,12 @@ +using System; + +namespace Discord.Commands +{ + /// + /// Marks the input to not be parsed by the parser. + /// + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] + public class RemainderAttribute : Attribute + { + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/RemarksAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/RemarksAttribute.cs new file mode 100644 index 0000000..2fbe2bf --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/RemarksAttribute.cs @@ -0,0 +1,19 @@ +using System; + +namespace Discord.Commands +{ + // Extension of the Cosmetic Summary, for Groups, Commands, and Parameters + /// + /// Attaches remarks to your commands. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public class RemarksAttribute : Attribute + { + public string Text { get; } + + public RemarksAttribute(string text) + { + Text = text; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/SummaryAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/SummaryAttribute.cs new file mode 100644 index 0000000..57e9b02 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/SummaryAttribute.cs @@ -0,0 +1,19 @@ +using System; + +namespace Discord.Commands +{ + // Cosmetic Summary, for Groups and Commands + /// + /// Attaches a summary to your command. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] + public class SummaryAttribute : Attribute + { + public string Text { get; } + + public SummaryAttribute(string text) + { + Text = text; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Builders/CommandBuilder.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Builders/CommandBuilder.cs new file mode 100644 index 0000000..3f1ca88 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Builders/CommandBuilder.cs @@ -0,0 +1,144 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace Discord.Commands.Builders +{ + public class CommandBuilder + { + private readonly List _preconditions; + private readonly List _parameters; + private readonly List _attributes; + private readonly List _aliases; + + public ModuleBuilder Module { get; } + internal Func Callback { get; set; } + + public string Name { get; set; } + public string Summary { get; set; } + public string Remarks { get; set; } + public string PrimaryAlias { get; set; } + public RunMode RunMode { get; set; } + public int Priority { get; set; } + public bool IgnoreExtraArgs { get; set; } + + public IReadOnlyList Preconditions => _preconditions; + public IReadOnlyList Parameters => _parameters; + public IReadOnlyList Attributes => _attributes; + public IReadOnlyList Aliases => _aliases; + + //Automatic + internal CommandBuilder(ModuleBuilder module) + { + Module = module; + + _preconditions = new List(); + _parameters = new List(); + _attributes = new List(); + _aliases = new List(); + } + //User-defined + internal CommandBuilder(ModuleBuilder module, string primaryAlias, Func callback) + : this(module) + { + Discord.Preconditions.NotNull(primaryAlias, nameof(primaryAlias)); + Discord.Preconditions.NotNull(callback, nameof(callback)); + + Callback = callback; + PrimaryAlias = primaryAlias; + _aliases.Add(primaryAlias); + } + + public CommandBuilder WithName(string name) + { + Name = name; + return this; + } + public CommandBuilder WithSummary(string summary) + { + Summary = summary; + return this; + } + public CommandBuilder WithRemarks(string remarks) + { + Remarks = remarks; + return this; + } + public CommandBuilder WithRunMode(RunMode runMode) + { + RunMode = runMode; + return this; + } + public CommandBuilder WithPriority(int priority) + { + Priority = priority; + return this; + } + + public CommandBuilder AddAliases(params string[] aliases) + { + for (int i = 0; i < aliases.Length; i++) + { + string alias = aliases[i] ?? ""; + if (!_aliases.Contains(alias)) + _aliases.Add(alias); + } + return this; + } + public CommandBuilder AddAttributes(params Attribute[] attributes) + { + _attributes.AddRange(attributes); + return this; + } + public CommandBuilder AddPrecondition(PreconditionAttribute precondition) + { + _preconditions.Add(precondition); + return this; + } + public CommandBuilder AddParameter(string name, Action createFunc) + { + var param = new ParameterBuilder(this, name, typeof(T)); + createFunc(param); + _parameters.Add(param); + return this; + } + public CommandBuilder AddParameter(string name, Type type, Action createFunc) + { + var param = new ParameterBuilder(this, name, type); + createFunc(param); + _parameters.Add(param); + return this; + } + internal CommandBuilder AddParameter(Action createFunc) + { + var param = new ParameterBuilder(this); + createFunc(param); + _parameters.Add(param); + return this; + } + + /// Only the last parameter in a command may have the Remainder or Multiple flag. + internal CommandInfo Build(ModuleInfo info, CommandService service) + { + //Default name to primary alias + if (Name == null) + Name = PrimaryAlias; + + if (_parameters.Count > 0) + { + var lastParam = _parameters[_parameters.Count - 1]; + + var firstMultipleParam = _parameters.FirstOrDefault(x => x.IsMultiple); + if ((firstMultipleParam != null) && (firstMultipleParam != lastParam)) + throw new InvalidOperationException($"Only the last parameter in a command may have the Multiple flag. Parameter: {firstMultipleParam.Name} in {PrimaryAlias}"); + + var firstRemainderParam = _parameters.FirstOrDefault(x => x.IsRemainder); + if ((firstRemainderParam != null) && (firstRemainderParam != lastParam)) + throw new InvalidOperationException($"Only the last parameter in a command may have the Remainder flag. Parameter: {firstRemainderParam.Name} in {PrimaryAlias}"); + } + + return new CommandInfo(this, info, service); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Builders/ModuleBuilder.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Builders/ModuleBuilder.cs new file mode 100644 index 0000000..6dc50db --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Builders/ModuleBuilder.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading.Tasks; + +namespace Discord.Commands.Builders +{ + public class ModuleBuilder + { + private readonly List _commands; + private readonly List _submodules; + private readonly List _preconditions; + private readonly List _attributes; + private readonly List _aliases; + + public CommandService Service { get; } + public ModuleBuilder Parent { get; } + public string Name { get; set; } + public string Summary { get; set; } + public string Remarks { get; set; } + public string Group { get; set; } + + public IReadOnlyList Commands => _commands; + public IReadOnlyList Modules => _submodules; + public IReadOnlyList Preconditions => _preconditions; + public IReadOnlyList Attributes => _attributes; + public IReadOnlyList Aliases => _aliases; + + internal TypeInfo TypeInfo { get; set; } + + //Automatic + internal ModuleBuilder(CommandService service, ModuleBuilder parent) + { + Service = service; + Parent = parent; + + _commands = new List(); + _submodules = new List(); + _preconditions = new List(); + _attributes = new List(); + _aliases = new List(); + } + //User-defined + internal ModuleBuilder(CommandService service, ModuleBuilder parent, string primaryAlias) + : this(service, parent) + { + Discord.Preconditions.NotNull(primaryAlias, nameof(primaryAlias)); + + _aliases = new List { primaryAlias }; + } + + public ModuleBuilder WithName(string name) + { + Name = name; + return this; + } + public ModuleBuilder WithSummary(string summary) + { + Summary = summary; + return this; + } + public ModuleBuilder WithRemarks(string remarks) + { + Remarks = remarks; + return this; + } + + public ModuleBuilder AddAliases(params string[] aliases) + { + for (int i = 0; i < aliases.Length; i++) + { + string alias = aliases[i] ?? ""; + if (!_aliases.Contains(alias)) + _aliases.Add(alias); + } + return this; + } + public ModuleBuilder AddAttributes(params Attribute[] attributes) + { + _attributes.AddRange(attributes); + return this; + } + public ModuleBuilder AddPrecondition(PreconditionAttribute precondition) + { + _preconditions.Add(precondition); + return this; + } + public ModuleBuilder AddCommand(string primaryAlias, Func callback, Action createFunc) + { + var builder = new CommandBuilder(this, primaryAlias, callback); + createFunc(builder); + _commands.Add(builder); + return this; + } + internal ModuleBuilder AddCommand(Action createFunc) + { + var builder = new CommandBuilder(this); + createFunc(builder); + _commands.Add(builder); + return this; + } + public ModuleBuilder AddModule(string primaryAlias, Action createFunc) + { + var builder = new ModuleBuilder(Service, this, primaryAlias); + createFunc(builder); + _submodules.Add(builder); + return this; + } + internal ModuleBuilder AddModule(Action createFunc) + { + var builder = new ModuleBuilder(Service, this); + createFunc(builder); + _submodules.Add(builder); + return this; + } + + private ModuleInfo BuildImpl(CommandService service, IServiceProvider services, ModuleInfo parent = null) + { + //Default name to first alias + if (Name == null) + Name = _aliases[0]; + + if (TypeInfo != null && !TypeInfo.IsAbstract) + { + var moduleInstance = ReflectionUtils.CreateObject(TypeInfo, service, services); + moduleInstance.OnModuleBuilding(service, this); + } + + return new ModuleInfo(this, service, services, parent); + } + + public ModuleInfo Build(CommandService service, IServiceProvider services) => BuildImpl(service, services); + + internal ModuleInfo Build(CommandService service, IServiceProvider services, ModuleInfo parent) => BuildImpl(service, services, parent); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Builders/ModuleClassBuilder.cs new file mode 100644 index 0000000..aec8dcb --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -0,0 +1,315 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Reflection; +using System.Threading.Tasks; + +using Discord.Commands.Builders; + +namespace Discord.Commands +{ + internal static class ModuleClassBuilder + { + private static readonly TypeInfo ModuleTypeInfo = typeof(IModuleBase).GetTypeInfo(); + + public static async Task> SearchAsync(Assembly assembly, CommandService service) + { + bool IsLoadableModule(TypeInfo info) + { + return info.DeclaredMethods.Any(x => x.GetCustomAttribute() != null) && + info.GetCustomAttribute() == null; + } + + var result = new List(); + + foreach (var typeInfo in assembly.DefinedTypes) + { + if (typeInfo.IsPublic || typeInfo.IsNestedPublic) + { + if (IsValidModuleDefinition(typeInfo) && + !typeInfo.IsDefined(typeof(DontAutoLoadAttribute))) + { + result.Add(typeInfo); + } + } + else if (IsLoadableModule(typeInfo)) + { + await service._cmdLogger.WarningAsync($"Class {typeInfo.FullName} is not public and cannot be loaded. To suppress this message, mark the class with {nameof(DontAutoLoadAttribute)}.").ConfigureAwait(false); + } + } + + return result; + } + + + public static Task> BuildAsync(CommandService service, IServiceProvider services, params TypeInfo[] validTypes) => BuildAsync(validTypes, service, services); + public static async Task> BuildAsync(IEnumerable validTypes, CommandService service, IServiceProvider services) + { + /*if (!validTypes.Any()) + throw new InvalidOperationException("Could not find any valid modules from the given selection");*/ + + var topLevelGroups = validTypes.Where(x => x.DeclaringType == null || !IsValidModuleDefinition(x.DeclaringType.GetTypeInfo())); + + var builtTypes = new List(); + + var result = new Dictionary(); + + foreach (var typeInfo in topLevelGroups) + { + // TODO: This shouldn't be the case; may be safe to remove? + if (result.ContainsKey(typeInfo.AsType())) + continue; + + var module = new ModuleBuilder(service, null); + + BuildModule(module, typeInfo, service, services); + BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service, services); + builtTypes.Add(typeInfo); + + result[typeInfo.AsType()] = module.Build(service, services); + } + + await service._cmdLogger.DebugAsync($"Successfully built {builtTypes.Count} modules.").ConfigureAwait(false); + + return result; + } + + private static void BuildSubTypes(ModuleBuilder builder, IEnumerable subTypes, List builtTypes, CommandService service, IServiceProvider services) + { + foreach (var typeInfo in subTypes) + { + if (!IsValidModuleDefinition(typeInfo)) + continue; + + if (builtTypes.Contains(typeInfo)) + continue; + + builder.AddModule((module) => + { + BuildModule(module, typeInfo, service, services); + BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service, services); + }); + + builtTypes.Add(typeInfo); + } + } + + private static void BuildModule(ModuleBuilder builder, TypeInfo typeInfo, CommandService service, IServiceProvider services) + { + var attributes = typeInfo.GetCustomAttributes(); + builder.TypeInfo = typeInfo; + + foreach (var attribute in attributes) + { + switch (attribute) + { + case NameAttribute name: + builder.Name = name.Text; + break; + case SummaryAttribute summary: + builder.Summary = summary.Text; + break; + case RemarksAttribute remarks: + builder.Remarks = remarks.Text; + break; + case AliasAttribute alias: + builder.AddAliases(alias.Aliases); + break; + case GroupAttribute group: + builder.Name = builder.Name ?? group.Prefix; + builder.Group = group.Prefix; + builder.AddAliases(group.Prefix); + break; + case PreconditionAttribute precondition: + builder.AddPrecondition(precondition); + break; + default: + builder.AddAttributes(attribute); + break; + } + } + + //Check for unspecified info + if (builder.Aliases.Count == 0) + builder.AddAliases(""); + if (builder.Name == null) + builder.Name = typeInfo.Name; + + var validCommands = typeInfo.DeclaredMethods.Where(IsValidCommandDefinition); + + foreach (var method in validCommands) + { + builder.AddCommand((command) => + { + BuildCommand(command, typeInfo, method, service, services); + }); + } + } + + private static void BuildCommand(CommandBuilder builder, TypeInfo typeInfo, MethodInfo method, CommandService service, IServiceProvider serviceprovider) + { + var attributes = method.GetCustomAttributes(); + + foreach (var attribute in attributes) + { + switch (attribute) + { + case CommandAttribute command: + builder.AddAliases(command.Text); + builder.RunMode = command.RunMode; + builder.Name = builder.Name ?? command.Text; + builder.IgnoreExtraArgs = command.IgnoreExtraArgs ?? service._ignoreExtraArgs; + break; + case NameAttribute name: + builder.Name = name.Text; + break; + case PriorityAttribute priority: + builder.Priority = priority.Priority; + break; + case SummaryAttribute summary: + builder.Summary = summary.Text; + break; + case RemarksAttribute remarks: + builder.Remarks = remarks.Text; + break; + case AliasAttribute alias: + builder.AddAliases(alias.Aliases); + break; + case PreconditionAttribute precondition: + builder.AddPrecondition(precondition); + break; + default: + builder.AddAttributes(attribute); + break; + } + } + + if (builder.Name == null) + builder.Name = method.Name; + + var parameters = method.GetParameters(); + int pos = 0, count = parameters.Length; + foreach (var paramInfo in parameters) + { + builder.AddParameter((parameter) => + { + BuildParameter(parameter, paramInfo, pos++, count, service, serviceprovider); + }); + } + + var createInstance = ReflectionUtils.CreateBuilder(typeInfo, service); + + async Task ExecuteCallback(ICommandContext context, object[] args, IServiceProvider services, CommandInfo cmd) + { + var instance = createInstance(services); + instance.SetContext(context); + + try + { + instance.BeforeExecute(cmd); + + var task = method.Invoke(instance, args) as Task ?? Task.Delay(0); + if (task is Task resultTask) + { + return await resultTask.ConfigureAwait(false); + } + else + { + await task.ConfigureAwait(false); + return ExecuteResult.FromSuccess(); + } + } + finally + { + instance.AfterExecute(cmd); + (instance as IDisposable)?.Dispose(); + } + } + + builder.Callback = ExecuteCallback; + } + + private static void BuildParameter(ParameterBuilder builder, System.Reflection.ParameterInfo paramInfo, int position, int count, CommandService service, IServiceProvider services) + { + var attributes = paramInfo.GetCustomAttributes(); + var paramType = paramInfo.ParameterType; + + builder.Name = paramInfo.Name; + + builder.IsOptional = paramInfo.IsOptional; + builder.DefaultValue = paramInfo.HasDefaultValue ? paramInfo.DefaultValue : null; + + foreach (var attribute in attributes) + { + switch (attribute) + { + case SummaryAttribute summary: + builder.Summary = summary.Text; + break; + case OverrideTypeReaderAttribute typeReader: + builder.TypeReader = GetTypeReader(service, paramType, typeReader.TypeReader, services); + break; + case ParamArrayAttribute _: + builder.IsMultiple = true; + paramType = paramType.GetElementType(); + break; + case ParameterPreconditionAttribute precon: + builder.AddPrecondition(precon); + break; + case NameAttribute name: + builder.Name = name.Text; + break; + case RemainderAttribute _: + if (position != count - 1) + throw new InvalidOperationException($"Remainder parameters must be the last parameter in a command. Parameter: {paramInfo.Name} in {paramInfo.Member.DeclaringType.Name}.{paramInfo.Member.Name}"); + + builder.IsRemainder = true; + break; + default: + builder.AddAttributes(attribute); + break; + } + } + + builder.ParameterType = paramType; + + if (builder.TypeReader == null) + { + builder.TypeReader = service.GetDefaultTypeReader(paramType) + ?? service.GetTypeReaders(paramType)?.FirstOrDefault().Value; + } + } + + internal static TypeReader GetTypeReader(CommandService service, Type paramType, Type typeReaderType, IServiceProvider services) + { + var readers = service.GetTypeReaders(paramType); + TypeReader reader = null; + if (readers != null) + { + if (readers.TryGetValue(typeReaderType, out reader)) + return reader; + } + + //We dont have a cached type reader, create one + reader = ReflectionUtils.CreateObject(typeReaderType.GetTypeInfo(), service, services); + service.AddTypeReader(paramType, reader, false); + + return reader; + } + + private static bool IsValidModuleDefinition(TypeInfo typeInfo) + { + return ModuleTypeInfo.IsAssignableFrom(typeInfo) && + !typeInfo.IsAbstract && + !typeInfo.ContainsGenericParameters; + } + + private static bool IsValidCommandDefinition(MethodInfo methodInfo) + { + return methodInfo.IsDefined(typeof(CommandAttribute)) && + (methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task)) && + !methodInfo.IsStatic && + !methodInfo.IsGenericMethod; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Builders/ParameterBuilder.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Builders/ParameterBuilder.cs new file mode 100644 index 0000000..4ad5bfa --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Builders/ParameterBuilder.cs @@ -0,0 +1,136 @@ +using System; +using System.Linq; +using System.Reflection; + +using System.Collections.Generic; + +namespace Discord.Commands.Builders +{ + public class ParameterBuilder + { + private readonly List _preconditions; + private readonly List _attributes; + + public CommandBuilder Command { get; } + public string Name { get; internal set; } + public Type ParameterType { get; internal set; } + + public TypeReader TypeReader { get; set; } + public bool IsOptional { get; set; } + public bool IsRemainder { get; set; } + public bool IsMultiple { get; set; } + public object DefaultValue { get; set; } + public string Summary { get; set; } + + public IReadOnlyList Preconditions => _preconditions; + public IReadOnlyList Attributes => _attributes; + + //Automatic + internal ParameterBuilder(CommandBuilder command) + { + _preconditions = new List(); + _attributes = new List(); + + Command = command; + } + //User-defined + internal ParameterBuilder(CommandBuilder command, string name, Type type) + : this(command) + { + Discord.Preconditions.NotNull(name, nameof(name)); + + Name = name; + SetType(type); + } + + internal void SetType(Type type) + { + TypeReader = GetReader(type); + + if (type.GetTypeInfo().IsValueType) + DefaultValue = Activator.CreateInstance(type); + else if (type.IsArray) + type = ParameterType.GetElementType(); + ParameterType = type; + } + + private TypeReader GetReader(Type type) + { + var commands = Command.Module.Service; + if (type.GetTypeInfo().GetCustomAttribute() != null) + { + IsRemainder = true; + var reader = commands.GetTypeReaders(type)?.FirstOrDefault().Value; + if (reader == null) + { + Type readerType; + try + { + readerType = typeof(NamedArgumentTypeReader<>).MakeGenericType(new[] { type }); + } + catch (ArgumentException ex) + { + throw new InvalidOperationException($"Parameter type '{type.Name}' for command '{Command.Name}' must be a class with a public parameterless constructor to use as a NamedArgumentType.", ex); + } + + reader = (TypeReader)Activator.CreateInstance(readerType, new[] { commands }); + commands.AddTypeReader(type, reader); + } + + return reader; + } + + + var readers = commands.GetTypeReaders(type); + if (readers != null) + return readers.FirstOrDefault().Value; + else + return commands.GetDefaultTypeReader(type); + } + + public ParameterBuilder WithSummary(string summary) + { + Summary = summary; + return this; + } + public ParameterBuilder WithDefault(object defaultValue) + { + DefaultValue = defaultValue; + return this; + } + public ParameterBuilder WithIsOptional(bool isOptional) + { + IsOptional = isOptional; + return this; + } + public ParameterBuilder WithIsRemainder(bool isRemainder) + { + IsRemainder = isRemainder; + return this; + } + public ParameterBuilder WithIsMultiple(bool isMultiple) + { + IsMultiple = isMultiple; + return this; + } + + public ParameterBuilder AddAttributes(params Attribute[] attributes) + { + _attributes.AddRange(attributes); + return this; + } + public ParameterBuilder AddPrecondition(ParameterPreconditionAttribute precondition) + { + _preconditions.Add(precondition); + return this; + } + + internal ParameterInfo Build(CommandInfo info) + { + if ((TypeReader ?? (TypeReader = GetReader(ParameterType))) == null) + throw new InvalidOperationException($"No type reader found for type {ParameterType.Name}, one must be specified"); + + return new ParameterInfo(this, info, Command.Module.Service); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandContext.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandContext.cs new file mode 100644 index 0000000..393cdf9 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandContext.cs @@ -0,0 +1,34 @@ +namespace Discord.Commands +{ + /// The context of a command which may contain the client, user, guild, channel, and message. + public class CommandContext : ICommandContext + { + /// + public IDiscordClient Client { get; } + /// + public IGuild Guild { get; } + /// + public IMessageChannel Channel { get; } + /// + public IUser User { get; } + /// + public IUserMessage Message { get; } + + /// Indicates whether the channel that the command is executed in is a private channel. + public bool IsPrivate => Channel is IPrivateChannel; + + /// + /// Initializes a new class with the provided client and message. + /// + /// The underlying client. + /// The underlying message. + public CommandContext(IDiscordClient client, IUserMessage msg) + { + Client = client; + Guild = (msg.Channel as IGuildChannel)?.Guild; + Channel = msg.Channel; + User = msg.Author; + Message = msg; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandError.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandError.cs new file mode 100644 index 0000000..b487d8a --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandError.cs @@ -0,0 +1,51 @@ +namespace Discord.Commands +{ + /// Defines the type of error a command can throw. + public enum CommandError + { + //Search + /// + /// Thrown when the command is unknown. + /// + UnknownCommand = 1, + + //Parse + /// + /// Thrown when the command fails to be parsed. + /// + ParseFailed, + /// + /// Thrown when the input text has too few or too many arguments. + /// + BadArgCount, + + //Parse (Type Reader) + //CastFailed, + /// + /// Thrown when the object cannot be found by the . + /// + ObjectNotFound, + /// + /// Thrown when more than one object is matched by . + /// + MultipleMatches, + + //Preconditions + /// + /// Thrown when the command fails to meet a 's conditions. + /// + UnmetPrecondition, + + //Execute + /// + /// Thrown when an exception occurs mid-command execution. + /// + Exception, + + //Runtime + /// + /// Thrown when the command is not successfully executed on runtime. + /// + Unsuccessful + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandException.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandException.cs new file mode 100644 index 0000000..6c5ab0a --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandException.cs @@ -0,0 +1,30 @@ +using System; + +namespace Discord.Commands +{ + /// + /// The exception that is thrown if another exception occurs during a command execution. + /// + public class CommandException : Exception + { + /// Gets the command that caused the exception. + public CommandInfo Command { get; } + /// Gets the command context of the exception. + public ICommandContext Context { get; } + + /// + /// Initializes a new instance of the class using a + /// information, a context, and the exception that + /// interrupted the execution. + /// + /// The command information. + /// The context of the command. + /// The exception that interrupted the command execution. + public CommandException(CommandInfo command, ICommandContext context, Exception ex) + : base($"Error occurred executing {command.GetLogText(context)}.", ex) + { + Command = command; + Context = context; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandMatch.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandMatch.cs new file mode 100644 index 0000000..c15a332 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandMatch.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + public struct CommandMatch + { + /// The command that matches the search result. + public CommandInfo Command { get; } + /// The alias of the command. + public string Alias { get; } + + public CommandMatch(CommandInfo command, string alias) + { + Command = command; + Alias = alias; + } + + public Task CheckPreconditionsAsync(ICommandContext context, IServiceProvider services = null) + => Command.CheckPreconditionsAsync(context, services); + public Task ParseAsync(ICommandContext context, SearchResult searchResult, PreconditionResult preconditionResult = null, IServiceProvider services = null) + => Command.ParseAsync(context, Alias.Length, searchResult, preconditionResult, services); + public Task ExecuteAsync(ICommandContext context, IEnumerable argList, IEnumerable paramList, IServiceProvider services) + => Command.ExecuteAsync(context, argList, paramList, services); + public Task ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) + => Command.ExecuteAsync(context, parseResult, services); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandParser.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandParser.cs new file mode 100644 index 0000000..88698cd --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandParser.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + internal static class CommandParser + { + private enum ParserPart + { + None, + Parameter, + QuotedParameter + } + public static async Task ParseArgsAsync(CommandInfo command, ICommandContext context, bool ignoreExtraArgs, IServiceProvider services, string input, int startPos, IReadOnlyDictionary aliasMap) + { + ParameterInfo curParam = null; + StringBuilder argBuilder = new StringBuilder(input.Length); + int endPos = input.Length; + var curPart = ParserPart.None; + int lastArgEndPos = int.MinValue; + var argList = ImmutableArray.CreateBuilder(); + var paramList = ImmutableArray.CreateBuilder(); + bool isEscaping = false; + char c, matchQuote = '\0'; + + // local helper functions + bool IsOpenQuote(IReadOnlyDictionary dict, char ch) + { + // return if the key is contained in the dictionary if it is populated + if (dict.Count != 0) + return dict.ContainsKey(ch); + // or otherwise if it is the default double quote + return c == '\"'; + } + + char GetMatch(IReadOnlyDictionary dict, char ch) + { + // get the corresponding value for the key, if it exists + // and if the dictionary is populated + if (dict.Count != 0 && dict.TryGetValue(c, out var value)) + return value; + // or get the default pair of the default double quote + return '\"'; + } + + for (int curPos = startPos; curPos <= endPos; curPos++) + { + if (curPos < endPos) + c = input[curPos]; + else + c = '\0'; + + //If we're processing an remainder parameter, ignore all other logic + if (curParam != null && curParam.IsRemainder && curPos != endPos) + { + argBuilder.Append(c); + continue; + } + + //If this character is escaped, skip it + if (isEscaping) + { + if (curPos != endPos) + { + // if this character matches the quotation mark of the end of the string + // means that it should be escaped + // but if is not, then there is no reason to escape it then + if (c != matchQuote) + { + // if no reason to escape the next character, then re-add \ to the arg + argBuilder.Append('\\'); + } + + argBuilder.Append(c); + isEscaping = false; + continue; + } + } + //Are we escaping the next character? + if (c == '\\' && (curParam == null || !curParam.IsRemainder)) + { + isEscaping = true; + continue; + } + + //If we're not currently processing one, are we starting the next argument yet? + if (curPart == ParserPart.None) + { + if (char.IsWhiteSpace(c) || curPos == endPos) + continue; //Skip whitespace between arguments + else if (curPos == lastArgEndPos) + return ParseResult.FromError(CommandError.ParseFailed, "There must be at least one character of whitespace between arguments."); + else + { + if (curParam == null) + curParam = command.Parameters.Count > argList.Count ? command.Parameters[argList.Count] : null; + + if (curParam != null && curParam.IsRemainder) + { + argBuilder.Append(c); + continue; + } + + if (IsOpenQuote(aliasMap, c)) + { + curPart = ParserPart.QuotedParameter; + matchQuote = GetMatch(aliasMap, c); + continue; + } + curPart = ParserPart.Parameter; + } + } + + //Has this parameter ended yet? + string argString = null; + if (curPart == ParserPart.Parameter) + { + if (curPos == endPos || char.IsWhiteSpace(c)) + { + argString = argBuilder.ToString(); + lastArgEndPos = curPos; + } + else + argBuilder.Append(c); + } + else if (curPart == ParserPart.QuotedParameter) + { + if (c == matchQuote) + { + argString = argBuilder.ToString(); //Remove quotes + lastArgEndPos = curPos + 1; + } + else + argBuilder.Append(c); + } + + if (argString != null) + { + if (curParam == null) + { + if (command.IgnoreExtraArgs) + break; + else + return ParseResult.FromError(CommandError.BadArgCount, "The input text has too many parameters."); + } + + var typeReaderResult = await curParam.ParseAsync(context, argString, services).ConfigureAwait(false); + if (!typeReaderResult.IsSuccess && typeReaderResult.Error != CommandError.MultipleMatches) + return ParseResult.FromError(typeReaderResult, curParam); + + if (curParam.IsMultiple) + { + paramList.Add(typeReaderResult); + + curPart = ParserPart.None; + } + else + { + argList.Add(typeReaderResult); + + curParam = null; + curPart = ParserPart.None; + } + argBuilder.Clear(); + } + } + + if (curParam != null && curParam.IsRemainder) + { + var typeReaderResult = await curParam.ParseAsync(context, argBuilder.ToString(), services).ConfigureAwait(false); + if (!typeReaderResult.IsSuccess) + return ParseResult.FromError(typeReaderResult, curParam); + argList.Add(typeReaderResult); + } + + if (isEscaping) + return ParseResult.FromError(CommandError.ParseFailed, "Input text may not end on an incomplete escape."); + if (curPart == ParserPart.QuotedParameter) + return ParseResult.FromError(CommandError.ParseFailed, "A quoted parameter is incomplete."); + + //Add missing optionals + for (int i = argList.Count; i < command.Parameters.Count; i++) + { + var param = command.Parameters[i]; + if (param.IsMultiple) + continue; + if (!param.IsOptional) + return ParseResult.FromError(CommandError.BadArgCount, "The input text has too few parameters."); + argList.Add(TypeReaderResult.FromSuccess(param.DefaultValue)); + } + + return ParseResult.FromSuccess(argList.ToImmutable(), paramList.ToImmutable()); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandService.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandService.cs new file mode 100644 index 0000000..d5c060f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandService.cs @@ -0,0 +1,624 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Discord.Commands.Builders; +using Discord.Logging; + +namespace Discord.Commands +{ + /// + /// Provides a framework for building Discord commands. + /// + /// + /// + /// The service provides a framework for building Discord commands both dynamically via runtime builders or + /// statically via compile-time modules. To create a command module at compile-time, see + /// (most common); otherwise, see . + /// + /// + /// This service also provides several events for monitoring command usages; such as + /// for any command-related log events, and + /// for information about commands that have + /// been successfully executed. + /// + /// + public class CommandService : IDisposable + { + /// + /// Occurs when a command-related information is received. + /// + public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } + internal readonly AsyncEvent> _logEvent = new AsyncEvent>(); + + /// + /// Occurs when a command is successfully executed without any error. + /// + /// + /// This event is fired when a command has been executed, successfully or not. When a command fails to + /// execute during parsing or precondition stage, the CommandInfo may not be returned. + /// + public event Func, ICommandContext, IResult, Task> CommandExecuted { add { _commandExecutedEvent.Add(value); } remove { _commandExecutedEvent.Remove(value); } } + internal readonly AsyncEvent, ICommandContext, IResult, Task>> _commandExecutedEvent = new AsyncEvent, ICommandContext, IResult, Task>>(); + + private readonly SemaphoreSlim _moduleLock; + private readonly ConcurrentDictionary _typedModuleDefs; + private readonly ConcurrentDictionary> _typeReaders; + private readonly ConcurrentDictionary _defaultTypeReaders; + private readonly ImmutableList<(Type EntityType, Type TypeReaderType)> _entityTypeReaders; + private readonly HashSet _moduleDefs; + private readonly CommandMap _map; + + internal readonly bool _caseSensitive, _throwOnError, _ignoreExtraArgs; + internal readonly char _separatorChar; + internal readonly RunMode _defaultRunMode; + internal readonly Logger _cmdLogger; + internal readonly LogManager _logManager; + internal readonly IReadOnlyDictionary _quotationMarkAliasMap; + + internal bool _isDisposed; + + /// + /// Represents all modules loaded within . + /// + public IEnumerable Modules => _moduleDefs.Select(x => x); + + /// + /// Represents all commands loaded within . + /// + public IEnumerable Commands => _moduleDefs.SelectMany(x => x.Commands); + + /// + /// Represents all loaded within . + /// + public ILookup TypeReaders => _typeReaders.SelectMany(x => x.Value.Select(y => new { y.Key, y.Value })).ToLookup(x => x.Key, x => x.Value); + + /// + /// Initializes a new class. + /// + public CommandService() : this(new CommandServiceConfig()) { } + + /// + /// Initializes a new class with the provided configuration. + /// + /// The configuration class. + /// + /// The cannot be set to . + /// + public CommandService(CommandServiceConfig config) + { + _caseSensitive = config.CaseSensitiveCommands; + _throwOnError = config.ThrowOnError; + _ignoreExtraArgs = config.IgnoreExtraArgs; + _separatorChar = config.SeparatorChar; + _defaultRunMode = config.DefaultRunMode; + _quotationMarkAliasMap = (config.QuotationMarkAliasMap ?? new Dictionary()).ToImmutableDictionary(); + if (_defaultRunMode == RunMode.Default) + throw new InvalidOperationException("The default run mode cannot be set to Default."); + + _logManager = new LogManager(config.LogLevel); + _logManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); + _cmdLogger = _logManager.CreateLogger("Command"); + + _moduleLock = new SemaphoreSlim(1, 1); + _typedModuleDefs = new ConcurrentDictionary(); + _moduleDefs = new HashSet(); + _map = new CommandMap(this); + _typeReaders = new ConcurrentDictionary>(); + + _defaultTypeReaders = new ConcurrentDictionary(); + foreach (var type in PrimitiveParsers.SupportedTypes) + { + _defaultTypeReaders[type] = PrimitiveTypeReader.Create(type); + _defaultTypeReaders[typeof(Nullable<>).MakeGenericType(type)] = NullableTypeReader.Create(type, _defaultTypeReaders[type]); + } + + var tsreader = new TimeSpanTypeReader(); + _defaultTypeReaders[typeof(TimeSpan)] = tsreader; + _defaultTypeReaders[typeof(TimeSpan?)] = NullableTypeReader.Create(typeof(TimeSpan), tsreader); + + _defaultTypeReaders[typeof(string)] = + new PrimitiveTypeReader((string x, out string y) => { y = x; return true; }, 0); + + var entityTypeReaders = ImmutableList.CreateBuilder<(Type, Type)>(); + entityTypeReaders.Add((typeof(IMessage), typeof(MessageTypeReader<>))); + entityTypeReaders.Add((typeof(IChannel), typeof(ChannelTypeReader<>))); + entityTypeReaders.Add((typeof(IRole), typeof(RoleTypeReader<>))); + entityTypeReaders.Add((typeof(IUser), typeof(UserTypeReader<>))); + _entityTypeReaders = entityTypeReaders.ToImmutable(); + } + + //Modules + public async Task CreateModuleAsync(string primaryAlias, Action buildFunc) + { + await _moduleLock.WaitAsync().ConfigureAwait(false); + try + { + var builder = new ModuleBuilder(this, null, primaryAlias); + buildFunc(builder); + + var module = builder.Build(this, null); + + return LoadModuleInternal(module); + } + finally + { + _moduleLock.Release(); + } + } + + /// + /// Add a command module from a . + /// + /// + /// The following example registers the module MyModule to commandService. + /// + /// await commandService.AddModuleAsync<MyModule>(serviceProvider); + /// + /// + /// The type of module. + /// The for your dependency injection solution if using one; otherwise, pass null. + /// This module has already been added. + /// + /// The fails to be built; an invalid type may have been provided. + /// + /// + /// A task that represents the asynchronous operation for adding the module. The task result contains the + /// built module. + /// + public Task AddModuleAsync(IServiceProvider services) => AddModuleAsync(typeof(T), services); + + /// + /// Adds a command module from a . + /// + /// The type of module. + /// The for your dependency injection solution if using one; otherwise, pass null . + /// This module has already been added. + /// + /// The fails to be built; an invalid type may have been provided. + /// + /// + /// A task that represents the asynchronous operation for adding the module. The task result contains the + /// built module. + /// + public async Task AddModuleAsync(Type type, IServiceProvider services) + { + services = services ?? EmptyServiceProvider.Instance; + + await _moduleLock.WaitAsync().ConfigureAwait(false); + try + { + var typeInfo = type.GetTypeInfo(); + + if (_typedModuleDefs.ContainsKey(type)) + throw new ArgumentException("This module has already been added."); + + var module = (await ModuleClassBuilder.BuildAsync(this, services, typeInfo).ConfigureAwait(false)).FirstOrDefault(); + + if (module.Value == default(ModuleInfo)) + throw new InvalidOperationException($"Could not build the module {type.FullName}, did you pass an invalid type?"); + + _typedModuleDefs[module.Key] = module.Value; + + return LoadModuleInternal(module.Value); + } + finally + { + _moduleLock.Release(); + } + } + /// + /// Add command modules from an . + /// + /// The containing command modules. + /// The for your dependency injection solution if using one; otherwise, pass null. + /// + /// A task that represents the asynchronous operation for adding the command modules. The task result + /// contains an enumerable collection of modules added. + /// + public async Task> AddModulesAsync(Assembly assembly, IServiceProvider services) + { + services = services ?? EmptyServiceProvider.Instance; + + await _moduleLock.WaitAsync().ConfigureAwait(false); + try + { + var types = await ModuleClassBuilder.SearchAsync(assembly, this).ConfigureAwait(false); + var moduleDefs = await ModuleClassBuilder.BuildAsync(types, this, services).ConfigureAwait(false); + + foreach (var info in moduleDefs) + { + _typedModuleDefs[info.Key] = info.Value; + LoadModuleInternal(info.Value); + } + + return moduleDefs.Select(x => x.Value).ToImmutableArray(); + } + finally + { + _moduleLock.Release(); + } + } + private ModuleInfo LoadModuleInternal(ModuleInfo module) + { + _moduleDefs.Add(module); + + foreach (var command in module.Commands) + _map.AddCommand(command); + + foreach (var submodule in module.Submodules) + LoadModuleInternal(submodule); + + return module; + } + /// + /// Removes the command module. + /// + /// The to be removed from the service. + /// + /// A task that represents the asynchronous removal operation. The task result contains a value that + /// indicates whether the is successfully removed. + /// + public async Task RemoveModuleAsync(ModuleInfo module) + { + await _moduleLock.WaitAsync().ConfigureAwait(false); + try + { + return RemoveModuleInternal(module); + } + finally + { + _moduleLock.Release(); + } + } + /// + /// Removes the command module. + /// + /// The of the module. + /// + /// A task that represents the asynchronous removal operation. The task result contains a value that + /// indicates whether the module is successfully removed. + /// + public Task RemoveModuleAsync() => RemoveModuleAsync(typeof(T)); + /// + /// Removes the command module. + /// + /// The of the module. + /// + /// A task that represents the asynchronous removal operation. The task result contains a value that + /// indicates whether the module is successfully removed. + /// + public async Task RemoveModuleAsync(Type type) + { + await _moduleLock.WaitAsync().ConfigureAwait(false); + try + { + if (!_typedModuleDefs.TryRemove(type, out var module)) + return false; + + return RemoveModuleInternal(module); + } + finally + { + _moduleLock.Release(); + } + } + private bool RemoveModuleInternal(ModuleInfo module) + { + if (!_moduleDefs.Remove(module)) + return false; + + foreach (var cmd in module.Commands) + _map.RemoveCommand(cmd); + + foreach (var submodule in module.Submodules) + { + RemoveModuleInternal(submodule); + } + + return true; + } + + //Type Readers + /// + /// Adds a custom to this for the supplied object + /// type. + /// If is a , a nullable will + /// also be added. + /// If a default exists for , a warning will be logged + /// and the default will be replaced. + /// + /// The object type to be read by the . + /// An instance of the to be added. + public void AddTypeReader(TypeReader reader) + => AddTypeReader(typeof(T), reader); + /// + /// Adds a custom to this for the supplied object + /// type. + /// If is a , a nullable for the + /// value type will also be added. + /// If a default exists for , a warning will be logged and + /// the default will be replaced. + /// + /// A instance for the type to be read. + /// An instance of the to be added. + public void AddTypeReader(Type type, TypeReader reader) + { + if (_defaultTypeReaders.ContainsKey(type)) + _ = _cmdLogger.WarningAsync($"The default TypeReader for {type.FullName} was replaced by {reader.GetType().FullName}." + + "To suppress this message, use AddTypeReader(reader, true)."); + AddTypeReader(type, reader, true); + } + /// + /// Adds a custom to this for the supplied object + /// type. + /// If is a , a nullable will + /// also be added. + /// + /// The object type to be read by the . + /// An instance of the to be added. + /// + /// Defines whether the should replace the default one for + /// if it exists. + /// + public void AddTypeReader(TypeReader reader, bool replaceDefault) + => AddTypeReader(typeof(T), reader, replaceDefault); + /// + /// Adds a custom to this for the supplied object + /// type. + /// If is a , a nullable for the + /// value type will also be added. + /// + /// A instance for the type to be read. + /// An instance of the to be added. + /// + /// Defines whether the should replace the default one for if + /// it exists. + /// + public void AddTypeReader(Type type, TypeReader reader, bool replaceDefault) + { + if (replaceDefault && HasDefaultTypeReader(type)) + { + _defaultTypeReaders.AddOrUpdate(type, reader, (k, v) => reader); + if (type.GetTypeInfo().IsValueType) + { + var nullableType = typeof(Nullable<>).MakeGenericType(type); + var nullableReader = NullableTypeReader.Create(type, reader); + _defaultTypeReaders.AddOrUpdate(nullableType, nullableReader, (k, v) => nullableReader); + } + } + else + { + var readers = _typeReaders.GetOrAdd(type, x => new ConcurrentDictionary()); + readers[reader.GetType()] = reader; + + if (type.GetTypeInfo().IsValueType) + AddNullableTypeReader(type, reader); + } + } + internal bool HasDefaultTypeReader(Type type) + { + if (_defaultTypeReaders.ContainsKey(type)) + return true; + + var typeInfo = type.GetTypeInfo(); + if (typeInfo.IsEnum) + return true; + return _entityTypeReaders.Any(x => type == x.EntityType || typeInfo.ImplementedInterfaces.Contains(x.TypeReaderType)); + } + internal void AddNullableTypeReader(Type valueType, TypeReader valueTypeReader) + { + var readers = _typeReaders.GetOrAdd(typeof(Nullable<>).MakeGenericType(valueType), x => new ConcurrentDictionary()); + var nullableReader = NullableTypeReader.Create(valueType, valueTypeReader); + readers[nullableReader.GetType()] = nullableReader; + } + internal IDictionary GetTypeReaders(Type type) + { + if (_typeReaders.TryGetValue(type, out var definedTypeReaders)) + return definedTypeReaders; + return null; + } + internal TypeReader GetDefaultTypeReader(Type type) + { + if (_defaultTypeReaders.TryGetValue(type, out var reader)) + return reader; + var typeInfo = type.GetTypeInfo(); + + //Is this an enum? + if (typeInfo.IsEnum) + { + reader = EnumTypeReader.GetReader(type); + _defaultTypeReaders[type] = reader; + return reader; + } + + //Is this an entity? + for (int i = 0; i < _entityTypeReaders.Count; i++) + { + if (type == _entityTypeReaders[i].EntityType || typeInfo.ImplementedInterfaces.Contains(_entityTypeReaders[i].EntityType)) + { + reader = Activator.CreateInstance(_entityTypeReaders[i].TypeReaderType.MakeGenericType(type)) as TypeReader; + _defaultTypeReaders[type] = reader; + return reader; + } + } + return null; + } + + //Execution + /// + /// Searches for the command. + /// + /// The context of the command. + /// The position of which the command starts at. + /// The result containing the matching commands. + public SearchResult Search(ICommandContext context, int argPos) + => Search(context.Message.Content.Substring(argPos)); + /// + /// Searches for the command. + /// + /// The context of the command. + /// The command string. + /// The result containing the matching commands. + public SearchResult Search(ICommandContext context, string input) + => Search(input); + public SearchResult Search(string input) + { + string searchInput = _caseSensitive ? input : input.ToLowerInvariant(); + var matches = _map.GetCommands(searchInput).OrderByDescending(x => x.Command.Priority).ToImmutableArray(); + + if (matches.Length > 0) + return SearchResult.FromSuccess(input, matches); + else + return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command."); + } + + /// + /// Executes the command. + /// + /// The context of the command. + /// The position of which the command starts at. + /// The service to be used in the command's dependency injection. + /// The handling mode when multiple command matches are found. + /// + /// A task that represents the asynchronous execution operation. The task result contains the result of the + /// command execution. + /// + public Task ExecuteAsync(ICommandContext context, int argPos, IServiceProvider services, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) + => ExecuteAsync(context, context.Message.Content.Substring(argPos), services, multiMatchHandling); + /// + /// Executes the command. + /// + /// The context of the command. + /// The command string. + /// The service to be used in the command's dependency injection. + /// The handling mode when multiple command matches are found. + /// + /// A task that represents the asynchronous execution operation. The task result contains the result of the + /// command execution. + /// + public async Task ExecuteAsync(ICommandContext context, string input, IServiceProvider services, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) + { + services = services ?? EmptyServiceProvider.Instance; + + var searchResult = Search(input); + if (!searchResult.IsSuccess) + { + await _commandExecutedEvent.InvokeAsync(Optional.Create(), context, searchResult).ConfigureAwait(false); + return searchResult; + } + + + var commands = searchResult.Commands; + var preconditionResults = new Dictionary(); + + foreach (var match in commands) + { + preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, services).ConfigureAwait(false); + } + + var successfulPreconditions = preconditionResults + .Where(x => x.Value.IsSuccess) + .ToArray(); + + if (successfulPreconditions.Length == 0) + { + //All preconditions failed, return the one from the highest priority command + var bestCandidate = preconditionResults + .OrderByDescending(x => x.Key.Command.Priority) + .FirstOrDefault(x => !x.Value.IsSuccess); + + await _commandExecutedEvent.InvokeAsync(bestCandidate.Key.Command, context, bestCandidate.Value).ConfigureAwait(false); + return bestCandidate.Value; + } + + //If we get this far, at least one precondition was successful. + + var parseResultsDict = new Dictionary(); + foreach (var pair in successfulPreconditions) + { + var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, services).ConfigureAwait(false); + + if (parseResult.Error == CommandError.MultipleMatches) + { + IReadOnlyList argList, paramList; + switch (multiMatchHandling) + { + case MultiMatchHandling.Best: + 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; + } + } + + parseResultsDict[pair.Key] = parseResult; + } + + // Calculates the 'score' of a command given a parse result + float CalculateScore(CommandMatch match, ParseResult parseResult) + { + float argValuesScore = 0, paramValuesScore = 0; + + if (match.Command.Parameters.Count > 0) + { + var argValuesSum = parseResult.ArgValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; + var paramValuesSum = parseResult.ParamValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; + + argValuesScore = argValuesSum / match.Command.Parameters.Count; + paramValuesScore = paramValuesSum / match.Command.Parameters.Count; + } + + var totalArgsScore = (argValuesScore + paramValuesScore) / 2; + return match.Command.Priority + totalArgsScore * 0.99f; + } + + //Order the parse results by their score so that we choose the most likely result to execute + var parseResults = parseResultsDict + .OrderByDescending(x => CalculateScore(x.Key, x.Value)); + + var successfulParses = parseResults + .Where(x => x.Value.IsSuccess) + .ToArray(); + + if (successfulParses.Length == 0) + { + //All parses failed, return the one from the highest priority command, using score as a tie breaker + var bestMatch = parseResults + .FirstOrDefault(x => !x.Value.IsSuccess); + + await _commandExecutedEvent.InvokeAsync(bestMatch.Key.Command, context, bestMatch.Value).ConfigureAwait(false); + return bestMatch.Value; + } + + //If we get this far, at least one parse was successful. Execute the most likely overload. + var chosenOverload = successfulParses[0]; + var result = await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services).ConfigureAwait(false); + if (!result.IsSuccess && !(result is RuntimeResult || result is ExecuteResult)) // succesful results raise the event in CommandInfo#ExecuteInternalAsync (have to raise it there b/c deffered execution) + await _commandExecutedEvent.InvokeAsync(chosenOverload.Key.Command, context, result); + return result; + } + + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + _moduleLock?.Dispose(); + } + + _isDisposed = true; + } + } + + void IDisposable.Dispose() + { + Dispose(true); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandServiceConfig.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandServiceConfig.cs new file mode 100644 index 0000000..2dedcea --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandServiceConfig.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; + +namespace Discord.Commands +{ + /// + /// Represents a configuration class for . + /// + public class CommandServiceConfig + { + /// + /// Gets or sets the default commands should have, if one is not specified on the + /// Command attribute or builder. + /// + public RunMode DefaultRunMode { get; set; } = RunMode.Sync; + + /// + /// Gets or sets the that separates an argument with another. + /// + public char SeparatorChar { get; set; } = ' '; + + /// + /// Gets or sets whether commands should be case-sensitive. + /// + public bool CaseSensitiveCommands { get; set; } = false; + + /// + /// Gets or sets the minimum log level severity that will be sent to the event. + /// + public LogSeverity LogLevel { get; set; } = LogSeverity.Info; + + /// + /// Gets or sets whether commands should push exceptions up to the caller. + /// + public bool ThrowOnError { get; set; } = true; + + /// + /// Collection of aliases for matching pairs of string delimiters. + /// The dictionary stores the opening delimiter as a key, and the matching closing delimiter as the value. + /// If no value is supplied will be used, which contains + /// many regional equivalents. + /// Only values that are specified in this map will be used as string delimiters, so if " is removed then + /// it won't be used. + /// If this map is set to null or empty, the default delimiter of " will be used. + /// + /// + /// + /// QuotationMarkAliasMap = new Dictionary<char, char%gt;() + /// { + /// {'\"', '\"' }, + /// {'“', '”' }, + /// {'「', '」' }, + /// } + /// + /// + public Dictionary QuotationMarkAliasMap { get; set; } = QuotationAliasUtils.GetDefaultAliasMap; + + /// + /// Gets or sets a value that indicates whether extra parameters should be ignored. + /// + public bool IgnoreExtraArgs { get; set; } = false; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/EmptyServiceProvider.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/EmptyServiceProvider.cs new file mode 100644 index 0000000..0bef376 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/EmptyServiceProvider.cs @@ -0,0 +1,11 @@ +using System; + +namespace Discord.Commands +{ + internal class EmptyServiceProvider : IServiceProvider + { + public static readonly EmptyServiceProvider Instance = new EmptyServiceProvider(); + + public object GetService(Type serviceType) => null; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Extensions/CommandServiceExtensions.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Extensions/CommandServiceExtensions.cs new file mode 100644 index 0000000..4c2262f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Extensions/CommandServiceExtensions.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + /// + /// Provides extension methods for the class. + /// + public static class CommandServiceExtensions + { + /// + /// Returns commands that can be executed under the current context. + /// + /// The set of commands to be checked against. + /// The current command context. + /// The service provider used for dependency injection upon precondition check. + /// + /// A read-only collection of commands that can be executed under the current context. + /// + public static async Task> GetExecutableCommandsAsync(this ICollection commands, ICommandContext context, IServiceProvider provider) + { + var executableCommands = new List(); + + var tasks = commands.Select(async c => + { + var result = await c.CheckPreconditionsAsync(context, provider).ConfigureAwait(false); + return new { Command = c, PreconditionResult = result }; + }); + + var results = await Task.WhenAll(tasks).ConfigureAwait(false); + + foreach (var result in results) + { + if (result.PreconditionResult.IsSuccess) + executableCommands.Add(result.Command); + } + + return executableCommands; + } + /// + /// Returns commands that can be executed under the current context. + /// + /// The desired command service class to check against. + /// The current command context. + /// The service provider used for dependency injection upon precondition check. + /// + /// A read-only collection of commands that can be executed under the current context. + /// + public static Task> GetExecutableCommandsAsync(this CommandService commandService, ICommandContext context, IServiceProvider provider) + => GetExecutableCommandsAsync(commandService.Commands.ToArray(), context, provider); + /// + /// Returns commands that can be executed under the current context. + /// + /// The module to be checked against. + /// The current command context. + /// The service provider used for dependency injection upon precondition check. + /// + /// A read-only collection of commands that can be executed under the current context. + /// + public static async Task> GetExecutableCommandsAsync(this ModuleInfo module, ICommandContext context, IServiceProvider provider) + { + var executableCommands = new List(); + + executableCommands.AddRange(await module.Commands.ToArray().GetExecutableCommandsAsync(context, provider).ConfigureAwait(false)); + + var tasks = module.Submodules.Select(async s => await s.GetExecutableCommandsAsync(context, provider).ConfigureAwait(false)); + var results = await Task.WhenAll(tasks).ConfigureAwait(false); + + executableCommands.AddRange(results.SelectMany(c => c)); + + return executableCommands; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Extensions/IEnumerableExtensions.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Extensions/IEnumerableExtensions.cs new file mode 100644 index 0000000..b922dd9 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Extensions/IEnumerableExtensions.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; + +namespace Discord.Commands +{ + public static class IEnumerableExtensions + { + public static IEnumerable Permutate( + this IEnumerable set, + IEnumerable others, + Func func) + { + foreach (TFirst elem in set) + { + foreach (TSecond elem2 in others) + { + yield return func(elem, elem2); + } + } + } + } +} \ No newline at end of file diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Extensions/MessageExtensions.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Extensions/MessageExtensions.cs new file mode 100644 index 0000000..f880e1d --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Extensions/MessageExtensions.cs @@ -0,0 +1,64 @@ +using System; + +namespace Discord.Commands +{ + /// + /// Provides extension methods for that relates to commands. + /// + public static class MessageExtensions + { + /// + /// Gets whether the message starts with the provided character. + /// + /// The message to check against. + /// The char prefix. + /// References where the command starts. + /// + /// true if the message begins with the char ; otherwise false. + /// + public static bool HasCharPrefix(this IUserMessage msg, char c, ref int argPos) + { + var text = msg.Content; + if (!string.IsNullOrEmpty(text) && text[0] == c) + { + argPos = 1; + return true; + } + return false; + } + /// + /// Gets whether the message starts with the provided string. + /// + public static bool HasStringPrefix(this IUserMessage msg, string str, ref int argPos, StringComparison comparisonType = StringComparison.Ordinal) + { + var text = msg.Content; + if (!string.IsNullOrEmpty(text) && text.StartsWith(str, comparisonType)) + { + argPos = str.Length; + return true; + } + return false; + } + /// + /// Gets whether the message starts with the user's mention string. + /// + public static bool HasMentionPrefix(this IUserMessage msg, IUser user, ref int argPos) + { + var text = msg.Content; + if (string.IsNullOrEmpty(text) || text.Length <= 3 || text[0] != '<' || text[1] != '@') return false; + + int endPos = text.IndexOf('>'); + if (endPos == -1) return false; + if (text.Length < endPos + 2 || text[endPos + 1] != ' ') return false; //Must end in "> " + + ulong userId; + if (!MentionUtils.TryParseUser(text.Substring(0, endPos + 1), out userId)) return false; + if (userId == user.Id) + { + argPos = endPos + 2; + return true; + } + return false; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/IModuleBase.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/IModuleBase.cs new file mode 100644 index 0000000..3b641ec --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/IModuleBase.cs @@ -0,0 +1,15 @@ +using Discord.Commands.Builders; + +namespace Discord.Commands +{ + internal interface IModuleBase + { + void SetContext(ICommandContext context); + + void BeforeExecute(CommandInfo command); + + void AfterExecute(CommandInfo command); + + void OnModuleBuilding(CommandService commandService, ModuleBuilder builder); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Info/CommandInfo.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Info/CommandInfo.cs new file mode 100644 index 0000000..3bcef98 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Info/CommandInfo.cs @@ -0,0 +1,339 @@ +using Discord.Commands.Builders; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + /// + /// Provides the information of a command. + /// + /// + /// This object contains the information of a command. This can include the module of the command, various + /// descriptions regarding the command, and its . + /// + [DebuggerDisplay("{Name,nq}")] + public class CommandInfo + { + private static readonly System.Reflection.MethodInfo _convertParamsMethod = typeof(CommandInfo).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList)); + private static readonly ConcurrentDictionary, object>> _arrayConverters = new ConcurrentDictionary, object>>(); + + private readonly CommandService _commandService; + private readonly Func _action; + + /// + /// Gets the module that the command belongs in. + /// + public ModuleInfo Module { get; } + /// + /// Gets the name of the command. If none is set, the first alias is used. + /// + public string Name { get; } + /// + /// Gets the summary of the command. + /// + /// + /// This field returns the summary of the command. and can be + /// useful in help commands and various implementation that fetches details of the command for the user. + /// + public string Summary { get; } + /// + /// Gets the remarks of the command. + /// + /// + /// This field returns the summary of the command. and can be + /// useful in help commands and various implementation that fetches details of the command for the user. + /// + public string Remarks { get; } + /// + /// Gets the priority of the command. This is used when there are multiple overloads of the command. + /// + public int Priority { get; } + /// + /// Indicates whether the command accepts a [] for its + /// parameter. + /// + public bool HasVarArgs { get; } + /// + /// Indicates whether extra arguments should be ignored for this command. + /// + public bool IgnoreExtraArgs { get; } + /// + /// Gets the that is being used for the command. + /// + public RunMode RunMode { get; } + + /// + /// Gets a list of aliases defined by the of the command. + /// + public IReadOnlyList Aliases { get; } + /// + /// Gets a list of information about the parameters of the command. + /// + public IReadOnlyList Parameters { get; } + /// + /// Gets a list of preconditions defined by the of the command. + /// + public IReadOnlyList Preconditions { get; } + /// + /// Gets a list of attributes of the command. + /// + public IReadOnlyList Attributes { get; } + + internal CommandInfo(CommandBuilder builder, ModuleInfo module, CommandService service) + { + Module = module; + + Name = builder.Name; + Summary = builder.Summary; + Remarks = builder.Remarks; + + RunMode = (builder.RunMode == RunMode.Default ? service._defaultRunMode : builder.RunMode); + Priority = builder.Priority; + + Aliases = module.Aliases + .Permutate(builder.Aliases, (first, second) => + { + if (first == "") + return second; + else if (second == "") + return first; + else + return first + service._separatorChar + second; + }) + .Select(x => service._caseSensitive ? x : x.ToLowerInvariant()) + .ToImmutableArray(); + + Preconditions = builder.Preconditions.ToImmutableArray(); + Attributes = builder.Attributes.ToImmutableArray(); + + Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray(); + HasVarArgs = builder.Parameters.Count > 0 && builder.Parameters[builder.Parameters.Count - 1].IsMultiple; + IgnoreExtraArgs = builder.IgnoreExtraArgs; + + _action = builder.Callback; + _commandService = service; + } + + public async Task CheckPreconditionsAsync(ICommandContext context, IServiceProvider services = null) + { + services = services ?? EmptyServiceProvider.Instance; + + async Task CheckGroups(IEnumerable preconditions, string type) + { + foreach (IGrouping preconditionGroup in preconditions.GroupBy(p => p.Group, StringComparer.Ordinal)) + { + if (preconditionGroup.Key == null) + { + foreach (PreconditionAttribute precondition in preconditionGroup) + { + var result = await precondition.CheckPermissionsAsync(context, this, services).ConfigureAwait(false); + if (!result.IsSuccess) + return result; + } + } + else + { + var results = new List(); + foreach (PreconditionAttribute precondition in preconditionGroup) + results.Add(await precondition.CheckPermissionsAsync(context, this, services).ConfigureAwait(false)); + + if (!results.Any(p => p.IsSuccess)) + return PreconditionGroupResult.FromError($"{type} precondition group {preconditionGroup.Key} failed.", results); + } + } + return PreconditionGroupResult.FromSuccess(); + } + + var moduleResult = await CheckGroups(Module.Preconditions, "Module").ConfigureAwait(false); + if (!moduleResult.IsSuccess) + return moduleResult; + + var commandResult = await CheckGroups(Preconditions, "Command").ConfigureAwait(false); + if (!commandResult.IsSuccess) + return commandResult; + + return PreconditionResult.FromSuccess(); + } + + public async Task ParseAsync(ICommandContext context, int startIndex, SearchResult searchResult, PreconditionResult preconditionResult = null, IServiceProvider services = null) + { + services = services ?? EmptyServiceProvider.Instance; + + if (!searchResult.IsSuccess) + return ParseResult.FromError(searchResult); + if (preconditionResult != null && !preconditionResult.IsSuccess) + return ParseResult.FromError(preconditionResult); + + string input = searchResult.Text.Substring(startIndex); + + return await CommandParser.ParseArgsAsync(this, context, _commandService._ignoreExtraArgs, services, input, 0, _commandService._quotationMarkAliasMap).ConfigureAwait(false); + } + + public Task ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) + { + if (!parseResult.IsSuccess) + return Task.FromResult((IResult)ExecuteResult.FromError(parseResult)); + + var argList = new object[parseResult.ArgValues.Count]; + for (int i = 0; i < parseResult.ArgValues.Count; i++) + { + if (!parseResult.ArgValues[i].IsSuccess) + return Task.FromResult((IResult)ExecuteResult.FromError(parseResult.ArgValues[i])); + argList[i] = parseResult.ArgValues[i].Values.First().Value; + } + + var paramList = new object[parseResult.ParamValues.Count]; + for (int i = 0; i < parseResult.ParamValues.Count; i++) + { + if (!parseResult.ParamValues[i].IsSuccess) + return Task.FromResult((IResult)ExecuteResult.FromError(parseResult.ParamValues[i])); + paramList[i] = parseResult.ParamValues[i].Values.First().Value; + } + + return ExecuteAsync(context, argList, paramList, services); + } + public async Task ExecuteAsync(ICommandContext context, IEnumerable argList, IEnumerable paramList, IServiceProvider services) + { + services = services ?? EmptyServiceProvider.Instance; + + try + { + object[] args = GenerateArgs(argList, paramList); + + for (int position = 0; position < Parameters.Count; position++) + { + var parameter = Parameters[position]; + object argument = args[position]; + var result = await parameter.CheckPreconditionsAsync(context, argument, services).ConfigureAwait(false); + if (!result.IsSuccess) + { + await Module.Service._commandExecutedEvent.InvokeAsync(this, context, result).ConfigureAwait(false); + return ExecuteResult.FromError(result); + } + } + + switch (RunMode) + { + case RunMode.Sync: //Always sync + return await ExecuteInternalAsync(context, args, services).ConfigureAwait(false); + case RunMode.Async: //Always async + var t2 = Task.Run(async () => + { + await ExecuteInternalAsync(context, args, services).ConfigureAwait(false); + }); + break; + } + return ExecuteResult.FromSuccess(); + } + catch (Exception ex) + { + return ExecuteResult.FromError(ex); + } + } + + private async Task ExecuteInternalAsync(ICommandContext context, object[] args, IServiceProvider services) + { + await Module.Service._cmdLogger.DebugAsync($"Executing {GetLogText(context)}").ConfigureAwait(false); + try + { + var task = _action(context, args, services, this); + if (task is Task resultTask) + { + var result = await resultTask.ConfigureAwait(false); + await Module.Service._commandExecutedEvent.InvokeAsync(this, context, result).ConfigureAwait(false); + if (result is RuntimeResult execResult) + return execResult; + } + else if (task is Task execTask) + { + var result = await execTask.ConfigureAwait(false); + await Module.Service._commandExecutedEvent.InvokeAsync(this, context, result).ConfigureAwait(false); + return result; + } + else + { + await task.ConfigureAwait(false); + var result = ExecuteResult.FromSuccess(); + await Module.Service._commandExecutedEvent.InvokeAsync(this, context, result).ConfigureAwait(false); + } + + var executeResult = ExecuteResult.FromSuccess(); + return executeResult; + } + catch (Exception ex) + { + var originalEx = ex; + while (ex is TargetInvocationException) //Happens with void-returning commands + ex = ex.InnerException; + + var wrappedEx = new CommandException(this, context, ex); + await Module.Service._cmdLogger.ErrorAsync(wrappedEx).ConfigureAwait(false); + + var result = ExecuteResult.FromError(ex); + await Module.Service._commandExecutedEvent.InvokeAsync(this, context, result).ConfigureAwait(false); + + if (Module.Service._throwOnError) + { + if (ex == originalEx) + throw; + else + ExceptionDispatchInfo.Capture(ex).Throw(); + } + + return result; + } + finally + { + await Module.Service._cmdLogger.VerboseAsync($"Executed {GetLogText(context)}").ConfigureAwait(false); + } + } + + private object[] GenerateArgs(IEnumerable argList, IEnumerable paramsList) + { + int argCount = Parameters.Count; + var array = new object[Parameters.Count]; + if (HasVarArgs) + argCount--; + + int i = 0; + foreach (object arg in argList) + { + if (i == argCount) + throw new InvalidOperationException("Command was invoked with too many parameters."); + array[i++] = arg; + } + if (i < argCount) + throw new InvalidOperationException("Command was invoked with too few parameters."); + + if (HasVarArgs) + { + var func = _arrayConverters.GetOrAdd(Parameters[Parameters.Count - 1].Type, t => + { + var method = _convertParamsMethod.MakeGenericMethod(t); + return (Func, object>)method.CreateDelegate(typeof(Func, object>)); + }); + array[i] = func(paramsList); + } + + return array; + } + + private static T[] ConvertParamsList(IEnumerable paramsList) + => paramsList.Cast().ToArray(); + + internal string GetLogText(ICommandContext context) + { + if (context.Guild != null) + return $"\"{Name}\" for {context.User} in {context.Guild}/{context.Channel}"; + else + return $"\"{Name}\" for {context.User} in {context.Channel}"; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Info/ModuleInfo.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Info/ModuleInfo.cs new file mode 100644 index 0000000..7b9959e --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Info/ModuleInfo.cs @@ -0,0 +1,147 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Collections.Immutable; +using Discord.Commands.Builders; + +namespace Discord.Commands +{ + /// + /// Provides the information of a module. + /// + public class ModuleInfo + { + /// + /// Gets the command service associated with this module. + /// + public CommandService Service { get; } + /// + /// Gets the name of this module. + /// + public string Name { get; } + /// + /// Gets the summary of this module. + /// + public string Summary { get; } + /// + /// Gets the remarks of this module. + /// + public string Remarks { get; } + /// + /// Gets the group name (main prefix) of this module. + /// + public string Group { get; } + + /// + /// Gets a read-only list of aliases associated with this module. + /// + public IReadOnlyList Aliases { get; } + /// + /// Gets a read-only list of commands associated with this module. + /// + public IReadOnlyList Commands { get; } + /// + /// Gets a read-only list of preconditions that apply to this module. + /// + public IReadOnlyList Preconditions { get; } + /// + /// Gets a read-only list of attributes that apply to this module. + /// + public IReadOnlyList Attributes { get; } + /// + /// Gets a read-only list of submodules associated with this module. + /// + public IReadOnlyList Submodules { get; } + /// + /// Gets the parent module of this submodule if applicable. + /// + public ModuleInfo Parent { get; } + /// + /// Gets a value that indicates whether this module is a submodule or not. + /// + public bool IsSubmodule => Parent != null; + + internal ModuleInfo(ModuleBuilder builder, CommandService service, IServiceProvider services, ModuleInfo parent = null) + { + Service = service; + + Name = builder.Name; + Summary = builder.Summary; + Remarks = builder.Remarks; + Group = builder.Group; + Parent = parent; + + Aliases = BuildAliases(builder, service).ToImmutableArray(); + Commands = builder.Commands.Select(x => x.Build(this, service)).ToImmutableArray(); + Preconditions = BuildPreconditions(builder).ToImmutableArray(); + Attributes = BuildAttributes(builder).ToImmutableArray(); + + Submodules = BuildSubmodules(builder, service, services).ToImmutableArray(); + } + + private static IEnumerable BuildAliases(ModuleBuilder builder, CommandService service) + { + var result = builder.Aliases.ToList(); + var builderQueue = new Queue(); + + var parent = builder; + while ((parent = parent.Parent) != null) + builderQueue.Enqueue(parent); + + while (builderQueue.Count > 0) + { + var level = builderQueue.Dequeue(); + // permute in reverse because we want to *prefix* our aliases + result = level.Aliases.Permutate(result, (first, second) => + { + if (first == "") + return second; + else if (second == "") + return first; + else + return first + service._separatorChar + second; + }).ToList(); + } + + return result; + } + + private List BuildSubmodules(ModuleBuilder parent, CommandService service, IServiceProvider services) + { + var result = new List(); + + foreach (var submodule in parent.Modules) + result.Add(submodule.Build(service, services, this)); + + return result; + } + + private static List BuildPreconditions(ModuleBuilder builder) + { + var result = new List(); + + ModuleBuilder parent = builder; + while (parent != null) + { + result.AddRange(parent.Preconditions); + parent = parent.Parent; + } + + return result; + } + + private static List BuildAttributes(ModuleBuilder builder) + { + var result = new List(); + + ModuleBuilder parent = builder; + while (parent != null) + { + result.AddRange(parent.Attributes); + parent = parent.Parent; + } + + return result; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Info/ParameterInfo.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Info/ParameterInfo.cs new file mode 100644 index 0000000..b435b30 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Info/ParameterInfo.cs @@ -0,0 +1,99 @@ +using Discord.Commands.Builders; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + /// + /// Provides the information of a parameter. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class ParameterInfo + { + private readonly TypeReader _reader; + + /// + /// Gets the command that associates with this parameter. + /// + public CommandInfo Command { get; } + /// + /// Gets the name of this parameter. + /// + public string Name { get; } + /// + /// Gets the summary of this parameter. + /// + public string Summary { get; } + /// + /// Gets a value that indicates whether this parameter is optional or not. + /// + public bool IsOptional { get; } + /// + /// Gets a value that indicates whether this parameter is a remainder parameter or not. + /// + public bool IsRemainder { get; } + public bool IsMultiple { get; } + /// + /// Gets the type of the parameter. + /// + public Type Type { get; } + /// + /// Gets the default value for this optional parameter if applicable. + /// + public object DefaultValue { get; } + + /// + /// Gets a read-only list of precondition that apply to this parameter. + /// + public IReadOnlyList Preconditions { get; } + /// + /// Gets a read-only list of attributes that apply to this parameter. + /// + public IReadOnlyList Attributes { get; } + + internal ParameterInfo(ParameterBuilder builder, CommandInfo command, CommandService service) + { + Command = command; + + Name = builder.Name; + Summary = builder.Summary; + IsOptional = builder.IsOptional; + IsRemainder = builder.IsRemainder; + IsMultiple = builder.IsMultiple; + + Type = builder.ParameterType; + DefaultValue = builder.DefaultValue; + + Preconditions = builder.Preconditions.ToImmutableArray(); + Attributes = builder.Attributes.ToImmutableArray(); + + _reader = builder.TypeReader; + } + + public async Task CheckPreconditionsAsync(ICommandContext context, object arg, IServiceProvider services = null) + { + services = services ?? EmptyServiceProvider.Instance; + + foreach (var precondition in Preconditions) + { + var result = await precondition.CheckPermissionsAsync(context, this, arg, services).ConfigureAwait(false); + if (!result.IsSuccess) + return result; + } + + return PreconditionResult.FromSuccess(); + } + + public async Task ParseAsync(ICommandContext context, string input, IServiceProvider services = null) + { + services = services ?? EmptyServiceProvider.Instance; + return await _reader.ReadAsync(context, input, services).ConfigureAwait(false); + } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name}{(IsOptional ? " (Optional)" : "")}{(IsRemainder ? " (Remainder)" : "")}"; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Map/CommandMap.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Map/CommandMap.cs new file mode 100644 index 0000000..141ec6f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Map/CommandMap.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; + +namespace Discord.Commands +{ + internal class CommandMap + { + private readonly CommandService _service; + private readonly CommandMapNode _root; + private static readonly string[] BlankAliases = { "" }; + + public CommandMap(CommandService service) + { + _service = service; + _root = new CommandMapNode(""); + } + + public void AddCommand(CommandInfo command) + { + foreach (string text in command.Aliases) + _root.AddCommand(_service, text, 0, command); + } + public void RemoveCommand(CommandInfo command) + { + foreach (string text in command.Aliases) + _root.RemoveCommand(_service, text, 0, command); + } + + public IEnumerable GetCommands(string text) + { + return _root.GetCommands(_service, text, 0, text != ""); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Map/CommandMapNode.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Map/CommandMapNode.cs new file mode 100644 index 0000000..16f469c --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Map/CommandMapNode.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Discord.Commands +{ + internal class CommandMapNode + { + private static readonly char[] WhitespaceChars = { ' ', '\r', '\n' }; + + private readonly ConcurrentDictionary _nodes; + private readonly string _name; + private readonly object _lockObj = new object(); + private ImmutableArray _commands; + + public bool IsEmpty => _commands.Length == 0 && _nodes.Count == 0; + + public CommandMapNode(string name) + { + _name = name; + _nodes = new ConcurrentDictionary(); + _commands = ImmutableArray.Create(); + } + + /// Cannot add commands to the root node. + public void AddCommand(CommandService service, string text, int index, CommandInfo command) + { + int nextSegment = NextSegment(text, index, service._separatorChar); + string name; + + lock (_lockObj) + { + if (text == "") + { + if (_name == "") + throw new InvalidOperationException("Cannot add commands to the root node."); + _commands = _commands.Add(command); + } + else + { + if (nextSegment == -1) + name = text.Substring(index); + else + name = text.Substring(index, nextSegment - index); + + string fullName = _name == "" ? name : _name + service._separatorChar + name; + var nextNode = _nodes.GetOrAdd(name, x => new CommandMapNode(fullName)); + nextNode.AddCommand(service, nextSegment == -1 ? "" : text, nextSegment + 1, command); + } + } + } + public void RemoveCommand(CommandService service, string text, int index, CommandInfo command) + { + int nextSegment = NextSegment(text, index, service._separatorChar); + + lock (_lockObj) + { + if (text == "") + _commands = _commands.Remove(command); + else + { + string name; + if (nextSegment == -1) + name = text.Substring(index); + else + name = text.Substring(index, nextSegment - index); + + if (_nodes.TryGetValue(name, out var nextNode)) + { + nextNode.RemoveCommand(service, nextSegment == -1 ? "" : text, nextSegment + 1, command); + if (nextNode.IsEmpty) + _nodes.TryRemove(name, out nextNode); + } + } + } + } + + public IEnumerable GetCommands(CommandService service, string text, int index, bool visitChildren = true) + { + var commands = _commands; + for (int i = 0; i < commands.Length; i++) + yield return new CommandMatch(_commands[i], _name); + + if (visitChildren) + { + string name; + CommandMapNode nextNode; + + //Search for next segment + int nextSegment = NextSegment(text, index, service._separatorChar); + if (nextSegment == -1) + name = text.Substring(index); + else + name = text.Substring(index, nextSegment - index); + if (_nodes.TryGetValue(name, out nextNode)) + { + foreach (var cmd in nextNode.GetCommands(service, nextSegment == -1 ? "" : text, nextSegment + 1, true)) + yield return cmd; + } + + //Check if this is the last command segment before args + nextSegment = NextSegment(text, index, WhitespaceChars, service._separatorChar); + if (nextSegment != -1) + { + name = text.Substring(index, nextSegment - index); + if (_nodes.TryGetValue(name, out nextNode)) + { + foreach (var cmd in nextNode.GetCommands(service, nextSegment == -1 ? "" : text, nextSegment + 1, false)) + yield return cmd; + } + } + } + } + + private static int NextSegment(string text, int startIndex, char separator) + { + return text.IndexOf(separator, startIndex); + } + private static int NextSegment(string text, int startIndex, char[] separators, char except) + { + int lowest = int.MaxValue; + for (int i = 0; i < separators.Length; i++) + { + if (separators[i] != except) + { + int index = text.IndexOf(separators[i], startIndex); + if (index != -1 && index < lowest) + lowest = index; + } + } + return (lowest != int.MaxValue) ? lowest : -1; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/ModuleBase.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/ModuleBase.cs new file mode 100644 index 0000000..9cd4ea1 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/ModuleBase.cs @@ -0,0 +1,72 @@ +using System; +using System.Threading.Tasks; +using Discord.Commands.Builders; + +namespace Discord.Commands +{ + /// + /// Provides a base class for a command module to inherit from. + /// + public abstract class ModuleBase : ModuleBase { } + + /// + /// Provides a base class for a command module to inherit from. + /// + /// A class that implements . + public abstract class ModuleBase : IModuleBase + where T : class, ICommandContext + { + /// + /// The underlying context of the command. + /// + /// + /// + public T Context { get; private set; } + + /// + /// Sends a message to the source channel. + /// + /// + /// Contents of the message; optional only if is specified. + /// + /// Specifies if Discord should read this aloud using text-to-speech. + /// An embed to be displayed alongside the . + protected virtual async Task ReplyAsync(string message = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) + { + return await Context.Channel.SendMessageAsync(message, isTTS, embed, options).ConfigureAwait(false); + } + /// + /// The method to execute before executing the command. + /// + /// The of the command to be executed. + protected virtual void BeforeExecute(CommandInfo command) + { + } + /// + /// The method to execute after executing the command. + /// + /// The of the command to be executed. + protected virtual void AfterExecute(CommandInfo command) + { + } + + /// + /// The method to execute when building the module. + /// + /// The used to create the module. + /// The builder used to build the module. + protected virtual void OnModuleBuilding(CommandService commandService, ModuleBuilder builder) + { + } + + //IModuleBase + void IModuleBase.SetContext(ICommandContext context) + { + var newValue = context as T; + Context = newValue ?? throw new InvalidOperationException($"Invalid context type. Expected {typeof(T).Name}, got {context.GetType().Name}."); + } + void IModuleBase.BeforeExecute(CommandInfo command) => BeforeExecute(command); + void IModuleBase.AfterExecute(CommandInfo command) => AfterExecute(command); + void IModuleBase.OnModuleBuilding(CommandService commandService, ModuleBuilder builder) => OnModuleBuilding(commandService, builder); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/MultiMatchHandling.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/MultiMatchHandling.cs new file mode 100644 index 0000000..319e58e --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/MultiMatchHandling.cs @@ -0,0 +1,13 @@ +namespace Discord.Commands +{ + /// + /// Specifies the behavior when multiple matches are found during the command parsing stage. + /// + public enum MultiMatchHandling + { + /// Indicates that when multiple results are found, an exception should be thrown. + Exception, + /// Indicates that when multiple results are found, the best result should be chosen. + Best + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/PrimitiveParsers.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/PrimitiveParsers.cs new file mode 100644 index 0000000..e9b6aac --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/PrimitiveParsers.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Discord.Commands +{ + internal delegate bool TryParseDelegate(string str, out T value); + + internal static class PrimitiveParsers + { + private static readonly Lazy> Parsers = new Lazy>(CreateParsers); + + public static IEnumerable SupportedTypes = Parsers.Value.Keys; + + static IReadOnlyDictionary CreateParsers() + { + var parserBuilder = ImmutableDictionary.CreateBuilder(); + parserBuilder[typeof(bool)] = (TryParseDelegate)bool.TryParse; + parserBuilder[typeof(sbyte)] = (TryParseDelegate)sbyte.TryParse; + parserBuilder[typeof(byte)] = (TryParseDelegate)byte.TryParse; + parserBuilder[typeof(short)] = (TryParseDelegate)short.TryParse; + parserBuilder[typeof(ushort)] = (TryParseDelegate)ushort.TryParse; + parserBuilder[typeof(int)] = (TryParseDelegate)int.TryParse; + parserBuilder[typeof(uint)] = (TryParseDelegate)uint.TryParse; + parserBuilder[typeof(long)] = (TryParseDelegate)long.TryParse; + parserBuilder[typeof(ulong)] = (TryParseDelegate)ulong.TryParse; + parserBuilder[typeof(float)] = (TryParseDelegate)float.TryParse; + parserBuilder[typeof(double)] = (TryParseDelegate)double.TryParse; + parserBuilder[typeof(decimal)] = (TryParseDelegate)decimal.TryParse; + parserBuilder[typeof(DateTime)] = (TryParseDelegate)DateTime.TryParse; + parserBuilder[typeof(DateTimeOffset)] = (TryParseDelegate)DateTimeOffset.TryParse; + //parserBuilder[typeof(TimeSpan)] = (TryParseDelegate)TimeSpan.TryParse; + parserBuilder[typeof(char)] = (TryParseDelegate)char.TryParse; + return parserBuilder.ToImmutable(); + } + + public static TryParseDelegate Get() => (TryParseDelegate)Parsers.Value[typeof(T)]; + public static Delegate Get(Type type) => Parsers.Value[type]; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Readers/ChannelTypeReader.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Readers/ChannelTypeReader.cs new file mode 100644 index 0000000..cdbc59c --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Readers/ChannelTypeReader.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + /// + /// A for parsing objects implementing . + /// + /// + /// This is shipped with Discord.Net and is used by default to parse any + /// implemented object within a command. The TypeReader will attempt to first parse the + /// input by mention, then the snowflake identifier, then by name; the highest candidate will be chosen as the + /// final output; otherwise, an erroneous is returned. + /// + /// The type to be checked; must implement . + public class ChannelTypeReader : TypeReader + where T : class, IChannel + { + /// + public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) + { + if (context.Guild != null) + { + var results = new Dictionary(); + var channels = await context.Guild.GetChannelsAsync(CacheMode.CacheOnly).ConfigureAwait(false); + + //By Mention (1.0) + if (MentionUtils.TryParseChannel(input, out ulong id)) + 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 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))) + AddResult(results, channel as T, channel.Name == input ? 0.80f : 0.70f); + + if (results.Count > 0) + return TypeReaderResult.FromSuccess(results.Values.ToReadOnlyCollection()); + } + + return TypeReaderResult.FromError(CommandError.ObjectNotFound, "Channel not found."); + } + + private void AddResult(Dictionary results, T channel, float score) + { + if (channel != null && !results.ContainsKey(channel.Id)) + results.Add(channel.Id, new TypeReaderValue(channel, score)); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Readers/EnumTypeReader.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Readers/EnumTypeReader.cs new file mode 100644 index 0000000..356d704 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Readers/EnumTypeReader.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + internal static class EnumTypeReader + { + public static TypeReader GetReader(Type type) + { + Type baseType = Enum.GetUnderlyingType(type); + var constructor = typeof(EnumTypeReader<>).MakeGenericType(baseType).GetTypeInfo().DeclaredConstructors.First(); + return (TypeReader)constructor.Invoke(new object[] { type, PrimitiveParsers.Get(baseType) }); + } + } + + internal class EnumTypeReader : TypeReader + { + private readonly IReadOnlyDictionary _enumsByName; + private readonly IReadOnlyDictionary _enumsByValue; + private readonly Type _enumType; + private readonly TryParseDelegate _tryParse; + + public EnumTypeReader(Type type, TryParseDelegate parser) + { + _enumType = type; + _tryParse = parser; + + var byNameBuilder = ImmutableDictionary.CreateBuilder(); + var byValueBuilder = ImmutableDictionary.CreateBuilder(); + + foreach (var v in Enum.GetNames(_enumType)) + { + var parsedValue = Enum.Parse(_enumType, v); + byNameBuilder.Add(v.ToLower(), parsedValue); + if (!byValueBuilder.ContainsKey((T)parsedValue)) + byValueBuilder.Add((T)parsedValue, parsedValue); + } + + _enumsByName = byNameBuilder.ToImmutable(); + _enumsByValue = byValueBuilder.ToImmutable(); + } + + /// + public override Task ReadAsync(ICommandContext context, string input, IServiceProvider services) + { + object enumValue; + + if (_tryParse(input, out T baseValue)) + { + if (_enumsByValue.TryGetValue(baseValue, out enumValue)) + return Task.FromResult(TypeReaderResult.FromSuccess(enumValue)); + else + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, $"Value is not a {_enumType.Name}.")); + } + else + { + if (_enumsByName.TryGetValue(input.ToLower(), out enumValue)) + return Task.FromResult(TypeReaderResult.FromSuccess(enumValue)); + else + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, $"Value is not a {_enumType.Name}.")); + } + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Readers/MessageTypeReader.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Readers/MessageTypeReader.cs new file mode 100644 index 0000000..acec2f1 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Readers/MessageTypeReader.cs @@ -0,0 +1,27 @@ +using System; +using System.Globalization; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + /// + /// A for parsing objects implementing . + /// + /// The type to be checked; must implement . + public class MessageTypeReader : TypeReader + where T : class, IMessage + { + /// + public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) + { + //By Id (1.0) + if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out ulong id)) + { + if (await context.Channel.GetMessageAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) is T msg) + return TypeReaderResult.FromSuccess(msg); + } + + return TypeReaderResult.FromError(CommandError.ObjectNotFound, "Message not found."); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Readers/NamedArgumentTypeReader.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Readers/NamedArgumentTypeReader.cs new file mode 100644 index 0000000..0adf610 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Readers/NamedArgumentTypeReader.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + internal sealed class NamedArgumentTypeReader : TypeReader + where T : class, new() + { + private static readonly IReadOnlyDictionary _tProps = typeof(T).GetTypeInfo().DeclaredProperties + .Where(p => p.SetMethod != null && p.SetMethod.IsPublic && !p.SetMethod.IsStatic) + .ToImmutableDictionary(p => p.Name, StringComparer.OrdinalIgnoreCase); + + private readonly CommandService _commands; + + public NamedArgumentTypeReader(CommandService commands) + { + _commands = commands; + } + + public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) + { + var result = new T(); + var state = ReadState.LookingForParameter; + int beginRead = 0, currentRead = 0; + + while (state != ReadState.End) + { + try + { + var prop = Read(out var arg); + var propVal = await ReadArgumentAsync(prop, arg).ConfigureAwait(false); + if (propVal != null) + prop.SetMethod.Invoke(result, new[] { propVal }); + else + return TypeReaderResult.FromError(CommandError.ParseFailed, $"Could not parse the argument for the parameter '{prop.Name}' as type '{prop.PropertyType}'."); + } + catch (Exception ex) + { + return TypeReaderResult.FromError(ex); + } + } + + return TypeReaderResult.FromSuccess(result); + + PropertyInfo Read(out string arg) + { + string currentParam = null; + char match = '\0'; + + for (; currentRead < input.Length; currentRead++) + { + var currentChar = input[currentRead]; + switch (state) + { + case ReadState.LookingForParameter: + if (Char.IsWhiteSpace(currentChar)) + continue; + else + { + beginRead = currentRead; + state = ReadState.InParameter; + } + break; + case ReadState.InParameter: + if (currentChar != ':') + continue; + else + { + currentParam = input.Substring(beginRead, currentRead - beginRead); + state = ReadState.LookingForArgument; + } + break; + case ReadState.LookingForArgument: + if (Char.IsWhiteSpace(currentChar)) + continue; + else + { + beginRead = currentRead; + state = (QuotationAliasUtils.GetDefaultAliasMap.TryGetValue(currentChar, out match)) + ? ReadState.InQuotedArgument + : ReadState.InArgument; + } + break; + case ReadState.InArgument: + if (!Char.IsWhiteSpace(currentChar)) + continue; + else + return GetPropAndValue(out arg); + case ReadState.InQuotedArgument: + if (currentChar != match) + continue; + else + return GetPropAndValue(out arg); + } + } + + if (currentParam == null) + throw new InvalidOperationException("No parameter name was read."); + + return GetPropAndValue(out arg); + + PropertyInfo GetPropAndValue(out string argv) + { + bool quoted = state == ReadState.InQuotedArgument; + state = (currentRead == (quoted ? input.Length - 1 : input.Length)) + ? ReadState.End + : ReadState.LookingForParameter; + + if (quoted) + { + argv = input.Substring(beginRead + 1, currentRead - beginRead - 1).Trim(); + currentRead++; + } + else + argv = input.Substring(beginRead, currentRead - beginRead); + + return _tProps[currentParam]; + } + } + + async Task ReadArgumentAsync(PropertyInfo prop, string arg) + { + var elemType = prop.PropertyType; + bool isCollection = false; + if (elemType.GetTypeInfo().IsGenericType && elemType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + { + elemType = prop.PropertyType.GenericTypeArguments[0]; + isCollection = true; + } + + var overridden = prop.GetCustomAttribute(); + var reader = (overridden != null) + ? ModuleClassBuilder.GetTypeReader(_commands, elemType, overridden.TypeReader, services) + : (_commands.GetDefaultTypeReader(elemType) + ?? _commands.GetTypeReaders(elemType).FirstOrDefault().Value); + + if (reader != null) + { + if (isCollection) + { + var method = _readMultipleMethod.MakeGenericMethod(elemType); + var task = (Task)method.Invoke(null, new object[] { reader, context, arg.Split(','), services }); + return await task.ConfigureAwait(false); + } + else + return await ReadSingle(reader, context, arg, services).ConfigureAwait(false); + } + return null; + } + } + + private static async Task ReadSingle(TypeReader reader, ICommandContext context, string arg, IServiceProvider services) + { + var readResult = await reader.ReadAsync(context, arg, services).ConfigureAwait(false); + return (readResult.IsSuccess) + ? readResult.BestMatch + : null; + } + private static async Task ReadMultiple(TypeReader reader, ICommandContext context, IEnumerable args, IServiceProvider services) + { + var objs = new List(); + foreach (var arg in args) + { + var read = await ReadSingle(reader, context, arg.Trim(), services).ConfigureAwait(false); + if (read != null) + objs.Add((TObj)read); + } + return objs.ToImmutableArray(); + } + private static readonly MethodInfo _readMultipleMethod = typeof(NamedArgumentTypeReader) + .GetTypeInfo() + .DeclaredMethods + .Single(m => m.IsPrivate && m.IsStatic && m.Name == nameof(ReadMultiple)); + + private enum ReadState + { + LookingForParameter, + InParameter, + LookingForArgument, + InArgument, + InQuotedArgument, + End + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Readers/NullableTypeReader.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Readers/NullableTypeReader.cs new file mode 100644 index 0000000..f68bf6e --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Readers/NullableTypeReader.cs @@ -0,0 +1,35 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + internal static class NullableTypeReader + { + public static TypeReader Create(Type type, TypeReader reader) + { + var constructor = typeof(NullableTypeReader<>).MakeGenericType(type).GetTypeInfo().DeclaredConstructors.First(); + return (TypeReader)constructor.Invoke(new object[] { reader }); + } + } + + internal class NullableTypeReader : TypeReader + where T : struct + { + private readonly TypeReader _baseTypeReader; + + public NullableTypeReader(TypeReader baseTypeReader) + { + _baseTypeReader = baseTypeReader; + } + + /// + public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) + { + if (string.Equals(input, "null", StringComparison.OrdinalIgnoreCase) || string.Equals(input, "nothing", StringComparison.OrdinalIgnoreCase)) + return TypeReaderResult.FromSuccess(new T?()); + return await _baseTypeReader.ReadAsync(context, input, services).ConfigureAwait(false); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs new file mode 100644 index 0000000..cb74139 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + internal static class PrimitiveTypeReader + { + public static TypeReader Create(Type type) + { + type = typeof(PrimitiveTypeReader<>).MakeGenericType(type); + return Activator.CreateInstance(type) as TypeReader; + } + } + + internal class PrimitiveTypeReader : TypeReader + { + private readonly TryParseDelegate _tryParse; + private readonly float _score; + + /// must be within the range [0, 1]. + public PrimitiveTypeReader() + : this(PrimitiveParsers.Get(), 1) + { } + + /// must be within the range [0, 1]. + public PrimitiveTypeReader(TryParseDelegate tryParse, float score) + { + if (score < 0 || score > 1) + throw new ArgumentOutOfRangeException(nameof(score), score, "Scores must be within the range [0, 1]."); + + _tryParse = tryParse; + _score = score; + } + + public override Task ReadAsync(ICommandContext context, string input, IServiceProvider services) + { + if (_tryParse(input, out T value)) + return Task.FromResult(TypeReaderResult.FromSuccess(new TypeReaderValue(value, _score))); + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, $"Failed to parse {typeof(T).Name}.")); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Readers/RoleTypeReader.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Readers/RoleTypeReader.cs new file mode 100644 index 0000000..4c9aaf4 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Readers/RoleTypeReader.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + /// + /// A for parsing objects implementing . + /// + /// The type to be checked; must implement . + public class RoleTypeReader : TypeReader + where T : class, IRole + { + /// + public override Task ReadAsync(ICommandContext context, string input, IServiceProvider services) + { + if (context.Guild != null) + { + var results = new Dictionary(); + var roles = context.Guild.Roles; + + //By Mention (1.0) + if (MentionUtils.TryParseRole(input, out var id)) + 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, 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))) + AddResult(results, role as T, role.Name == input ? 0.80f : 0.70f); + + if (results.Count > 0) + return Task.FromResult(TypeReaderResult.FromSuccess(results.Values.ToReadOnlyCollection())); + } + return Task.FromResult(TypeReaderResult.FromError(CommandError.ObjectNotFound, "Role not found.")); + } + + private void AddResult(Dictionary results, T role, float score) + { + if (role != null && !results.ContainsKey(role.Id)) + results.Add(role.Id, new TypeReaderValue(role, score)); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Readers/TimeSpanTypeReader.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Readers/TimeSpanTypeReader.cs new file mode 100644 index 0000000..b4a27cb --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Readers/TimeSpanTypeReader.cs @@ -0,0 +1,35 @@ +using System; +using System.Globalization; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + internal class TimeSpanTypeReader : TypeReader + { + private static readonly string[] Formats = { + "%d'd'%h'h'%m'm'%s's'", //4d3h2m1s + "%d'd'%h'h'%m'm'", //4d3h2m + "%d'd'%h'h'%s's'", //4d3h 1s + "%d'd'%h'h'", //4d3h + "%d'd'%m'm'%s's'", //4d 2m1s + "%d'd'%m'm'", //4d 2m + "%d'd'%s's'", //4d 1s + "%d'd'", //4d + "%h'h'%m'm'%s's'", // 3h2m1s + "%h'h'%m'm'", // 3h2m + "%h'h'%s's'", // 3h 1s + "%h'h'", // 3h + "%m'm'%s's'", // 2m1s + "%m'm'", // 2m + "%s's'", // 1s + }; + + /// + public override Task ReadAsync(ICommandContext context, string input, IServiceProvider services) + { + return (TimeSpan.TryParseExact(input.ToLowerInvariant(), Formats, CultureInfo.InvariantCulture, out var timeSpan)) + ? Task.FromResult(TypeReaderResult.FromSuccess(timeSpan)) + : Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse TimeSpan")); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Readers/TypeReader.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Readers/TypeReader.cs new file mode 100644 index 0000000..af78099 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Readers/TypeReader.cs @@ -0,0 +1,22 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + /// + /// Defines a reader class that parses user input into a specified type. + /// + public abstract class TypeReader + { + /// + /// Attempts to parse the into the desired type. + /// + /// The context of the command. + /// The raw input of the command. + /// The service collection used for dependency injection. + /// + /// A task that represents the asynchronous parsing operation. The task result contains the parsing result. + /// + public abstract Task ReadAsync(ICommandContext context, string input, IServiceProvider services); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Readers/UserTypeReader.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Readers/UserTypeReader.cs new file mode 100644 index 0000000..c0104e3 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Readers/UserTypeReader.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + /// + /// A for parsing objects implementing . + /// + /// The type to be checked; must implement . + public class UserTypeReader : TypeReader + where T : class, IUser + { + /// + public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) + { + var results = new Dictionary(); + IAsyncEnumerable channelUsers = context.Channel.GetUsersAsync(CacheMode.CacheOnly).Flatten(); // it's better + IReadOnlyCollection guildUsers = ImmutableArray.Create(); + + if (context.Guild != null) + guildUsers = await context.Guild.GetUsersAsync(CacheMode.CacheOnly).ConfigureAwait(false); + + //By Mention (1.0) + if (MentionUtils.TryParseUser(input, out var id)) + { + 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, CacheMode.CacheOnly).ConfigureAwait(false) as T, 1.00f); + } + + //By Id (0.9) + if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id)) + { + 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, CacheMode.CacheOnly).ConfigureAwait(false) as T, 0.90f); + } + + //By Username + Discriminator (0.7-0.85) + int index = input.LastIndexOf('#'); + if (index >= 0) + { + string username = input.Substring(0, index); + if (ushort.TryParse(input.Substring(index + 1), out ushort discriminator)) + { + var channelUser = await channelUsers.FirstOrDefaultAsync(x => x.DiscriminatorValue == discriminator && + string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)).ConfigureAwait(false); + AddResult(results, channelUser as T, channelUser?.Username == username ? 0.85f : 0.75f); + + var guildUser = guildUsers.FirstOrDefault(x => x.DiscriminatorValue == discriminator && + string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)); + AddResult(results, guildUser as T, guildUser?.Username == username ? 0.80f : 0.70f); + } + } + + //By Username (0.5-0.6) + { + await channelUsers + .Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase)) + .ForEachAsync(channelUser => AddResult(results, channelUser as T, channelUser.Username == input ? 0.65f : 0.55f)) + .ConfigureAwait(false); + + foreach (var guildUser in guildUsers.Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase))) + AddResult(results, guildUser as T, guildUser.Username == input ? 0.60f : 0.50f); + } + + //By Nickname (0.5-0.6) + { + await channelUsers + .Where(x => string.Equals(input, (x as IGuildUser)?.Nickname, StringComparison.OrdinalIgnoreCase)) + .ForEachAsync(channelUser => AddResult(results, channelUser as T, (channelUser as IGuildUser).Nickname == input ? 0.65f : 0.55f)) + .ConfigureAwait(false); + + foreach (var guildUser in guildUsers.Where(x => string.Equals(input, x.Nickname, StringComparison.OrdinalIgnoreCase))) + AddResult(results, guildUser as T, guildUser.Nickname == input ? 0.60f : 0.50f); + } + + if (results.Count > 0) + return TypeReaderResult.FromSuccess(results.Values.ToImmutableArray()); + return TypeReaderResult.FromError(CommandError.ObjectNotFound, "User not found."); + } + + private void AddResult(Dictionary results, T user, float score) + { + if (user != null && !results.ContainsKey(user.Id)) + results.Add(user.Id, new TypeReaderValue(user, score)); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Results/ExecuteResult.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Results/ExecuteResult.cs new file mode 100644 index 0000000..0559995 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Results/ExecuteResult.cs @@ -0,0 +1,85 @@ +using System; +using System.Diagnostics; + +namespace Discord.Commands +{ + /// + /// Contains information of the command's overall execution result. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public struct ExecuteResult : IResult + { + /// + /// Gets the exception that may have occurred during the command execution. + /// + public Exception Exception { get; } + + /// + public CommandError? Error { get; } + /// + public string ErrorReason { get; } + + /// + public bool IsSuccess => !Error.HasValue; + + private ExecuteResult(Exception exception, CommandError? error, string errorReason) + { + Exception = exception; + Error = error; + ErrorReason = errorReason; + } + + /// + /// Initializes a new with no error, indicating a successful execution. + /// + /// + /// A that does not contain any errors. + /// + public static ExecuteResult FromSuccess() + => new ExecuteResult(null, null, null); + /// + /// Initializes a new with a specified and its + /// reason, indicating an unsuccessful execution. + /// + /// The type of error. + /// The reason behind the error. + /// + /// A that contains a and reason. + /// + public static ExecuteResult FromError(CommandError error, string reason) + => new ExecuteResult(null, error, reason); + /// + /// Initializes a new with a specified exception, indicating an unsuccessful + /// execution. + /// + /// The exception that caused the command execution to fail. + /// + /// A that contains the exception that caused the unsuccessful execution, along + /// with a of type Exception as well as the exception message as the + /// reason. + /// + public static ExecuteResult FromError(Exception ex) + => new ExecuteResult(ex, CommandError.Exception, ex.Message); + /// + /// Initializes a new with a specified result; this may or may not be an + /// successful execution depending on the and + /// specified. + /// + /// The result to inherit from. + /// + /// A that inherits the error type and reason. + /// + public static ExecuteResult FromError(IResult result) + => new ExecuteResult(null, result.Error, result.ErrorReason); + + /// + /// Gets a string that indicates the execution result. + /// + /// + /// Success if is true; otherwise ": + /// ". + /// + public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; + private string DebuggerDisplay => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Results/IResult.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Results/IResult.cs new file mode 100644 index 0000000..c11b580 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Results/IResult.cs @@ -0,0 +1,31 @@ +namespace Discord.Commands +{ + /// + /// Contains information of the result related to a command. + /// + public interface IResult + { + /// + /// Describes the error type that may have occurred during the operation. + /// + /// + /// A indicating the type of error that may have occurred during the operation; + /// null if the operation was successful. + /// + CommandError? Error { get; } + /// + /// Describes the reason for the error. + /// + /// + /// A string containing the error reason. + /// + string ErrorReason { get; } + /// + /// Indicates whether the operation was successful or not. + /// + /// + /// true if the result is positive; otherwise false. + /// + bool IsSuccess { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Results/ParseResult.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Results/ParseResult.cs new file mode 100644 index 0000000..43351b6 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Results/ParseResult.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Discord.Commands +{ + /// + /// Contains information for the parsing result from the command service's parser. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public struct ParseResult : IResult + { + public IReadOnlyList ArgValues { get; } + public IReadOnlyList ParamValues { get; } + + /// + public CommandError? Error { get; } + /// + public string ErrorReason { get; } + + /// + /// Provides information about the parameter that caused the parsing error. + /// + /// + /// A indicating the parameter info of the error that may have occurred during parsing; + /// null if the parsing was successful or the parsing error is not specific to a single parameter. + /// + public ParameterInfo ErrorParameter { get; } + + /// + public bool IsSuccess => !Error.HasValue; + + private ParseResult(IReadOnlyList argValues, IReadOnlyList paramValues, CommandError? error, string errorReason, ParameterInfo errorParamInfo) + { + ArgValues = argValues; + ParamValues = paramValues; + Error = error; + ErrorReason = errorReason; + ErrorParameter = errorParamInfo; + } + + public static ParseResult FromSuccess(IReadOnlyList argValues, IReadOnlyList paramValues) + { + for (int i = 0; i < argValues.Count; i++) + { + if (argValues[i].Values.Count > 1) + return new ParseResult(argValues, paramValues, CommandError.MultipleMatches, "Multiple matches found.", null); + } + for (int i = 0; i < paramValues.Count; i++) + { + if (paramValues[i].Values.Count > 1) + return new ParseResult(argValues, paramValues, CommandError.MultipleMatches, "Multiple matches found.", null); + } + return new ParseResult(argValues, paramValues, null, null, null); + } + public static ParseResult FromSuccess(IReadOnlyList argValues, IReadOnlyList paramValues) + { + var argList = new TypeReaderResult[argValues.Count]; + for (int i = 0; i < argValues.Count; i++) + argList[i] = TypeReaderResult.FromSuccess(argValues[i]); + TypeReaderResult[] paramList = null; + if (paramValues != null) + { + paramList = new TypeReaderResult[paramValues.Count]; + for (int i = 0; i < paramValues.Count; i++) + paramList[i] = TypeReaderResult.FromSuccess(paramValues[i]); + } + return new ParseResult(argList, paramList, null, null, null); + } + + public static ParseResult FromError(CommandError error, string reason) + => new ParseResult(null, null, error, reason, null); + public static ParseResult FromError(CommandError error, string reason, ParameterInfo parameterInfo) + => new ParseResult(null, null, error, reason, parameterInfo); + public static ParseResult FromError(Exception ex) + => FromError(CommandError.Exception, ex.Message); + public static ParseResult FromError(IResult result) + => new ParseResult(null, null, result.Error, result.ErrorReason, null); + public static ParseResult FromError(IResult result, ParameterInfo parameterInfo) + => new ParseResult(null, null, result.Error, result.ErrorReason, parameterInfo); + + public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; + private string DebuggerDisplay => IsSuccess ? $"Success ({ArgValues.Count}{(ParamValues.Count > 0 ? $" +{ParamValues.Count} Values" : "")})" : $"{Error}: {ErrorReason}"; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Results/PreconditionGroupResult.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Results/PreconditionGroupResult.cs new file mode 100644 index 0000000..7ecade4 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Results/PreconditionGroupResult.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Discord.Commands +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class PreconditionGroupResult : PreconditionResult + { + public IReadOnlyCollection PreconditionResults { get; } + + protected PreconditionGroupResult(CommandError? error, string errorReason, ICollection preconditions) + : base(error, errorReason) + { + PreconditionResults = (preconditions ?? new List(0)).ToReadOnlyCollection(); + } + + public new static PreconditionGroupResult FromSuccess() + => new PreconditionGroupResult(null, null, null); + public static PreconditionGroupResult FromError(string reason, ICollection preconditions) + => new PreconditionGroupResult(CommandError.UnmetPrecondition, reason, preconditions); + public static new PreconditionGroupResult FromError(Exception ex) + => new PreconditionGroupResult(CommandError.Exception, ex.Message, null); + public static new PreconditionGroupResult FromError(IResult result) //needed? + => new PreconditionGroupResult(result.Error, result.ErrorReason, null); + + public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; + private string DebuggerDisplay => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Results/PreconditionResult.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Results/PreconditionResult.cs new file mode 100644 index 0000000..d8e399a --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Results/PreconditionResult.cs @@ -0,0 +1,59 @@ +using System; +using System.Diagnostics; + +namespace Discord.Commands +{ + /// + /// Represents a result type for command preconditions. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class PreconditionResult : IResult + { + /// + public CommandError? Error { get; } + /// + public string ErrorReason { get; } + + /// + public bool IsSuccess => !Error.HasValue; + + /// + /// Initializes a new class with the command type + /// and reason. + /// + /// The type of failure. + /// The reason of failure. + protected PreconditionResult(CommandError? error, string errorReason) + { + Error = error; + ErrorReason = errorReason; + } + + /// + /// Returns a with no errors. + /// + public static PreconditionResult FromSuccess() + => new PreconditionResult(null, null); + /// + /// Returns a with and the + /// specified reason. + /// + /// The reason of failure. + public static PreconditionResult FromError(string reason) + => new PreconditionResult(CommandError.UnmetPrecondition, reason); + public static PreconditionResult FromError(Exception ex) + => new PreconditionResult(CommandError.Exception, ex.Message); + /// + /// Returns a with the specified type. + /// + /// The result of failure. + public static PreconditionResult FromError(IResult result) + => new PreconditionResult(result.Error, result.ErrorReason); + + /// + /// Returns a string indicating whether the is successful. + /// + public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; + private string DebuggerDisplay => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Results/RuntimeResult.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Results/RuntimeResult.cs new file mode 100644 index 0000000..e4c86fc --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Results/RuntimeResult.cs @@ -0,0 +1,33 @@ +using System.Diagnostics; + +namespace Discord.Commands +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public abstract class RuntimeResult : IResult + { + /// + /// Initializes a new class with the type of error and reason. + /// + /// The type of failure, or null if none. + /// The reason of failure. + protected RuntimeResult(CommandError? error, string reason) + { + Error = error; + Reason = reason; + } + + /// + public CommandError? Error { get; } + /// Describes the execution reason or result. + public string Reason { get; } + + /// + public bool IsSuccess => !Error.HasValue; + + /// + string IResult.ErrorReason => Reason; + + public override string ToString() => Reason ?? (IsSuccess ? "Successful" : "Unsuccessful"); + private string DebuggerDisplay => IsSuccess ? $"Success: {Reason ?? "No Reason"}" : $"{Error}: {Reason}"; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Results/SearchResult.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Results/SearchResult.cs new file mode 100644 index 0000000..d1f1ea0 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Results/SearchResult.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Discord.Commands +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public struct SearchResult : IResult + { + public string Text { 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) + { + Text = text; + Commands = commands; + Error = error; + ErrorReason = errorReason; + } + + 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); + public static SearchResult FromError(Exception ex) + => FromError(CommandError.Exception, ex.Message); + public static SearchResult FromError(IResult result) + => new SearchResult(null, null, result.Error, result.ErrorReason); + + public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; + private string DebuggerDisplay => IsSuccess ? $"Success ({Commands.Count} Results)" : $"{Error}: {ErrorReason}"; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Results/TypeReaderResult.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Results/TypeReaderResult.cs new file mode 100644 index 0000000..2fbdb45 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Results/TypeReaderResult.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; + +namespace Discord.Commands +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public struct TypeReaderValue + { + public object Value { get; } + public float Score { get; } + + public TypeReaderValue(object value, float score) + { + Value = value; + Score = score; + } + + public override string ToString() => Value?.ToString(); + private string DebuggerDisplay => $"[{Value}, {Math.Round(Score, 2)}]"; + } + + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public struct TypeReaderResult : IResult + { + public IReadOnlyCollection Values { get; } + + /// + public CommandError? Error { get; } + /// + public string ErrorReason { get; } + + /// + public bool IsSuccess => !Error.HasValue; + + /// TypeReaderResult was not successful. + public object BestMatch => IsSuccess + ? (Values.Count == 1 ? Values.Single().Value : Values.OrderByDescending(v => v.Score).First().Value) + : throw new InvalidOperationException("TypeReaderResult was not successful."); + + private TypeReaderResult(IReadOnlyCollection values, CommandError? error, string errorReason) + { + Values = values; + Error = error; + ErrorReason = errorReason; + } + + public static TypeReaderResult FromSuccess(object value) + => new TypeReaderResult(ImmutableArray.Create(new TypeReaderValue(value, 1.0f)), null, null); + public static TypeReaderResult FromSuccess(TypeReaderValue value) + => new TypeReaderResult(ImmutableArray.Create(value), null, null); + public static TypeReaderResult FromSuccess(IReadOnlyCollection values) + => new TypeReaderResult(values, null, null); + public static TypeReaderResult FromError(CommandError error, string reason) + => new TypeReaderResult(null, error, reason); + public static TypeReaderResult FromError(Exception ex) + => FromError(CommandError.Exception, ex.Message); + public static TypeReaderResult FromError(IResult result) + => new TypeReaderResult(null, result.Error, result.ErrorReason); + + public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; + private string DebuggerDisplay => IsSuccess ? $"Success ({string.Join(", ", Values)})" : $"{Error}: {ErrorReason}"; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/RunMode.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/RunMode.cs new file mode 100644 index 0000000..8e230b5 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/RunMode.cs @@ -0,0 +1,23 @@ +namespace Discord.Commands +{ + /// + /// Specifies the behavior of the command execution workflow. + /// + /// + /// + public enum RunMode + { + /// + /// The default behaviour set in . + /// + Default, + /// + /// Executes the command on the same thread as gateway one. + /// + Sync, + /// + /// Executes the command on a different thread from the gateway one. + /// + Async + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Utilities/QuotationAliasUtils.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Utilities/QuotationAliasUtils.cs new file mode 100644 index 0000000..2612e99 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Utilities/QuotationAliasUtils.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; + +namespace Discord.Commands +{ + /// + /// Utility class which contains the default matching pairs of quotation marks for CommandServiceConfig + /// + internal static class QuotationAliasUtils + { + /// + /// A default map of open-close pairs of quotation marks. + /// Contains many regional and Unicode equivalents. + /// Used in the . + /// + /// + internal static Dictionary GetDefaultAliasMap + { + get + { + // Output of a gist provided by https://gist.github.com/ufcpp + // https://gist.github.com/ufcpp/5b2cf9a9bf7d0b8743714a0b88f7edc5 + // This was not used for the implementation because of incompatibility with netstandard1.1 + return new Dictionary { + {'\"', '\"' }, + {'«', '»' }, + {'‘', '’' }, + {'“', '”' }, + {'„', '‟' }, + {'‹', '›' }, + {'‚', '‛' }, + {'《', '》' }, + {'〈', '〉' }, + {'「', '」' }, + {'『', '』' }, + {'〝', '〞' }, + {'﹁', '﹂' }, + {'﹃', '﹄' }, + {'"', '"' }, + {''', ''' }, + {'「', '」' }, + {'(', ')' }, + {'༺', '༻' }, + {'༼', '༽' }, + {'᚛', '᚜' }, + {'⁅', '⁆' }, + {'⌈', '⌉' }, + {'⌊', '⌋' }, + {'❨', '❩' }, + {'❪', '❫' }, + {'❬', '❭' }, + {'❮', '❯' }, + {'❰', '❱' }, + {'❲', '❳' }, + {'❴', '❵' }, + {'⟅', '⟆' }, + {'⟦', '⟧' }, + {'⟨', '⟩' }, + {'⟪', '⟫' }, + {'⟬', '⟭' }, + {'⟮', '⟯' }, + {'⦃', '⦄' }, + {'⦅', '⦆' }, + {'⦇', '⦈' }, + {'⦉', '⦊' }, + {'⦋', '⦌' }, + {'⦍', '⦎' }, + {'⦏', '⦐' }, + {'⦑', '⦒' }, + {'⦓', '⦔' }, + {'⦕', '⦖' }, + {'⦗', '⦘' }, + {'⧘', '⧙' }, + {'⧚', '⧛' }, + {'⧼', '⧽' }, + {'⸂', '⸃' }, + {'⸄', '⸅' }, + {'⸉', '⸊' }, + {'⸌', '⸍' }, + {'⸜', '⸝' }, + {'⸠', '⸡' }, + {'⸢', '⸣' }, + {'⸤', '⸥' }, + {'⸦', '⸧' }, + {'⸨', '⸩' }, + {'【', '】'}, + {'〔', '〕' }, + {'〖', '〗' }, + {'〘', '〙' }, + {'〚', '〛' } + }; + } + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Utilities/ReflectionUtils.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Utilities/ReflectionUtils.cs new file mode 100644 index 0000000..062af04 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Utilities/ReflectionUtils.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Discord.Commands +{ + internal static class ReflectionUtils + { + private static readonly TypeInfo ObjectTypeInfo = typeof(object).GetTypeInfo(); + + internal static T CreateObject(TypeInfo typeInfo, CommandService commands, IServiceProvider services = null) + => CreateBuilder(typeInfo, commands)(services); + internal static Func CreateBuilder(TypeInfo typeInfo, CommandService commands) + { + var constructor = GetConstructor(typeInfo); + var parameters = constructor.GetParameters(); + var properties = GetProperties(typeInfo); + + return (services) => + { + var args = new object[parameters.Length]; + for (int i = 0; i < parameters.Length; i++) + args[i] = GetMember(commands, services, parameters[i].ParameterType, typeInfo); + var obj = InvokeConstructor(constructor, args, typeInfo); + + foreach(var property in properties) + property.SetValue(obj, GetMember(commands, services, property.PropertyType, typeInfo)); + return obj; + }; + } + private static T InvokeConstructor(ConstructorInfo constructor, object[] args, TypeInfo ownerType) + { + try + { + return (T)constructor.Invoke(args); + } + catch (Exception ex) + { + throw new Exception($"Failed to create \"{ownerType.FullName}\".", ex); + } + } + + private static ConstructorInfo GetConstructor(TypeInfo ownerType) + { + var constructors = ownerType.DeclaredConstructors.Where(x => !x.IsStatic).ToArray(); + if (constructors.Length == 0) + throw new InvalidOperationException($"No constructor found for \"{ownerType.FullName}\"."); + else if (constructors.Length > 1) + throw new InvalidOperationException($"Multiple constructors found for \"{ownerType.FullName}\"."); + return constructors[0]; + } + private static PropertyInfo[] GetProperties(TypeInfo ownerType) + { + var result = new List(); + while (ownerType != ObjectTypeInfo) + { + foreach (var prop in ownerType.DeclaredProperties) + { + if (prop.SetMethod?.IsStatic == false && prop.SetMethod?.IsPublic == true && prop.GetCustomAttribute() == null) + result.Add(prop); + } + ownerType = ownerType.BaseType.GetTypeInfo(); + } + return result.ToArray(); + } + private static object GetMember(CommandService commands, IServiceProvider services, Type memberType, TypeInfo ownerType) + { + if (memberType == typeof(CommandService)) + return commands; + if (memberType == typeof(IServiceProvider) || memberType == services.GetType()) + return services; + var service = services.GetService(memberType); + if (service != null) + return service; + throw new InvalidOperationException($"Failed to create \"{ownerType.FullName}\", dependency \"{memberType.Name}\" was not found."); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/AssemblyInfo.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/AssemblyInfo.cs new file mode 100644 index 0000000..b7c60f3 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Discord.Net.Relay")] +[assembly: InternalsVisibleTo("Discord.Net.Rest")] +[assembly: InternalsVisibleTo("Discord.Net.Rpc")] +[assembly: InternalsVisibleTo("Discord.Net.WebSocket")] +[assembly: InternalsVisibleTo("Discord.Net.Webhook")] +[assembly: InternalsVisibleTo("Discord.Net.Commands")] +[assembly: InternalsVisibleTo("Discord.Net.Tests")] +[assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")] diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Audio/AudioApplication.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Audio/AudioApplication.cs new file mode 100644 index 0000000..276d934 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Audio/AudioApplication.cs @@ -0,0 +1,9 @@ +namespace Discord.Audio +{ + public enum AudioApplication : int + { + Voice, + Music, + Mixed + } +} \ No newline at end of file diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Audio/AudioInStream.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Audio/AudioInStream.cs new file mode 100644 index 0000000..656c0bc --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Audio/AudioInStream.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Audio +{ + public abstract class AudioInStream : AudioStream + { + public abstract int AvailableFrames { get; } + + public override bool CanRead => true; + public override bool CanWrite => true; + + public abstract Task ReadFrameAsync(CancellationToken cancelToken); + public abstract bool TryReadFrame(CancellationToken cancelToken, out RTPFrame frame); + + public override Task FlushAsync(CancellationToken cancelToken) { throw new NotSupportedException(); } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Audio/AudioOutStream.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Audio/AudioOutStream.cs new file mode 100644 index 0000000..cbc3167 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Audio/AudioOutStream.cs @@ -0,0 +1,23 @@ +using System; +using System.IO; + +namespace Discord.Audio +{ + public abstract class AudioOutStream : AudioStream + { + public override bool CanWrite => true; + + /// + /// Reading this stream is not supported. + public override int Read(byte[] buffer, int offset, int count) => + throw new NotSupportedException(); + /// + /// Setting the length to this stream is not supported. + public override void SetLength(long value) => + throw new NotSupportedException(); + /// + /// Seeking this stream is not supported.. + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException(); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Audio/AudioStream.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Audio/AudioStream.cs new file mode 100644 index 0000000..2287d47 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Audio/AudioStream.cs @@ -0,0 +1,58 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Audio +{ + public abstract class AudioStream : Stream + { + public override bool CanRead => false; + public override bool CanSeek => false; + public override bool CanWrite => false; + + /// This stream does not accept headers. + public virtual void WriteHeader(ushort seq, uint timestamp, bool missed) => + throw new InvalidOperationException("This stream does not accept headers."); + public override void Write(byte[] buffer, int offset, int count) + { + WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); + } + public override void Flush() + { + FlushAsync(CancellationToken.None).GetAwaiter().GetResult(); + } + public void Clear() + { + ClearAsync(CancellationToken.None).GetAwaiter().GetResult(); + } + + public virtual Task ClearAsync(CancellationToken cancellationToken) { return Task.Delay(0); } + + /// + /// Reading stream length is not supported. + public override long Length => + throw new NotSupportedException(); + + /// + /// Getting or setting this stream position is not supported. + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + /// + /// Reading this stream is not supported. + public override int Read(byte[] buffer, int offset, int count) => + throw new NotSupportedException(); + /// + /// Setting the length to this stream is not supported. + public override void SetLength(long value) => + throw new NotSupportedException(); + /// + /// Seeking this stream is not supported.. + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException(); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Audio/IAudioClient.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Audio/IAudioClient.cs new file mode 100644 index 0000000..018c8bc --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Audio/IAudioClient.cs @@ -0,0 +1,35 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Audio +{ + public interface IAudioClient : IDisposable + { + event Func Connected; + event Func Disconnected; + event Func LatencyUpdated; + event Func UdpLatencyUpdated; + event Func StreamCreated; + event Func StreamDestroyed; + event Func SpeakingUpdated; + + /// Gets the current connection state of this client. + ConnectionState ConnectionState { get; } + /// Gets the estimated round-trip latency, in milliseconds, to the voice WebSocket server. + int Latency { get; } + /// Gets the estimated round-trip latency, in milliseconds, to the voice UDP server. + int UdpLatency { get; } + + Task StopAsync(); + Task SetSpeakingAsync(bool value); + + /// Creates a new outgoing stream accepting Opus-encoded data. + AudioOutStream CreateOpusStream(int bufferMillis = 1000); + /// Creates a new outgoing stream accepting Opus-encoded data. This is a direct stream with no internal timer. + AudioOutStream CreateDirectOpusStream(); + /// Creates a new outgoing stream accepting PCM (raw) data. + AudioOutStream CreatePCMStream(AudioApplication application, int? bitrate = null, int bufferMillis = 1000, int packetLoss = 30); + /// Creates a new direct outgoing stream accepting PCM (raw) data. This is a direct stream with no internal timer. + AudioOutStream CreateDirectPCMStream(AudioApplication application, int? bitrate = null, int packetLoss = 30); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Audio/RTPFrame.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Audio/RTPFrame.cs new file mode 100644 index 0000000..6254b71 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Audio/RTPFrame.cs @@ -0,0 +1,18 @@ +namespace Discord.Audio +{ + public struct RTPFrame + { + public readonly ushort Sequence; + public readonly uint Timestamp; + public readonly byte[] Payload; + public readonly bool Missed; + + public RTPFrame(ushort sequence, uint timestamp, byte[] payload, bool missed) + { + Sequence = sequence; + Timestamp = timestamp; + Payload = payload; + Missed = missed; + } + } +} \ No newline at end of file diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/CDN.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/CDN.cs new file mode 100644 index 0000000..32ffbba --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/CDN.cs @@ -0,0 +1,160 @@ +using System; + +namespace Discord +{ + /// + /// Represents a class containing the strings related to various Content Delivery Networks (CDNs). + /// + public static class CDN + { + /// + /// Returns an application icon URL. + /// + /// The application identifier. + /// The icon identifier. + /// + /// A URL pointing to the application's icon. + /// + public static string GetApplicationIconUrl(ulong appId, string iconId) + => iconId != null ? $"{DiscordConfig.CDNUrl}app-icons/{appId}/{iconId}.jpg" : null; + + /// + /// Returns a user avatar URL. + /// + /// The user snowflake identifier. + /// The avatar identifier. + /// The size of the image to return in horizontal pixels. This can be any power of two between 16 and 2048. + /// The format to return. + /// + /// A URL pointing to the user's avatar in the specified size. + /// + public static string GetUserAvatarUrl(ulong userId, string avatarId, ushort size, ImageFormat format) + { + if (avatarId == null) + return null; + string extension = FormatToExtension(format, avatarId); + return $"{DiscordConfig.CDNUrl}avatars/{userId}/{avatarId}.{extension}?size={size}"; + } + /// + /// Returns the default user avatar URL. + /// + /// The discriminator value of a user. + /// + /// A URL pointing to the user's default avatar when one isn't set. + /// + public static string GetDefaultUserAvatarUrl(ushort discriminator) + { + return $"{DiscordConfig.CDNUrl}embed/avatars/{discriminator % 5}.png"; + } + /// + /// Returns an icon URL. + /// + /// The guild snowflake identifier. + /// The icon identifier. + /// + /// A URL pointing to the guild's icon. + /// + public static string GetGuildIconUrl(ulong guildId, string iconId) + => iconId != null ? $"{DiscordConfig.CDNUrl}icons/{guildId}/{iconId}.jpg" : null; + /// + /// Returns a guild splash URL. + /// + /// The guild snowflake identifier. + /// The splash icon identifier. + /// + /// A URL pointing to the guild's icon. + /// + public static string GetGuildSplashUrl(ulong guildId, string splashId) + => splashId != null ? $"{DiscordConfig.CDNUrl}splashes/{guildId}/{splashId}.jpg" : null; + /// + /// Returns a channel icon URL. + /// + /// The channel snowflake identifier. + /// The icon identifier. + /// + /// A URL pointing to the channel's icon. + /// + public static string GetChannelIconUrl(ulong channelId, string iconId) + => iconId != null ? $"{DiscordConfig.CDNUrl}channel-icons/{channelId}/{iconId}.jpg" : null; + + /// + /// Returns a guild banner URL. + /// + /// The guild snowflake identifier. + /// The banner image identifier. + /// The size of the image to return in horizontal pixels. This can be any power of two between 16 and 2048 inclusive. + /// + /// A URL pointing to the guild's banner image. + /// + public static string GetGuildBannerUrl(ulong guildId, string bannerId, ushort? size = null) + { + if (!string.IsNullOrEmpty(bannerId)) + return $"{DiscordConfig.CDNUrl}banners/{guildId}/{bannerId}.jpg" + (size.HasValue ? $"?size={size}" : string.Empty); + return null; + } + /// + /// Returns an emoji URL. + /// + /// The emoji snowflake identifier. + /// Whether this emoji is animated. + /// + /// A URL pointing to the custom emote. + /// + public static string GetEmojiUrl(ulong emojiId, bool animated) + => $"{DiscordConfig.CDNUrl}emojis/{emojiId}.{(animated ? "gif" : "png")}"; + + /// + /// Returns a Rich Presence asset URL. + /// + /// The application identifier. + /// The asset identifier. + /// The size of the image to return in. This can be any power of two between 16 and 2048. + /// The format to return. + /// + /// A URL pointing to the asset image in the specified size. + /// + public static string GetRichAssetUrl(ulong appId, string assetId, ushort size, ImageFormat format) + { + string extension = FormatToExtension(format, ""); + return $"{DiscordConfig.CDNUrl}app-assets/{appId}/{assetId}.{extension}?size={size}"; + } + + /// + /// Returns a Spotify album URL. + /// + /// The identifier for the album art (e.g. 6be8f4c8614ecf4f1dd3ebba8d8692d8ce4951ac). + /// + /// A URL pointing to the Spotify album art. + /// + public static string GetSpotifyAlbumArtUrl(string albumArtId) + => $"https://i.scdn.co/image/{albumArtId}"; + /// + /// Returns a Spotify direct URL for a track. + /// + /// The identifier for the track (e.g. 4uLU6hMCjMI75M1A2tKUQC). + /// + /// A URL pointing to the Spotify track. + /// + public static string GetSpotifyDirectUrl(string trackId) + => $"https://open.spotify.com/track/{trackId}"; + + private static string FormatToExtension(ImageFormat format, string imageId) + { + if (format == ImageFormat.Auto) + format = imageId.StartsWith("a_") ? ImageFormat.Gif : ImageFormat.Png; + switch (format) + { + case ImageFormat.Gif: + return "gif"; + case ImageFormat.Jpeg: + return "jpeg"; + case ImageFormat.Png: + return "png"; + case ImageFormat.WebP: + return "webp"; + default: + throw new ArgumentException(nameof(format)); + } + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Commands/ICommandContext.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Commands/ICommandContext.cs new file mode 100644 index 0000000..d56eb38 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Commands/ICommandContext.cs @@ -0,0 +1,29 @@ +namespace Discord.Commands +{ + /// + /// Represents a context of a command. This may include the client, guild, channel, user, and message. + /// + public interface ICommandContext + { + /// + /// Gets the that the command is executed with. + /// + IDiscordClient Client { get; } + /// + /// Gets the that the command is executed in. + /// + IGuild Guild { get; } + /// + /// Gets the that the command is executed in. + /// + IMessageChannel Channel { get; } + /// + /// Gets the who executed the command. + /// + IUser User { get; } + /// + /// Gets the that the command is interpreted from. + /// + IUserMessage Message { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/ConnectionState.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/ConnectionState.cs new file mode 100644 index 0000000..fadbc40 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/ConnectionState.cs @@ -0,0 +1,15 @@ +namespace Discord +{ + /// Specifies the connection state of a client. + public enum ConnectionState : byte + { + /// The client has disconnected from Discord. + Disconnected, + /// The client is connecting to Discord. + Connecting, + /// The client has established a connection to Discord. + Connected, + /// The client is disconnecting from Discord. + Disconnecting + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/DiscordConfig.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/DiscordConfig.cs new file mode 100644 index 0000000..51970a7 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/DiscordConfig.cs @@ -0,0 +1,174 @@ +using System.Reflection; + +namespace Discord +{ + /// + /// Defines various behaviors of Discord.Net. + /// + public class DiscordConfig + { + /// + /// Returns the API version Discord.Net uses. + /// + /// + /// An representing the API version that Discord.Net uses to communicate with Discord. + /// A list of available API version can be seen on the official + /// Discord API documentation + /// . + /// + public const int APIVersion = 6; + /// + /// Returns the Voice API version Discord.Net uses. + /// + /// + /// An representing the API version that Discord.Net uses to communicate with Discord's + /// voice server. + /// + public const int VoiceAPIVersion = 3; + /// + /// Gets the Discord.Net version, including the build number. + /// + /// + /// A string containing the detailed version information, including its build number; Unknown when + /// the version fails to be fetched. + /// + public static string Version { get; } = + typeof(DiscordConfig).GetTypeInfo().Assembly.GetCustomAttribute()?.InformationalVersion ?? + typeof(DiscordConfig).GetTypeInfo().Assembly.GetName().Version.ToString(3) ?? + "Unknown"; + + /// + /// Gets the user agent that Discord.Net uses in its clients. + /// + /// + /// The user agent used in each Discord.Net request. + /// + public static string UserAgent { get; } = $"DiscordBot (https://github.com/RogueException/Discord.Net, v{Version})"; + /// + /// Returns the base Discord API URL. + /// + /// + /// The Discord API URL using . + /// + public static readonly string APIUrl = $"https://discordapp.com/api/v{APIVersion}/"; + /// + /// Returns the base Discord CDN URL. + /// + /// + /// The base Discord Content Delivery Network (CDN) URL. + /// + public const string CDNUrl = "https://cdn.discordapp.com/"; + /// + /// Returns the base Discord invite URL. + /// + /// + /// The base Discord invite URL. + /// + public const string InviteUrl = "https://discord.gg/"; + + /// + /// Returns the default timeout for requests. + /// + /// + /// The amount of time it takes in milliseconds before a request is timed out. + /// + public const int DefaultRequestTimeout = 15000; + /// + /// Returns the max length for a Discord message. + /// + /// + /// The maximum length of a message allowed by Discord. + /// + public const int MaxMessageSize = 2000; + /// + /// Returns the max messages allowed to be in a request. + /// + /// + /// The maximum number of messages that can be gotten per-batch. + /// + public const int MaxMessagesPerBatch = 100; + /// + /// Returns the max users allowed to be in a request. + /// + /// + /// The maximum number of users that can be gotten per-batch. + /// + public const int MaxUsersPerBatch = 1000; + /// + /// Returns the max guilds allowed to be in a request. + /// + /// + /// The maximum number of guilds that can be gotten per-batch. + /// + public const int MaxGuildsPerBatch = 100; + /// + /// Returns the max user reactions allowed to be in a request. + /// + /// + /// The maximum number of user reactions that can be gotten per-batch. + /// + public const int MaxUserReactionsPerBatch = 100; + /// + /// Returns the max audit log entries allowed to be in a request. + /// + /// + /// The maximum number of audit log entries that can be gotten per-batch. + /// + public const int MaxAuditLogEntriesPerBatch = 100; + + /// + /// Gets or sets how a request should act in the case of an error, by default. + /// + /// + /// The currently set . + /// + public RetryMode DefaultRetryMode { get; set; } = RetryMode.AlwaysRetry; + + /// + /// Gets or sets the minimum log level severity that will be sent to the Log event. + /// + /// + /// The currently set for logging level. + /// + public LogSeverity LogLevel { get; set; } = LogSeverity.Info; + + /// + /// Gets or sets whether the initial log entry should be printed. + /// + /// + /// If set to true, the library will attempt to print the current version of the library, as well as + /// the API version it uses on startup. + /// + internal bool DisplayInitialLog { get; set; } = true; + + /// + /// Gets or sets the level of precision of the rate limit reset response. + /// + /// + /// If set to , this value will be rounded up to the + /// nearest second. + /// + /// + /// The currently set . + /// + public RateLimitPrecision RateLimitPrecision { get; set; } = RateLimitPrecision.Millisecond; + + /// + /// Gets or sets whether or not rate-limits should use the system clock. + /// + /// + /// If set to false, we will use the X-RateLimit-Reset-After header + /// to determine when a rate-limit expires, rather than comparing the + /// X-RateLimit-Reset timestamp to the system time. + /// + /// This should only be changed to false if the system is known to have + /// a clock that is out of sync. Relying on the Reset-After header will + /// incur network lag. + /// + /// Regardless of this property, we still rely on the system's wall-clock + /// to determine if a bucket is rate-limited; we do not use any monotonic + /// clock. Your system will still need a stable clock. + /// + public bool UseSystemClock { get; set; } = true; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/ActivityProperties.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/ActivityProperties.cs new file mode 100644 index 0000000..a7d1323 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/ActivityProperties.cs @@ -0,0 +1,38 @@ +using System; + +namespace Discord +{ + /// + /// Flags for the property, that are ORd together. + /// These describe what the activity payload includes. + /// + [Flags] + public enum ActivityProperties + { + /// + /// Indicates that no actions on this activity can be taken. + /// + None = 0, + Instance = 1, + /// + /// Indicates that this activity can be joined. + /// + Join = 0b10, + /// + /// Indicates that this activity can be spectated. + /// + Spectate = 0b100, + /// + /// Indicates that a user may request to join an activity. + /// + JoinRequest = 0b1000, + /// + /// Indicates that a user can listen along in Spotify. + /// + Sync = 0b10000, + /// + /// Indicates that a user can play this song. + /// + Play = 0b100000 + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/ActivityType.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/ActivityType.cs new file mode 100644 index 0000000..8c44f49 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/ActivityType.cs @@ -0,0 +1,29 @@ +namespace Discord +{ + /// + /// Specifies a Discord user's activity type. + /// + public enum ActivityType + { + /// + /// The user is playing a game. + /// + Playing = 0, + /// + /// The user is streaming online. + /// + Streaming = 1, + /// + /// The user is listening to a song. + /// + Listening = 2, + /// + /// The user is watching some form of media. + /// + Watching = 3, + /// + /// The user has set a custom status. + /// + CustomStatus = 4, + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/CustomStatusGame.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/CustomStatusGame.cs new file mode 100644 index 0000000..7bd2664 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/CustomStatusGame.cs @@ -0,0 +1,40 @@ +using System; +using System.Diagnostics; + +namespace Discord +{ + /// + /// A user's activity for their custom status. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class CustomStatusGame : Game + { + internal CustomStatusGame() { } + + /// + /// Gets the emote, if it is set. + /// + /// + /// An containing the or set by the user. + /// + public IEmote Emote { get; internal set; } + + /// + /// Gets the timestamp of when this status was created. + /// + /// + /// A containing the time when this status was created. + /// + public DateTimeOffset CreatedAt { get; internal set; } + + /// + /// Gets the state of the status. + /// + public string State { get; internal set; } + + public override string ToString() + => $"{Emote} {State}"; + + private string DebuggerDisplay => $"{Name}"; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/Game.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/Game.cs new file mode 100644 index 0000000..8891e14 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/Game.cs @@ -0,0 +1,38 @@ +using System.Diagnostics; + +namespace Discord +{ + /// + /// A user's game status. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class Game : IActivity + { + /// + public string Name { get; internal set; } + /// + public ActivityType Type { get; internal set; } + /// + public ActivityProperties Flags { get; internal set; } + /// + public string Details { get; internal set; } + + internal Game() { } + /// + /// Creates a with the provided name and . + /// + /// The name of the game. + /// The type of activity. + public Game(string name, ActivityType type = ActivityType.Playing, ActivityProperties flags = ActivityProperties.None, string details = null) + { + Name = name; + Type = type; + Flags = flags; + Details = details; + } + + /// Returns the name of the . + public override string ToString() => Name; + private string DebuggerDisplay => Name; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/GameAsset.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/GameAsset.cs new file mode 100644 index 0000000..7217bde --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/GameAsset.cs @@ -0,0 +1,38 @@ +namespace Discord +{ + /// + /// An asset for a object containing the text and image. + /// + public class GameAsset + { + internal GameAsset() { } + + internal ulong? ApplicationId { get; set; } + + /// + /// Gets the description of the asset. + /// + /// + /// A string containing the description of the asset. + /// + public string Text { get; internal set; } + /// + /// Gets the image ID of the asset. + /// + /// + /// A string containing the unique image identifier of the asset. + /// + public string ImageId { get; internal set; } + + /// + /// Returns the image URL of the asset. + /// + /// The size of the image to return in. This can be any power of two between 16 and 2048. + /// The format to return. + /// + /// A string pointing to the image URL of the asset; null when the application ID does not exist. + /// + public string GetImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => ApplicationId.HasValue ? CDN.GetRichAssetUrl(ApplicationId.Value, ImageId, size, format) : null; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/GameParty.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/GameParty.cs new file mode 100644 index 0000000..0cfa998 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/GameParty.cs @@ -0,0 +1,26 @@ +namespace Discord +{ + /// + /// Party information for a object. + /// + public class GameParty + { + internal GameParty() { } + + /// + /// Gets the ID of the party. + /// + /// + /// A string containing the unique identifier of the party. + /// + public string Id { get; internal set; } + public long Members { get; internal set; } + /// + /// Gets the party's current and maximum size. + /// + /// + /// A representing the capacity of the party. + /// + public long Capacity { get; internal set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/GameSecrets.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/GameSecrets.cs new file mode 100644 index 0000000..595b885 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/GameSecrets.cs @@ -0,0 +1,28 @@ +namespace Discord +{ + /// + /// Party secret for a object. + /// + public class GameSecrets + { + /// + /// Gets the secret for a specific instanced match. + /// + public string Match { get; } + /// + /// Gets the secret for joining a party. + /// + public string Join { get; } + /// + /// Gets the secret for spectating a game. + /// + public string Spectate { get; } + + internal GameSecrets(string match, string join, string spectate) + { + Match = match; + Join = join; + Spectate = spectate; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/GameTimestamps.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/GameTimestamps.cs new file mode 100644 index 0000000..a41388a --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/GameTimestamps.cs @@ -0,0 +1,25 @@ +using System; + +namespace Discord +{ + /// + /// Timestamps for a object. + /// + public class GameTimestamps + { + /// + /// Gets when the activity started. + /// + public DateTimeOffset? Start { get; } + /// + /// Gets when the activity ends. + /// + public DateTimeOffset? End { get; } + + internal GameTimestamps(DateTimeOffset? start, DateTimeOffset? end) + { + Start = start; + End = end; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/IActivity.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/IActivity.cs new file mode 100644 index 0000000..96704b8 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/IActivity.cs @@ -0,0 +1,40 @@ +namespace Discord +{ + /// + /// A user's activity status, typically a . + /// + public interface IActivity + { + /// + /// Gets the name of the activity. + /// + /// + /// A string containing the name of the activity that the user is doing. + /// + string Name { get; } + /// + /// Gets the type of the activity. + /// + /// + /// The type of activity. + /// + ActivityType Type { get; } + /// + /// Gets the flags that are relevant to this activity. + /// + /// + /// This value is determined by bitwise OR-ing values together. + /// + /// + /// The value of flags for this activity. + /// + ActivityProperties Flags { get; } + /// + /// Gets the details on what the player is currently doing. + /// + /// + /// A string describing what the player is doing. + /// + string Details { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/RichGame.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/RichGame.cs new file mode 100644 index 0000000..2da8d74 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/RichGame.cs @@ -0,0 +1,48 @@ +using System.Diagnostics; + +namespace Discord +{ + /// + /// A user's Rich Presence status. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RichGame : Game + { + internal RichGame() { } + + /// + /// Gets the user's current party status. + /// + public string State { get; internal set; } + /// + /// Gets the application ID for the game. + /// + public ulong ApplicationId { get; internal set; } + /// + /// Gets the small image for the presence and their hover texts. + /// + public GameAsset SmallAsset { get; internal set; } + /// + /// Gets the large image for the presence and their hover texts. + /// + public GameAsset LargeAsset { get; internal set; } + /// + /// Gets the information for the current party of the player. + /// + public GameParty Party { get; internal set; } + /// + /// Gets the secrets for Rich Presence joining and spectating. + /// + public GameSecrets Secrets { get; internal set; } + /// + /// Gets the timestamps for start and/or end of the game. + /// + public GameTimestamps Timestamps { get; internal set; } + + /// + /// Returns the name of the Rich Presence. + /// + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} (Rich)"; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/SpotifyGame.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/SpotifyGame.cs new file mode 100644 index 0000000..4eab34f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/SpotifyGame.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Discord +{ + /// + /// A user's activity for listening to a song on Spotify. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SpotifyGame : Game + { + /// + /// Gets the song's artist(s). + /// + /// + /// A collection of string containing all artists featured in the track (e.g. Avicii; Rita Ora). + /// + public IReadOnlyCollection Artists { get; internal set; } + /// + /// Gets the Spotify album title of the song. + /// + /// + /// A string containing the name of the album (e.g. AVĪCI (01)). + /// + public string AlbumTitle { get; internal set; } + /// + /// Gets the track title of the song. + /// + /// + /// A string containing the name of the song (e.g. Lonely Together (feat. Rita Ora)). + /// + public string TrackTitle { get; internal set; } + + /// + /// Gets the date when the track started playing. + /// + /// + /// A containing the start timestamp of the song. + /// + public DateTimeOffset? StartedAt { get; internal set; } + + /// + /// Gets the date when the track ends. + /// + /// + /// A containing the finish timestamp of the song. + /// + public DateTimeOffset? EndsAt { get; internal set; } + + /// + /// Gets the duration of the song. + /// + /// + /// A containing the duration of the song. + /// + public TimeSpan? Duration { get; internal set; } + + /// + /// Gets the elapsed duration of the song. + /// + /// + /// A containing the elapsed duration of the song. + /// + public TimeSpan? Elapsed => DateTimeOffset.UtcNow - StartedAt; + + /// + /// Gets the remaining duration of the song. + /// + /// + /// A containing the remaining duration of the song. + /// + public TimeSpan? Remaining => EndsAt - DateTimeOffset.UtcNow; + + /// + /// Gets the track ID of the song. + /// + /// + /// A string containing the Spotify ID of the track (e.g. 7DoN0sCGIT9IcLrtBDm4f0). + /// + public string TrackId { get; internal set; } + /// + /// Gets the session ID of the song. + /// + /// + /// The purpose of this property is currently unknown. + /// + /// + /// A string containing the session ID. + /// + public string SessionId { get; internal set; } + + /// + /// Gets the URL of the album art. + /// + /// + /// A URL pointing to the album art of the track (e.g. + /// https://i.scdn.co/image/ba2fd8823d42802c2f8738db0b33a4597f2f39e7). + /// + public string AlbumArtUrl { get; internal set; } + /// + /// Gets the direct Spotify URL of the track. + /// + /// + /// A URL pointing directly to the track on Spotify. (e.g. + /// https://open.spotify.com/track/7DoN0sCGIT9IcLrtBDm4f0). + /// + public string TrackUrl { get; internal set; } + + internal SpotifyGame() { } + + /// + /// Gets the full information of the song. + /// + /// + /// A string containing the full information of the song (e.g. + /// Avicii, Rita Ora - Lonely Together (feat. Rita Ora) (3:08) + /// + public override string ToString() => $"{string.Join(", ", Artists)} - {TrackTitle} ({Duration})"; + private string DebuggerDisplay => $"{Name} (Spotify)"; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/StreamingGame.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/StreamingGame.cs new file mode 100644 index 0000000..127ae0b --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Activities/StreamingGame.cs @@ -0,0 +1,34 @@ +using System.Diagnostics; + +namespace Discord +{ + /// + /// A user's activity for streaming on services such as Twitch. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class StreamingGame : Game + { + /// + /// Gets the URL of the stream. + /// + public string Url { get; internal set; } + + /// + /// Creates a new based on the on the stream URL. + /// + /// The name of the stream. + /// The URL of the stream. + public StreamingGame(string name, string url) + { + Name = name; + Url = url; + Type = ActivityType.Streaming; + } + + /// + /// Gets the name of the stream. + /// + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Url})"; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/AuditLogs/ActionType.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/AuditLogs/ActionType.cs new file mode 100644 index 0000000..2561a09 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/AuditLogs/ActionType.cs @@ -0,0 +1,122 @@ +namespace Discord +{ + /// + /// Representing a type of action within an . + /// + public enum ActionType + { + /// + /// this guild was updated. + /// + GuildUpdated = 1, + + /// + /// A channel was created. + /// + ChannelCreated = 10, + /// + /// A channel was updated. + /// + ChannelUpdated = 11, + /// + /// A channel was deleted. + /// + ChannelDeleted = 12, + + /// + /// A permission overwrite was created for a channel. + /// + OverwriteCreated = 13, + /// + /// A permission overwrite was updated for a channel. + /// + OverwriteUpdated = 14, + /// + /// A permission overwrite was deleted for a channel. + /// + OverwriteDeleted = 15, + + /// + /// A user was kicked from this guild. + /// + Kick = 20, + /// + /// A prune took place in this guild. + /// + Prune = 21, + /// + /// A user banned another user from this guild. + /// + Ban = 22, + /// + /// A user unbanned another user from this guild. + /// + Unban = 23, + + /// + /// A guild member whose information was updated. + /// + MemberUpdated = 24, + /// + /// A guild member's role collection was updated. + /// + MemberRoleUpdated = 25, + + /// + /// A role was created in this guild. + /// + RoleCreated = 30, + /// + /// A role was updated in this guild. + /// + RoleUpdated = 31, + /// + /// A role was deleted from this guild. + /// + RoleDeleted = 32, + + /// + /// An invite was created in this guild. + /// + InviteCreated = 40, + /// + /// An invite was updated in this guild. + /// + InviteUpdated = 41, + /// + /// An invite was deleted from this guild. + /// + InviteDeleted = 42, + + /// + /// A Webhook was created in this guild. + /// + WebhookCreated = 50, + /// + /// A Webhook was updated in this guild. + /// + WebhookUpdated = 51, + /// + /// A Webhook was deleted from this guild. + /// + WebhookDeleted = 52, + + /// + /// An emoji was created in this guild. + /// + EmojiCreated = 60, + /// + /// An emoji was updated in this guild. + /// + EmojiUpdated = 61, + /// + /// An emoji was deleted from this guild. + /// + EmojiDeleted = 62, + + /// + /// A message was deleted from this guild. + /// + MessageDeleted = 72 + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/AuditLogs/IAuditLogData.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/AuditLogs/IAuditLogData.cs new file mode 100644 index 0000000..a99a14e --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/AuditLogs/IAuditLogData.cs @@ -0,0 +1,8 @@ +namespace Discord +{ + /// + /// Represents data applied to an . + /// + public interface IAuditLogData + { } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/AuditLogs/IAuditLogEntry.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/AuditLogs/IAuditLogEntry.cs new file mode 100644 index 0000000..15ae5fd --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/AuditLogs/IAuditLogEntry.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a generic audit log entry. + /// + public interface IAuditLogEntry : ISnowflakeEntity + { + /// + /// Gets the action which occurred to create this entry. + /// + /// + /// The type of action for this audit log entry. + /// + ActionType Action { get; } + + /// + /// Gets the data for this entry. + /// + /// + /// An for this audit log entry; null if no data is available. + /// + IAuditLogData Data { get; } + + /// + /// Gets the user responsible for causing the changes. + /// + /// + /// A user object. + /// + IUser User { get; } + + /// + /// Gets the reason behind the change. + /// + /// + /// A string containing the reason for the change; null if none is provided. + /// + string Reason { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/CacheMode.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/CacheMode.cs new file mode 100644 index 0000000..503a804 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/CacheMode.cs @@ -0,0 +1,17 @@ +namespace Discord +{ + /// + /// Specifies the cache mode that should be used. + /// + public enum CacheMode + { + /// + /// Allows the object to be downloaded if it does not exist in the current cache. + /// + AllowDownload, + /// + /// Only allows the object to be pulled from the existing cache. + /// + CacheOnly + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/ChannelType.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/ChannelType.cs new file mode 100644 index 0000000..6dd910b --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/ChannelType.cs @@ -0,0 +1,19 @@ +namespace Discord +{ + /// Defines the types of channels. + public enum ChannelType + { + /// The channel is a text channel. + Text = 0, + /// The channel is a Direct Message channel. + DM = 1, + /// The channel is a voice channel. + Voice = 2, + /// The channel is a group channel. + Group = 3, + /// The channel is a category channel. + Category = 4, + /// The channel is a news channel. + News = 5 + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/Direction.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/Direction.cs new file mode 100644 index 0000000..efdf4ff --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/Direction.cs @@ -0,0 +1,30 @@ +namespace Discord +{ + /// + /// Specifies the direction of where message(s) should be retrieved from. + /// + /// + /// This enum is used to specify the direction for retrieving messages. + /// + /// At the time of writing, is not yet implemented into + /// . + /// Attempting to use the method with will throw + /// a . + /// + /// + public enum Direction + { + /// + /// The message(s) should be retrieved before a message. + /// + Before, + /// + /// The message(s) should be retrieved after a message. + /// + After, + /// + /// The message(s) should be retrieved around a message. + /// + Around + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs new file mode 100644 index 0000000..9552b0a --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs @@ -0,0 +1,34 @@ +namespace Discord +{ + /// + /// Properties that are used to modify an with the specified changes. + /// + /// + public class GuildChannelProperties + { + /// + /// Gets or sets the channel to this name. + /// + /// + /// This property defines the new name for this channel. + /// + /// When modifying an , the must be alphanumeric with + /// dashes. It must match the RegEx [a-z0-9-_]{2,100}. + /// + /// + public Optional Name { get; set; } + /// + /// Moves the channel to the following position. This property is zero-based. + /// + public Optional Position { get; set; } + /// + /// Gets or sets the category ID for this channel. + /// + /// + /// Setting this value to a category's snowflake identifier will change or set this channel's parent to the + /// specified channel; setting this value to 0 will detach this channel from its parent if one + /// is set. + /// + public Optional CategoryId { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/IAudioChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/IAudioChannel.cs new file mode 100644 index 0000000..179f4b0 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/IAudioChannel.cs @@ -0,0 +1,31 @@ +using Discord.Audio; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a generic audio channel. + /// + public interface IAudioChannel : IChannel + { + /// + /// Connects to this audio channel. + /// + /// Determines whether the client should deaf itself upon connection. + /// Determines whether the client should mute itself upon connection. + /// Determines whether the audio client is an external one or not. + /// + /// A task representing the asynchronous connection operation. The task result contains the + /// responsible for the connection. + /// + Task ConnectAsync(bool selfDeaf = false, bool selfMute = false, bool external = false); + + /// + /// Disconnects from this audio channel. + /// + /// + /// A task representing the asynchronous operation for disconnecting from the audio channel. + /// + Task DisconnectAsync(); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/ICategoryChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/ICategoryChannel.cs new file mode 100644 index 0000000..838908b --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/ICategoryChannel.cs @@ -0,0 +1,9 @@ +namespace Discord +{ + /// + /// Represents a generic category channel. + /// + public interface ICategoryChannel : IGuildChannel + { + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/IChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/IChannel.cs new file mode 100644 index 0000000..e2df86f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/IChannel.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a generic channel. + /// + public interface IChannel : ISnowflakeEntity + { + /// + /// Gets the name of this channel. + /// + /// + /// A string containing the name of this channel. + /// + string Name { get; } + + /// + /// Gets a collection of users that are able to view the channel or are currently in this channel. + /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the individual messages as a + /// collection. + /// + /// This method will attempt to fetch all users that is able to view this channel or is currently in this channel. + /// The library will attempt to split up the requests according to and . + /// In other words, if there are 3000 users, and the constant + /// is 1000, the request will be split into 3 individual requests; thus returning 53individual asynchronous + /// responses, hence the need of flattening. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// Paged collection of users. + /// + IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + + /// + /// Gets a user in this channel. + /// + /// The snowflake identifier of the user (e.g. 168693960628371456). + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a user object that + /// represents the found user; null if none is found. + /// + Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/IDMChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/IDMChannel.cs new file mode 100644 index 0000000..f0ef7f3 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/IDMChannel.cs @@ -0,0 +1,27 @@ +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a generic direct-message channel. + /// + public interface IDMChannel : IMessageChannel, IPrivateChannel + { + /// + /// Gets the recipient of all messages in this channel. + /// + /// + /// A user object that represents the other user in this channel. + /// + IUser Recipient { get; } + + /// + /// Closes this private channel, removing it from your channel list. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous close operation. + /// + Task CloseAsync(RequestOptions options = null); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/IGroupChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/IGroupChannel.cs new file mode 100644 index 0000000..77af345 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/IGroupChannel.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a generic private group channel. + /// + public interface IGroupChannel : IMessageChannel, IPrivateChannel, IAudioChannel + { + /// + /// Leaves this group. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous leave operation. + /// + Task LeaveAsync(RequestOptions options = null); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/IGuildChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/IGuildChannel.cs new file mode 100644 index 0000000..992bd71 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/IGuildChannel.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a generic guild channel. + /// + /// + /// + /// + public interface IGuildChannel : IChannel, IDeletable + { + /// + /// Gets the position of this channel. + /// + /// + /// An representing the position of this channel in the guild's channel list relative to + /// others of the same type. + /// + int Position { get; } + + /// + /// Gets the guild associated with this channel. + /// + /// + /// A guild object that this channel belongs to. + /// + IGuild Guild { get; } + /// + /// Gets the guild ID associated with this channel. + /// + /// + /// An representing the guild snowflake identifier for the guild that this channel + /// belongs to. + /// + ulong GuildId { get; } + /// + /// Gets a collection of permission overwrites for this channel. + /// + /// + /// A collection of overwrites associated with this channel. + /// + IReadOnlyCollection PermissionOverwrites { get; } + + /// + /// Modifies this guild channel. + /// + /// + /// This method modifies the current guild channel with the specified properties. To see an example of this + /// method and what properties are available, please refer to . + /// + /// The delegate containing the properties to modify the channel with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + Task ModifyAsync(Action func, RequestOptions options = null); + + /// + /// Gets the permission overwrite for a specific role. + /// + /// The role to get the overwrite from. + /// + /// An overwrite object for the targeted role; null if none is set. + /// + OverwritePermissions? GetPermissionOverwrite(IRole role); + /// + /// Gets the permission overwrite for a specific user. + /// + /// The user to get the overwrite from. + /// + /// An overwrite object for the targeted user; null if none is set. + /// + OverwritePermissions? GetPermissionOverwrite(IUser user); + /// + /// Removes the permission overwrite for the given role, if one exists. + /// + /// The role to remove the overwrite from. + /// The options to be used when sending the request. + /// + /// A task representing the asynchronous operation for removing the specified permissions from the channel. + /// + Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null); + /// + /// Removes the permission overwrite for the given user, if one exists. + /// + /// The user to remove the overwrite from. + /// The options to be used when sending the request. + /// + /// A task representing the asynchronous operation for removing the specified permissions from the channel. + /// + Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null); + + /// + /// Adds or updates the permission overwrite for the given role. + /// + /// + /// The following example fetches a role via and a channel via + /// . Next, it checks if an overwrite had already been set via + /// ; if not, it denies the role from sending any + /// messages to the channel. + /// + /// + /// The role to add the overwrite to. + /// The overwrite to add to the role. + /// The options to be used when sending the request. + /// + /// A task representing the asynchronous permission operation for adding the specified permissions to the + /// channel. + /// + Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null); + /// + /// Adds or updates the permission overwrite for the given user. + /// + /// + /// The following example fetches a user via and a channel via + /// . Next, it checks if an overwrite had already been set via + /// ; if not, it denies the user from sending any + /// messages to the channel. + /// + /// + /// The user to add the overwrite to. + /// The overwrite to add to the user. + /// The options to be used when sending the request. + /// + /// A task representing the asynchronous permission operation for adding the specified permissions to the channel. + /// + Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null); + + /// + /// Gets a collection of users that are able to view the channel or are currently in this channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// Paged collection of users. + /// + new IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a user in this channel. + /// + /// The snowflake identifier of the user. + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task representing the asynchronous get operation. The task result contains a guild user object that + /// represents the user; null if none is found. + /// + new Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/IMessageChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/IMessageChannel.cs new file mode 100644 index 0000000..b5aa69d --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/IMessageChannel.cs @@ -0,0 +1,269 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a generic channel that can send and receive messages. + /// + public interface IMessageChannel : IChannel + { + /// + /// Sends a message to this message channel. + /// + /// + /// The following example sends a message with the current system time in RFC 1123 format to the channel and + /// deletes itself after 5 seconds. + /// + /// + /// The message to be sent. + /// Determines whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); + /// + /// Sends a file to this message channel with an optional caption. + /// + /// + /// The following example uploads a local file called wumpus.txt along with the text + /// good discord boi to the channel. + /// + /// The following example uploads a local image called b1nzy.jpg embedded inside a rich embed to the + /// channel. + /// + /// + /// + /// This method sends a file as if you are uploading an attachment directly from your Discord client. + /// + /// If you wish to upload an image and have it embedded in a embed, + /// you may upload the file and refer to the file with "attachment://filename.ext" in the + /// . See the example section for its usage. + /// + /// + /// The file path of the file. + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// Whether the message attachment should be hidden as a spoiler. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false); + /// + /// Sends a file to this message channel with an optional caption. + /// + /// + /// The following example uploads a streamed image that will be called b1nzy.jpg embedded inside a + /// rich embed to the channel. + /// + /// + /// + /// This method sends a file as if you are uploading an attachment directly from your Discord client. + /// + /// If you wish to upload an image and have it embedded in a embed, + /// you may upload the file and refer to the file with "attachment://filename.ext" in the + /// . See the example section for its usage. + /// + /// + /// The of the file to be sent. + /// The name of the attachment. + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// Whether the message attachment should be hidden as a spoiler. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false); + + /// + /// Gets a message from this message channel. + /// + /// The snowflake identifier of the message. + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous get operation for retrieving the message. The task result contains + /// the retrieved message; null if no message is found with the specified identifier. + /// + Task GetMessageAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + + /// + /// Gets the last N messages from this message channel. + /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the individual messages as a + /// collection. + /// + /// + /// Do not fetch too many messages at once! This may cause unwanted preemptive rate limit or even actual + /// rate limit, causing your bot to freeze! + /// + /// This method will attempt to fetch the number of messages specified under . The + /// library will attempt to split up the requests according to your and + /// . In other words, should the user request 500 messages, + /// and the constant is 100, the request will + /// be split into 5 individual requests; thus returning 5 individual asynchronous responses, hence the need + /// of flattening. + /// + /// + /// The following example downloads 300 messages and gets messages that belong to the user + /// 53905483156684800. + /// + /// + /// The numbers of message to be gotten from. + /// The that determines whether the object should be fetched from + /// cache. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// + IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, + CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a collection of messages in this channel. + /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the individual messages as a + /// collection. + /// + /// + /// Do not fetch too many messages at once! This may cause unwanted preemptive rate limit or even actual + /// rate limit, causing your bot to freeze! + /// + /// This method will attempt to fetch the number of messages specified under around + /// the message depending on the . The library will + /// attempt to split up the requests according to your and + /// . In other words, should the user request 500 messages, + /// and the constant is 100, the request will + /// be split into 5 individual requests; thus returning 5 individual asynchronous responses, hence the need + /// of flattening. + /// + /// + /// The following example gets 5 message prior to the message identifier 442012544660537354. + /// + /// The following example attempts to retrieve messageCount number of messages from the + /// beginning of the channel and prints them to the console. + /// + /// + /// The ID of the starting message to get the messages from. + /// The direction of the messages to be gotten from. + /// The numbers of message to be gotten from. + /// The that determines whether the object should be fetched from + /// cache. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// + 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. + /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the individual messages as a + /// collection. + /// + /// + /// Do not fetch too many messages at once! This may cause unwanted preemptive rate limit or even actual + /// rate limit, causing your bot to freeze! + /// + /// This method will attempt to fetch the number of messages specified under around + /// the message depending on the . The library will + /// attempt to split up the requests according to your and + /// . In other words, should the user request 500 messages, + /// and the constant is 100, the request will + /// be split into 5 individual requests; thus returning 5 individual asynchronous responses, hence the need + /// of flattening. + /// + /// + /// The following example gets 5 message prior to a specific message, oldMessage. + /// + /// + /// The starting message to get the messages from. + /// The direction of the messages to be gotten from. + /// The numbers of message to be gotten from. + /// The that determines whether the object should be fetched from + /// cache. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// + 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. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation for retrieving pinned messages in this channel. + /// The task result contains a collection of messages found in the pinned messages. + /// + Task> GetPinnedMessagesAsync(RequestOptions options = null); + + /// + /// Deletes a message. + /// + /// The snowflake identifier of the message that would be removed. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous removal operation. + /// + Task DeleteMessageAsync(ulong messageId, RequestOptions options = null); + /// Deletes a message based on the provided message in this channel. + /// The message that would be removed. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous removal operation. + /// + Task DeleteMessageAsync(IMessage message, RequestOptions options = null); + + /// + /// Broadcasts the "user is typing" message to all users in this channel, lasting 10 seconds. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous operation that triggers the broadcast. + /// + Task TriggerTypingAsync(RequestOptions options = null); + /// + /// Continuously broadcasts the "user is typing" message to all users in this channel until the returned + /// object is disposed. + /// + /// + /// The following example keeps the client in the typing state until LongRunningAsync has finished. + /// + /// + /// The options to be used when sending the request. + /// + /// A disposable object that, upon its disposal, will stop the client from broadcasting its typing state in + /// this channel. + /// + IDisposable EnterTypingState(RequestOptions options = null); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/INestedChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/INestedChannel.cs new file mode 100644 index 0000000..e38e1db --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/INestedChannel.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a type of guild channel that can be nested within a category. + /// + public interface INestedChannel : IGuildChannel + { + /// + /// Gets the parent (category) ID of this channel in the guild's channel list. + /// + /// + /// A representing the snowflake identifier of the parent of this channel; + /// null if none is set. + /// + ulong? CategoryId { get; } + /// + /// Gets the parent (category) channel of this channel. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the category channel + /// representing the parent of this channel; null if none is set. + /// + Task GetCategoryAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + + /// + /// Syncs the permissions of this nested channel with its parent's. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous operation for syncing channel permissions with its parent's. + /// + Task SyncPermissionsAsync(RequestOptions options = null); + + /// + /// Creates a new invite to this channel. + /// + /// + /// The following example creates a new invite to this channel; the invite lasts for 12 hours and can only + /// be used 3 times throughout its lifespan. + /// + /// await guildChannel.CreateInviteAsync(maxAge: 43200, maxUses: 3); + /// + /// + /// 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, the user accepting this invite will be kicked from the guild after closing their client. + /// If true, don't try to reuse a similar invite (useful for creating many unique one time use invites). + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous invite creation operation. The task result contains an invite + /// metadata object containing information for the created invite. + /// + Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null); + /// + /// Gets a collection of all invites to this channel. + /// B + /// + /// The following example gets all of the invites that have been created in this channel and selects the + /// most used invite. + /// + /// var invites = await channel.GetInvitesAsync(); + /// if (invites.Count == 0) return; + /// var invite = invites.OrderByDescending(x => x.Uses).FirstOrDefault(); + /// + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of invite metadata that are created for this channel. + /// + Task> GetInvitesAsync(RequestOptions options = null); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/IPrivateChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/IPrivateChannel.cs new file mode 100644 index 0000000..cd2307c --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/IPrivateChannel.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace Discord +{ + /// + /// Represents a generic channel that is private to select recipients. + /// + public interface IPrivateChannel : IChannel + { + /// + /// Gets the users that can access this channel. + /// + /// + /// A read-only collection of users that can access this channel. + /// + IReadOnlyCollection Recipients { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/ITextChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/ITextChannel.cs new file mode 100644 index 0000000..29c764e --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/ITextChannel.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a generic channel in a guild that can send and receive messages. + /// + public interface ITextChannel : IMessageChannel, IMentionable, INestedChannel + { + /// + /// Gets a value that indicates whether the channel is NSFW. + /// + /// + /// true if the channel has the NSFW flag enabled; otherwise false. + /// + bool IsNsfw { get; } + + /// + /// Gets the current topic for this text channel. + /// + /// + /// A string representing the topic set in the channel; null if none is set. + /// + string Topic { get; } + + /// + /// Gets the current slow-mode delay for this channel. + /// + /// + /// An representing the time in seconds required before the user can send another + /// message; 0 if disabled. + /// + int SlowModeInterval { get; } + + /// + /// Bulk-deletes multiple messages. + /// + /// + /// The following example gets 250 messages from the channel and deletes them. + /// + /// var messages = await textChannel.GetMessagesAsync(250).FlattenAsync(); + /// await textChannel.DeleteMessagesAsync(messages); + /// + /// + /// + /// This method attempts to remove the messages specified in bulk. + /// + /// Due to the limitation set by Discord, this method can only remove messages that are posted within 14 days! + /// + /// + /// The messages to be bulk-deleted. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous bulk-removal operation. + /// + Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null); + /// + /// Bulk-deletes multiple messages. + /// + /// + /// This method attempts to remove the messages specified in bulk. + /// + /// Due to the limitation set by Discord, this method can only remove messages that are posted within 14 days! + /// + /// + /// The snowflake identifier of the messages to be bulk-deleted. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous bulk-removal operation. + /// + Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null); + + /// + /// Modifies this text channel. + /// + /// The delegate containing the properties to modify the channel with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + /// + Task ModifyAsync(Action func, RequestOptions options = null); + + /// + /// Creates a webhook in this text channel. + /// + /// The name of the webhook. + /// The avatar of the webhook. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// webhook. + /// + Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null); + /// + /// Gets a webhook available in this text channel. + /// + /// The identifier of the webhook. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a webhook associated + /// with the identifier; null if the webhook is not found. + /// + Task GetWebhookAsync(ulong id, RequestOptions options = null); + /// + /// Gets the webhooks available in this text channel. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of webhooks that is available in this channel. + /// + Task> GetWebhooksAsync(RequestOptions options = null); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs new file mode 100644 index 0000000..9c2d008 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a generic voice channel in a guild. + /// + public interface IVoiceChannel : INestedChannel, IAudioChannel + { + /// + /// Gets the bit-rate that the clients in this voice channel are requested to use. + /// + /// + /// An representing the bit-rate (bps) that this voice channel defines and requests the + /// client(s) to use. + /// + int Bitrate { get; } + /// + /// Gets the max number of users allowed to be connected to this channel at once. + /// + /// + /// An representing the maximum number of users that are allowed to be connected to this + /// channel at once; null if a limit is not set. + /// + int? UserLimit { get; } + + /// + /// Modifies this voice channel. + /// + /// The properties to modify the channel with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + /// + Task ModifyAsync(Action func, RequestOptions options = null); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/ReorderChannelProperties.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/ReorderChannelProperties.cs new file mode 100644 index 0000000..ffd90da --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/ReorderChannelProperties.cs @@ -0,0 +1,32 @@ +namespace Discord +{ + /// + /// Provides properties that are used to reorder an . + /// + public class ReorderChannelProperties + { + /// + /// Gets the ID of the channel to apply this position to. + /// + /// + /// A representing the snowflake identifier of this channel. + /// + public ulong Id { get; } + /// + /// Gets the new zero-based position of this channel. + /// + /// + /// An representing the new position of this channel. + /// + public int Position { get; } + + /// Initializes a new instance of the class used to reorder a channel. + /// Sets the ID of the channel to apply this position to. + /// Sets the new zero-based position of this channel. + public ReorderChannelProperties(ulong id, int position) + { + Id = id; + Position = position; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs new file mode 100644 index 0000000..821f358 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs @@ -0,0 +1,42 @@ +using System; + +namespace Discord +{ + /// + /// Provides properties that are used to modify an with the specified changes. + /// + /// + public class TextChannelProperties : GuildChannelProperties + { + /// + /// Gets or sets the topic of the channel. + /// + /// + /// Setting this value to any string other than null or will set the + /// channel topic or description to the desired value. + /// + public Optional Topic { get; set; } + /// + /// Gets or sets whether this channel should be flagged as NSFW. + /// + /// + /// Setting this value to true will mark the channel as NSFW (Not Safe For Work) and will prompt the + /// user about its possibly mature nature before they may view the channel; setting this value to false will + /// remove the NSFW indicator. + /// + public Optional IsNsfw { get; set; } + /// + /// Gets or sets the slow-mode ratelimit in seconds for this channel. + /// + /// + /// Setting this value to anything above zero will require each user to wait X seconds before + /// sending another message; setting this value to 0 will disable slow-mode for this channel. + /// + /// Users with or + /// will be exempt from slow-mode. + /// + /// + /// Thrown if the value does not fall within [0, 21600]. + public Optional SlowModeInterval { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/VoiceChannelProperties.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/VoiceChannelProperties.cs new file mode 100644 index 0000000..fb4d478 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Channels/VoiceChannelProperties.cs @@ -0,0 +1,17 @@ +namespace Discord +{ + /// + /// Provides properties that are used to modify an with the specified changes. + /// + public class VoiceChannelProperties : GuildChannelProperties + { + /// + /// Gets or sets the bitrate of the voice connections in this channel. Must be greater than 8000. + /// + public Optional Bitrate { get; set; } + /// + /// Gets or sets the maximum number of users that can be present in a channel, or null if none. + /// + public Optional UserLimit { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Emotes/Emoji.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Emotes/Emoji.cs new file mode 100644 index 0000000..d5e7950 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Emotes/Emoji.cs @@ -0,0 +1,47 @@ +namespace Discord +{ + /// + /// A Unicode emoji. + /// + public class Emoji : IEmote + { + // TODO: need to constrain this to Unicode-only emojis somehow + + /// + public string Name { get; } + /// + /// Gets the Unicode representation of this emote. + /// + /// + /// A string that resolves to . + /// + public override string ToString() => Name; + + /// + /// Initializes a new class with the provided Unicode. + /// + /// The pure UTF-8 encoding of an emoji. + public Emoji(string unicode) + { + Name = unicode; + } + + /// + /// Determines whether the specified emoji is equal to the current one. + /// + /// The object to compare with the current object. + public override bool Equals(object other) + { + if (other == null) return false; + if (other == this) return true; + + var otherEmoji = other as Emoji; + if (otherEmoji == null) return false; + + return string.Equals(Name, otherEmoji.Name); + } + + /// + public override int GetHashCode() => Name.GetHashCode(); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Emotes/Emote.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Emotes/Emote.cs new file mode 100644 index 0000000..6054b3f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Emotes/Emote.cs @@ -0,0 +1,106 @@ +using System; +using System.Globalization; +using System.Diagnostics; + +namespace Discord +{ + /// + /// A custom image-based emote. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class Emote : IEmote, ISnowflakeEntity + { + /// + public string Name { get; } + /// + public ulong Id { get; } + /// + /// Gets whether this emote is animated. + /// + /// + /// A boolean that determines whether or not this emote is an animated one. + /// + public bool Animated { get; } + /// + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + /// + /// Gets the image URL of this emote. + /// + /// + /// A string that points to the URL of this emote. + /// + public string Url => CDN.GetEmojiUrl(Id, Animated); + + internal Emote(ulong id, string name, bool animated) + { + Id = id; + Name = name; + Animated = animated; + } + + /// + /// Determines whether the specified emote is equal to the current emote. + /// + /// The object to compare with the current object. + public override bool Equals(object other) + { + if (other == null) return false; + if (other == this) return true; + + var otherEmote = other as Emote; + if (otherEmote == null) return false; + + return Id == otherEmote.Id; + } + + /// + public override int GetHashCode() + => Id.GetHashCode(); + + /// Parses an from its raw format. + /// The raw encoding of an emote (e.g. <:dab:277855270321782784>). + /// An emote. + /// Invalid emote format. + public static Emote Parse(string text) + { + if (TryParse(text, out Emote result)) + return result; + throw new ArgumentException(message: "Invalid emote format.", paramName: nameof(text)); + } + + /// Tries to parse an from its raw format. + /// The raw encoding of an emote; for example, <:dab:277855270321782784>. + /// An emote. + public static bool TryParse(string text, out Emote result) + { + result = null; + if (text.Length >= 4 && text[0] == '<' && (text[1] == ':' || (text[1] == 'a' && text[2] == ':')) && text[text.Length - 1] == '>') + { + bool animated = text[1] == 'a'; + int startIndex = animated ? 3 : 2; + + int splitIndex = text.IndexOf(':', startIndex); + if (splitIndex == -1) + return false; + + if (!ulong.TryParse(text.Substring(splitIndex + 1, text.Length - splitIndex - 2), NumberStyles.None, CultureInfo.InvariantCulture, out ulong id)) + return false; + + string name = text.Substring(startIndex, splitIndex - startIndex); + result = new Emote(id, name, animated); + return true; + } + return false; + + } + + private string DebuggerDisplay => $"{Name} ({Id})"; + /// + /// Returns the raw representation of the emote. + /// + /// + /// A string representing the raw presentation of the emote (e.g. <:thonkang:282745590985523200>). + /// + public override string ToString() => $"<{(Animated ? "a" : "")}:{Name}:{Id}>"; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Emotes/EmoteProperties.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Emotes/EmoteProperties.cs new file mode 100644 index 0000000..41679d2 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Emotes/EmoteProperties.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace Discord +{ + /// + /// Provides properties that are used to modify an with the specified changes. + /// + /// + public class EmoteProperties + { + /// + /// Gets or sets the name of the . + /// + public Optional Name { get; set; } + /// + /// Gets or sets the roles that can access this . + /// + public Optional> Roles { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Emotes/GuildEmote.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Emotes/GuildEmote.cs new file mode 100644 index 0000000..4bd0845 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Emotes/GuildEmote.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Diagnostics; + +namespace Discord +{ + /// + /// An image-based emote that is attached to a guild. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class GuildEmote : Emote + { + /// + /// Gets whether this emoji is managed by an integration. + /// + /// + /// A boolean that determines whether or not this emote is managed by a Twitch integration. + /// + public bool IsManaged { get; } + /// + /// Gets whether this emoji must be wrapped in colons. + /// + /// + /// A boolean that determines whether or not this emote requires the use of colons in chat to be used. + /// + public bool RequireColons { get; } + /// + /// Gets the roles that are allowed to use this emoji. + /// + /// + /// A read-only list containing snowflake identifiers for roles that are allowed to use this emoji. + /// + public IReadOnlyList RoleIds { get; } + /// + /// Gets the user ID associated with the creation of this emoji. + /// + /// + /// An snowflake identifier representing the user who created this emoji; + /// null if unknown. + /// + public ulong? CreatorId { get; } + + internal GuildEmote(ulong id, string name, bool animated, bool isManaged, bool requireColons, IReadOnlyList roleIds, ulong? userId) : base(id, name, animated) + { + IsManaged = isManaged; + RequireColons = requireColons; + RoleIds = roleIds; + CreatorId = userId; + } + + private string DebuggerDisplay => $"{Name} ({Id})"; + /// + /// Gets the raw representation of the emote. + /// + /// + /// A string representing the raw presentation of the emote (e.g. <:thonkang:282745590985523200>). + /// + public override string ToString() => $"<{(Animated ? "a" : "")}:{Name}:{Id}>"; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Emotes/IEmote.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Emotes/IEmote.cs new file mode 100644 index 0000000..9141e85 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Emotes/IEmote.cs @@ -0,0 +1,16 @@ +namespace Discord +{ + /// + /// Represents a general container for any type of emote in a message. + /// + public interface IEmote + { + /// + /// Gets the display name or Unicode representation of this emote. + /// + /// + /// A string representing the display name or the Unicode representation (e.g. 🤔) of this emote. + /// + string Name { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/DefaultMessageNotifications.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/DefaultMessageNotifications.cs new file mode 100644 index 0000000..ffcd28c --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/DefaultMessageNotifications.cs @@ -0,0 +1,17 @@ +namespace Discord +{ + /// + /// Specifies the default message notification behavior the guild uses. + /// + public enum DefaultMessageNotifications + { + /// + /// By default, all messages will trigger notifications. + /// + AllMessages = 0, + /// + /// By default, only mentions will trigger notifications. + /// + MentionsOnly = 1 + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/ExplicitContentFilterLevel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/ExplicitContentFilterLevel.cs new file mode 100644 index 0000000..54c0bda --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/ExplicitContentFilterLevel.cs @@ -0,0 +1,13 @@ +namespace Discord +{ + public enum ExplicitContentFilterLevel + { + /// No messages will be scanned. + Disabled = 0, + /// Scans messages from all guild members that do not have a role. + /// Recommented option for servers that use roles for trusted membership. + MembersWithoutRoles = 1, + /// Scan messages sent by all guild members. + AllMembers = 2 + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs new file mode 100644 index 0000000..34473e9 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs @@ -0,0 +1,21 @@ +namespace Discord +{ + /// + /// Provides properties that are used to modify the widget of an with the specified changes. + /// + public class GuildEmbedProperties + { + /// + /// Sets whether the widget should be enabled. + /// + public Optional Enabled { get; set; } + /// + /// Sets the channel that the invite should place its users in, if not null. + /// + public Optional Channel { get; set; } + /// + /// Sets the channel the invite should place its users in, if not null. + /// + public Optional ChannelId { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/GuildIntegrationProperties.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/GuildIntegrationProperties.cs new file mode 100644 index 0000000..2ca19b5 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/GuildIntegrationProperties.cs @@ -0,0 +1,21 @@ +namespace Discord +{ + /// + /// Provides properties used to modify an with the specified changes. + /// + public class GuildIntegrationProperties + { + /// + /// Gets or sets the behavior when an integration subscription lapses. + /// + public Optional ExpireBehavior { get; set; } + /// + /// Gets or sets the period (in seconds) where the integration will ignore lapsed subscriptions. + /// + public Optional ExpireGracePeriod { get; set; } + /// + /// Gets or sets whether emoticons should be synced for this integration. + /// + public Optional EnableEmoticons { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/GuildProperties.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/GuildProperties.cs new file mode 100644 index 0000000..981e119 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/GuildProperties.cs @@ -0,0 +1,112 @@ +using System.Globalization; + +namespace Discord +{ + /// + /// Provides properties that are used to modify an with the specified changes. + /// + /// + public class GuildProperties + { + /// + /// Gets or sets the name of the guild. Must be within 100 characters. + /// + public Optional Name { get; set; } + /// + /// Gets or sets the region for the guild's voice connections. + /// + public Optional Region { get; set; } + /// + /// Gets or sets the ID of the region for the guild's voice connections. + /// + public Optional RegionId { get; set; } + /// + /// Gets or sets the verification level new users need to achieve before speaking. + /// + public Optional VerificationLevel { get; set; } + /// + /// Gets or sets the default message notification state for the guild. + /// + public Optional DefaultMessageNotifications { get; set; } + /// + /// Gets or sets how many seconds before a user is sent to AFK. This value MUST be one of: (60, 300, 900, + /// 1800, 3600). + /// + public Optional AfkTimeout { get; set; } + /// + /// Gets or sets the icon of the guild. + /// + public Optional Icon { get; set; } + /// + /// Gets or sets the banner of the guild. + /// + public Optional Banner { get; set; } + /// + /// Gets or sets the guild's splash image. + /// + /// + /// The guild must be partnered for this value to have any effect. + /// + public Optional Splash { get; set; } + /// + /// Gets or sets the where AFK users should be sent. + /// + public Optional AfkChannel { get; set; } + /// + /// Gets or sets the ID of the where AFK users should be sent. + /// + public Optional AfkChannelId { get; set; } + /// + /// Gets or sets the where system messages should be sent. + /// + public Optional SystemChannel { get; set; } + /// + /// Gets or sets the ID of the where system messages should be sent. + /// + public Optional SystemChannelId { get; set; } + /// + /// Gets or sets the owner of this guild. + /// + public Optional Owner { get; set; } + /// + /// Gets or sets the ID of the owner of this guild. + /// + public Optional OwnerId { get; set; } + /// + /// Gets or sets the explicit content filter level of this guild. + /// + public Optional ExplicitContentFilter { get; set; } + /// + /// Gets or sets the flags that DISABLE types of system channel messages. + /// + /// + /// These flags are inverted. Setting a flag will disable that system channel message from being sent. + /// A value of will allow all system channel message types to be sent, + /// given that the has also been set. + /// A value of will deny guild boost messages from being sent, and allow all + /// other types of messages. + /// Refer to the extension methods and + /// to check if these system channel message types + /// are enabled, without the need to manipulate the logic of the flag. + /// + public Optional SystemChannelFlags { get; set; } + /// + /// Gets or sets the preferred locale of the guild in IETF BCP 47 language tag format. + /// + /// + /// This property takes precedence over . + /// When it is set, the value of + /// will not be used. + /// + public Optional PreferredLocale { get; set; } + /// + /// Gets or sets the preferred locale of the guild. + /// + /// + /// The property takes precedence + /// over this property. When is set, + /// the value of will be unused. + /// + public Optional PreferredCulture { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/IBan.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/IBan.cs new file mode 100644 index 0000000..617f2fe --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/IBan.cs @@ -0,0 +1,23 @@ +namespace Discord +{ + /// + /// Represents a generic ban object. + /// + public interface IBan + { + /// + /// Gets the banned user. + /// + /// + /// A user that was banned. + /// + IUser User { get; } + /// + /// Gets the reason why the user is banned if specified. + /// + /// + /// A string containing the reason behind the ban; null if none is specified. + /// + string Reason { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/IGuild.cs new file mode 100644 index 0000000..a18e91b --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -0,0 +1,791 @@ +using Discord.Audio; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a generic guild/server. + /// + public interface IGuild : IDeletable, ISnowflakeEntity + { + /// + /// Gets the name of this guild. + /// + /// + /// A string containing the name of this guild. + /// + string Name { get; } + /// + /// Gets the amount of time (in seconds) a user must be inactive in a voice channel for until they are + /// automatically moved to the AFK voice channel. + /// + /// + /// An representing the amount of time in seconds for a user to be marked as inactive + /// and moved into the AFK voice channel. + /// + int AFKTimeout { get; } + /// + /// Gets a value that indicates whether this guild is embeddable (i.e. can use widget). + /// + /// + /// true if this guild can be embedded via widgets; otherwise false. + /// + bool IsEmbeddable { get; } + /// + /// Gets the default message notifications for users who haven't explicitly set their notification settings. + /// + DefaultMessageNotifications DefaultMessageNotifications { get; } + /// + /// Gets the level of Multi-Factor Authentication requirements a user must fulfill before being allowed to + /// perform administrative actions in this guild. + /// + /// + /// The level of MFA requirement. + /// + MfaLevel MfaLevel { get; } + /// + /// Gets the level of requirements a user must fulfill before being allowed to post messages in this guild. + /// + /// + /// The level of requirements. + /// + VerificationLevel VerificationLevel { get; } + /// + /// Gets the level of content filtering applied to user's content in a Guild. + /// + /// + /// The level of explicit content filtering. + /// + ExplicitContentFilterLevel ExplicitContentFilter { get; } + /// + /// Gets the ID of this guild's icon. + /// + /// + /// An identifier for the splash image; null if none is set. + /// + string IconId { get; } + /// + /// Gets the URL of this guild's icon. + /// + /// + /// A URL pointing to the guild's icon; null if none is set. + /// + string IconUrl { get; } + /// + /// Gets the ID of this guild's splash image. + /// + /// + /// An identifier for the splash image; null if none is set. + /// + string SplashId { get; } + /// + /// Gets the URL of this guild's splash image. + /// + /// + /// A URL pointing to the guild's splash image; null if none is set. + /// + string SplashUrl { get; } + /// + /// Determines if this guild is currently connected and ready to be used. + /// + /// + /// + /// This property only applies to a WebSocket-based client. + /// + /// This boolean is used to determine if the guild is currently connected to the WebSocket and is ready to be used/accessed. + /// + /// + /// true if this guild is currently connected and ready to be used; otherwise false. + /// + bool Available { get; } + + /// + /// Gets the ID of the AFK voice channel for this guild. + /// + /// + /// A representing the snowflake identifier of the AFK voice channel; null if + /// none is set. + /// + ulong? AFKChannelId { get; } + /// + /// Gets the ID of the default channel for this guild. + /// + /// + /// This property retrieves the snowflake identifier of the first viewable text channel for this guild. + /// + /// This channel does not guarantee the user can send message to it, as it only looks for the first viewable + /// text channel. + /// + /// + /// + /// A representing the snowflake identifier of the default text channel; 0 if + /// none can be found. + /// + ulong DefaultChannelId { get; } + /// + /// Gets the ID of the widget embed channel of this guild. + /// + /// + /// A representing the snowflake identifier of the embedded channel found within the + /// widget settings of this guild; null if none is set. + /// + ulong? EmbedChannelId { get; } + /// + /// Gets the ID of the channel where randomized welcome messages are sent. + /// + /// + /// A representing the snowflake identifier of the system channel where randomized + /// welcome messages are sent; null if none is set. + /// + ulong? SystemChannelId { get; } + /// + /// Gets the ID of the user that owns this guild. + /// + /// + /// A representing the snowflake identifier of the user that owns this guild. + /// + ulong OwnerId { get; } + /// + /// Gets the application ID of the guild creator if it is bot-created. + /// + /// + /// A representing the snowflake identifier of the application ID that created this guild, or null if it was not bot-created. + /// + ulong? ApplicationId { get; } + /// + /// Gets the ID of the region hosting this guild's voice channels. + /// + /// + /// A string containing the identifier for the voice region that this guild uses (e.g. eu-central). + /// + string VoiceRegionId { get; } + /// + /// Gets the currently associated with this guild. + /// + /// + /// An currently associated with this guild. + /// + IAudioClient AudioClient { get; } + /// + /// Gets the built-in role containing all users in this guild. + /// + /// + /// A role object that represents an @everyone role in this guild. + /// + IRole EveryoneRole { get; } + /// + /// Gets a collection of all custom emotes for this guild. + /// + /// + /// A read-only collection of all custom emotes for this guild. + /// + IReadOnlyCollection Emotes { get; } + /// + /// Gets a collection of all extra features added to this guild. + /// + /// + /// A read-only collection of enabled features in this guild. + /// + IReadOnlyCollection Features { get; } + /// + /// Gets a collection of all roles in this guild. + /// + /// + /// A read-only collection of roles found within this guild. + /// + IReadOnlyCollection Roles { get; } + /// + /// Gets the tier of guild boosting in this guild. + /// + /// + /// The tier of guild boosting in this guild. + /// + PremiumTier PremiumTier { get; } + /// + /// Gets the identifier for this guilds banner image. + /// + /// + /// An identifier for the banner image; null if none is set. + /// + string BannerId { get; } + /// + /// Gets the URL of this guild's banner image. + /// + /// + /// A URL pointing to the guild's banner image; null if none is set. + /// + string BannerUrl { get; } + /// + /// Gets the code for this guild's vanity invite URL. + /// + /// + /// A string containing the vanity invite code for this guild; null if none is set. + /// + string VanityURLCode { get; } + /// + /// Gets the flags for the types of system channel messages that are disabled. + /// + /// + /// The flags for the types of system channel messages that are disabled. + /// + SystemChannelMessageDeny SystemChannelFlags { get; } + /// + /// Gets the description for the guild. + /// + /// + /// The description for the guild; null if none is set. + /// + string Description { get; } + /// + /// Gets the number of premium subscribers of this guild. + /// + /// + /// This is the number of users who have boosted this guild. + /// + /// + /// The number of premium subscribers of this guild. + /// + int PremiumSubscriptionCount { get; } + + /// + /// Gets the preferred locale of this guild in IETF BCP 47 + /// language tag format. + /// + /// + /// The preferred locale of the guild in IETF BCP 47 + /// language tag format. + /// + string PreferredLocale { get; } + + /// + /// Gets the preferred culture of this guild. + /// + /// + /// The preferred culture information of this guild. + /// + CultureInfo PreferredCulture { get; } + + /// + /// Modifies this guild. + /// + /// The delegate containing the properties to modify the guild with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + Task ModifyAsync(Action func, RequestOptions options = null); + /// + /// Modifies this guild's embed channel. + /// + /// The delegate containing the properties to modify the guild widget with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + Task ModifyEmbedAsync(Action func, RequestOptions options = null); + /// + /// Bulk-modifies the order of channels in this guild. + /// + /// The properties used to modify the channel positions with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous reorder operation. + /// + Task ReorderChannelsAsync(IEnumerable args, RequestOptions options = null); + /// + /// Bulk-modifies the order of roles in this guild. + /// + /// The properties used to modify the role positions with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous reorder operation. + /// + Task ReorderRolesAsync(IEnumerable args, RequestOptions options = null); + /// + /// Leaves this guild. + /// + /// + /// This method will make the currently logged-in user leave the guild. + /// + /// If the user is the owner of this guild, use instead. + /// + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous leave operation. + /// + Task LeaveAsync(RequestOptions options = null); + + /// + /// Gets a collection of all users banned in this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// ban objects that this guild currently possesses, with each object containing the user banned and reason + /// behind the ban. + /// + Task> GetBansAsync(RequestOptions options = null); + /// + /// Gets a ban object for a banned user. + /// + /// The banned user. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a ban object, which + /// contains the user information and the reason for the ban; null if the ban entry cannot be found. + /// + Task GetBanAsync(IUser user, RequestOptions options = null); + /// + /// Gets a ban object for a banned user. + /// + /// The snowflake identifier for the banned user. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a ban object, which + /// contains the user information and the reason for the ban; null if the ban entry cannot be found. + /// + Task GetBanAsync(ulong userId, RequestOptions options = null); + /// + /// Bans the user from this guild and optionally prunes their recent messages. + /// + /// The user to ban. + /// The number of days to remove messages from this user for, and this number must be between [0, 7]. + /// The reason of the ban to be written in the audit log. + /// The options to be used when sending the request. + /// is not between 0 to 7. + /// + /// A task that represents the asynchronous add operation for the ban. + /// + Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null); + /// + /// Bans the user from this guild and optionally prunes their recent messages. + /// + /// The snowflake ID of the user to ban. + /// The number of days to remove messages from this user for, and this number must be between [0, 7]. + /// The reason of the ban to be written in the audit log. + /// The options to be used when sending the request. + /// is not between 0 to 7. + /// + /// A task that represents the asynchronous add operation for the ban. + /// + Task AddBanAsync(ulong userId, int pruneDays = 0, string reason = null, RequestOptions options = null); + /// + /// Unbans the user if they are currently banned. + /// + /// The user to be unbanned. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous removal operation for the ban. + /// + Task RemoveBanAsync(IUser user, RequestOptions options = null); + /// + /// Unbans the user if they are currently banned. + /// + /// The snowflake identifier of the user to be unbanned. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous removal operation for the ban. + /// + Task RemoveBanAsync(ulong userId, RequestOptions options = null); + + /// + /// Gets a collection of all channels in this guild. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// generic channels found within this guild. + /// + Task> GetChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a channel in this guild. + /// + /// The snowflake identifier for the channel. + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the generic channel + /// associated with the specified ; null if none is found. + /// + Task GetChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a collection of all text channels in this guild. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// message channels found within this guild. + /// + Task> GetTextChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a text channel in this guild. + /// + /// The snowflake identifier for the text channel. + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the text channel + /// associated with the specified ; null if none is found. + /// + Task GetTextChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a collection of all voice channels in this guild. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// voice channels found within this guild. + /// + Task> GetVoiceChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a collection of all category channels in this guild. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// category channels found within this guild. + /// + Task> GetCategoriesAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a voice channel in this guild. + /// + /// The snowflake identifier for the voice channel. + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the voice channel associated + /// with the specified ; null if none is found. + /// + Task GetVoiceChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets the AFK voice channel in this guild. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the voice channel that the + /// AFK users will be moved to after they have idled for too long; null if none is set. + /// + Task GetAFKChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets the system channel where randomized welcome messages are sent in this guild. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the text channel where + /// randomized welcome messages will be sent to; null if none is set. + /// + Task GetSystemChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets the first viewable text channel in this guild. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the first viewable text + /// channel in this guild; null if none is found. + /// + Task GetDefaultChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets the embed channel (i.e. the channel set in the guild's widget settings) in this guild. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the embed channel set + /// within the server's widget settings; null if none is set. + /// + Task GetEmbedChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + + /// + /// Creates a new text channel in this guild. + /// + /// + /// The following example creates a new text channel under an existing category named Wumpus with a set topic. + /// + /// + /// The new name for the text channel. + /// The delegate containing the properties to be applied to the channel upon its creation. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// text channel. + /// + Task CreateTextChannelAsync(string name, Action func = null, RequestOptions options = null); + /// + /// Creates a new voice channel in this guild. + /// + /// The new name for the voice channel. + /// The delegate containing the properties to be applied to the channel upon its creation. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// voice channel. + /// + Task CreateVoiceChannelAsync(string name, Action func = null, RequestOptions options = null); + /// + /// Creates a new channel category in this guild. + /// + /// The new name for the category. + /// The delegate containing the properties to be applied to the channel upon its creation. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// category channel. + /// + Task CreateCategoryAsync(string name, Action func = null, RequestOptions options = null); + + /// + /// Gets a collection of all the voice regions this guild can access. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// voice regions the guild can access. + /// + Task> GetVoiceRegionsAsync(RequestOptions options = null); + + Task> GetIntegrationsAsync(RequestOptions options = null); + Task CreateIntegrationAsync(ulong id, string type, RequestOptions options = null); + + /// + /// Gets a collection of all invites in this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// invite metadata, each representing information for an invite found within this guild. + /// + Task> GetInvitesAsync(RequestOptions options = null); + /// + /// Gets the vanity invite URL of this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the partial metadata of + /// the vanity invite found within this guild; null if none is found. + /// + Task GetVanityInviteAsync(RequestOptions options = null); + + /// + /// Gets a role in this guild. + /// + /// The snowflake identifier for the role. + /// + /// A role that is associated with the specified ; null if none is found. + /// + IRole GetRole(ulong id); + /// + /// Creates a new role with the provided name. + /// + /// The new name for the role. + /// The guild permission that the role should possess. + /// The color of the role. + /// Whether the role is separated from others on the sidebar. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// role. + /// + Task CreateRoleAsync(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false, RequestOptions options = null); + // TODO remove CreateRoleAsync overload that does not have isMentionable when breaking change is acceptable + /// + /// Creates a new role with the provided name. + /// + /// The new name for the role. + /// The guild permission that the role should possess. + /// The color of the role. + /// Whether the role is separated from others on the sidebar. + /// Whether the role can be mentioned. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// role. + /// + Task CreateRoleAsync(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false, bool isMentionable = false, RequestOptions options = null); + + /// + /// Adds a user to this guild. + /// + /// + /// This method requires you have an OAuth2 access token for the user, requested with the guilds.join scope, and that the bot have the MANAGE_INVITES permission in the guild. + /// + /// The snowflake identifier of the user. + /// The OAuth2 access token for the user, requested with the guilds.join scope. + /// The delegate containing the properties to be applied to the user upon being added to the guild. + /// The options to be used when sending the request. + /// A guild user associated with the specified ; null if the user is already in the guild. + Task AddGuildUserAsync(ulong userId, string accessToken, Action func = null, RequestOptions options = null); + /// + /// Gets a collection of all users in this guild. + /// + /// + /// This method retrieves all users found within this guild. + /// + /// This may return an incomplete collection in the WebSocket implementation due to how Discord does not + /// send a complete user list for large guilds. + /// + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a collection of guild + /// users found within this guild. + /// + Task> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a user from this guild. + /// + /// + /// This method retrieves a user found within this guild. + /// + /// This may return null in the WebSocket implementation due to incomplete user collection in + /// large guilds. + /// + /// + /// The snowflake identifier of the user. + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the guild user + /// associated with the specified ; null if none is found. + /// + Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets the current user for this guild. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the currently logged-in + /// user within this guild. + /// + Task GetCurrentUserAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets the owner of this guild. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the owner of this guild. + /// + Task GetOwnerAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Downloads all users for this guild if the current list is incomplete. + /// + /// + /// A task that represents the asynchronous download operation. + /// + Task DownloadUsersAsync(); + /// + /// Prunes inactive users. + /// + /// + /// + /// This method removes all users that have not logged on in the provided number of . + /// + /// + /// If is true, this method will only return the number of users that + /// would be removed without kicking the users. + /// + /// + /// The number of days required for the users to be kicked. + /// Whether this prune action is a simulation. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous prune operation. The task result contains the number of users to + /// be or has been removed from this guild. + /// + Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null); + + /// + /// Gets the specified number of audit log entries for this guild. + /// + /// The number of audit log entries to fetch. + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// The audit log entry ID to get entries before. + /// The type of actions to filter. + /// The user ID to filter entries for. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of the requested audit log entries. + /// + Task> GetAuditLogsAsync(int limit = DiscordConfig.MaxAuditLogEntriesPerBatch, + CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null, ulong? beforeId = null, ulong? userId = null, + ActionType? actionType = null); + + /// + /// Gets a webhook found within this guild. + /// + /// The identifier for the webhook. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the webhook with the + /// specified ; null if none is found. + /// + Task GetWebhookAsync(ulong id, RequestOptions options = null); + /// + /// Gets a collection of all webhook from this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of webhooks found within the guild. + /// + Task> GetWebhooksAsync(RequestOptions options = null); + + /// + /// Gets a specific emote from this guild. + /// + /// The snowflake identifier for the guild emote. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the emote found with the + /// specified ; null if none is found. + /// + Task GetEmoteAsync(ulong id, RequestOptions options = null); + /// + /// Creates a new in this guild. + /// + /// The name of the guild emote. + /// The image of the new emote. + /// The roles to limit the emote usage to. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the created emote. + /// + Task CreateEmoteAsync(string name, Image image, Optional> roles = default(Optional>), RequestOptions options = null); + + /// + /// Modifies an existing in this guild. + /// + /// The emote to be modified. + /// The delegate containing the properties to modify the emote with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. The task result contains the modified + /// emote. + /// + Task ModifyEmoteAsync(GuildEmote emote, Action func, RequestOptions options = null); + /// + /// Deletes an existing from this guild. + /// + /// The emote to delete. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous removal operation. + /// + Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/IGuildIntegration.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/IGuildIntegration.cs new file mode 100644 index 0000000..6fe3f7b --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/IGuildIntegration.cs @@ -0,0 +1,74 @@ +using System; + +namespace Discord +{ + /// + /// Holds information for a guild integration feature. + /// + public interface IGuildIntegration + { + /// + /// Gets the integration ID. + /// + /// + /// An representing the unique identifier value of this integration. + /// + ulong Id { get; } + /// + /// Gets the integration name. + /// + /// + /// A string containing the name of this integration. + /// + string Name { get; } + /// + /// Gets the integration type (Twitch, YouTube, etc). + /// + /// + /// A string containing the name of the type of integration. + /// + string Type { get; } + /// + /// Gets a value that indicates whether this integration is enabled or not. + /// + /// + /// true if this integration is enabled; otherwise false. + /// + bool IsEnabled { get; } + /// + /// Gets a value that indicates whether this integration is syncing or not. + /// + /// + /// An integration with syncing enabled will update its "subscribers" on an interval, while one with syncing + /// disabled will not. A user must manually choose when sync the integration if syncing is disabled. + /// + /// + /// true if this integration is syncing; otherwise false. + /// + bool IsSyncing { get; } + /// + /// Gets the ID that this integration uses for "subscribers". + /// + ulong ExpireBehavior { get; } + /// + /// Gets the grace period before expiring "subscribers". + /// + ulong ExpireGracePeriod { get; } + /// + /// Gets when this integration was last synced. + /// + /// + /// A containing a date and time of day when the integration was last synced. + /// + DateTimeOffset SyncedAt { get; } + /// + /// Gets integration account information. + /// + IntegrationAccount Account { get; } + + IGuild Guild { get; } + ulong GuildId { get; } + ulong RoleId { get; } + IUser User { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/IUserGuild.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/IUserGuild.cs new file mode 100644 index 0000000..b6685ed --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/IUserGuild.cs @@ -0,0 +1,22 @@ +namespace Discord +{ + public interface IUserGuild : IDeletable, ISnowflakeEntity + { + /// + /// Gets the name of this guild. + /// + string Name { get; } + /// + /// Gets the icon URL associated with this guild, or null if one is not set. + /// + string IconUrl { get; } + /// + /// Returns true if the current user owns this guild. + /// + bool IsOwner { get; } + /// + /// Returns the current user's permissions for this guild. + /// + GuildPermissions Permissions { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/IVoiceRegion.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/IVoiceRegion.cs new file mode 100644 index 0000000..9cef849 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/IVoiceRegion.cs @@ -0,0 +1,51 @@ +namespace Discord +{ + /// + /// Represents a region of which the user connects to when using voice. + /// + public interface IVoiceRegion + { + /// + /// Gets the unique identifier for this voice region. + /// + /// + /// A string that represents the identifier for this voice region (e.g. eu-central). + /// + string Id { get; } + /// + /// Gets the name of this voice region. + /// + /// + /// A string that represents the human-readable name of this voice region (e.g. Central Europe). + /// + string Name { get; } + /// + /// Gets a value that indicates whether or not this voice region is exclusive to partnered servers. + /// + /// + /// true if this voice region is exclusive to VIP accounts; otherwise false. + /// + bool IsVip { get; } + /// + /// Gets a value that indicates whether this voice region is optimal for your client in terms of latency. + /// + /// + /// true if this voice region is the closest to your machine; otherwise false . + /// + bool IsOptimal { get; } + /// + /// Gets a value that indicates whether this voice region is no longer being maintained. + /// + /// + /// true if this is a deprecated voice region; otherwise false. + /// + bool IsDeprecated { get; } + /// + /// Gets a value that indicates whether this voice region is custom-made for events. + /// + /// + /// true if this is a custom voice region (used for events/etc); otherwise false/ + /// + bool IsCustom { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/IntegrationAccount.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/IntegrationAccount.cs new file mode 100644 index 0000000..340115f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/IntegrationAccount.cs @@ -0,0 +1,18 @@ +using System.Diagnostics; + +namespace Discord +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public struct IntegrationAccount + { + /// Gets the ID of the account. + /// A unique identifier of this integration account. + public string Id { get; } + /// Gets the name of the account. + /// A string containing the name of this integration account. + public string Name { get; private set; } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id})"; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/MfaLevel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/MfaLevel.cs new file mode 100644 index 0000000..57edac2 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/MfaLevel.cs @@ -0,0 +1,17 @@ +namespace Discord +{ + /// + /// Specifies the guild's Multi-Factor Authentication (MFA) level requirement. + /// + public enum MfaLevel + { + /// + /// Users have no additional MFA restriction on this guild. + /// + Disabled = 0, + /// + /// Users must have MFA enabled on their account to perform administrative actions. + /// + Enabled = 1 + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/PermissionTarget.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/PermissionTarget.cs new file mode 100644 index 0000000..3da2fb1 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/PermissionTarget.cs @@ -0,0 +1,17 @@ +namespace Discord +{ + /// + /// Specifies the target of the permission. + /// + public enum PermissionTarget + { + /// + /// The target of the permission is a role. + /// + Role, + /// + /// The target of the permission is a user. + /// + User + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/PremiumTier.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/PremiumTier.cs new file mode 100644 index 0000000..b7e4c93 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/PremiumTier.cs @@ -0,0 +1,22 @@ +namespace Discord +{ + public enum PremiumTier + { + /// + /// Used for guilds that have no guild boosts. + /// + None = 0, + /// + /// Used for guilds that have Tier 1 guild boosts. + /// + Tier1 = 1, + /// + /// Used for guilds that have Tier 2 guild boosts. + /// + Tier2 = 2, + /// + /// Used for guilds that have Tier 3 guild boosts. + /// + Tier3 = 3 + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/SystemChannelMessageDeny.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/SystemChannelMessageDeny.cs new file mode 100644 index 0000000..3f69693 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/SystemChannelMessageDeny.cs @@ -0,0 +1,22 @@ +using System; + +namespace Discord +{ + [Flags] + public enum SystemChannelMessageDeny + { + /// + /// Deny none of the system channel messages. + /// This will enable all of the system channel messages. + /// + None = 0, + /// + /// Deny the messages that are sent when a user joins the guild. + /// + WelcomeMessage = 0b1, + /// + /// Deny the messages that are sent when a user boosts the guild. + /// + GuildBoost = 0b10 + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/VerificationLevel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/VerificationLevel.cs new file mode 100644 index 0000000..3a5ae04 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Guilds/VerificationLevel.cs @@ -0,0 +1,29 @@ +namespace Discord +{ + /// + /// Specifies the verification level the guild uses. + /// + public enum VerificationLevel + { + /// + /// Users have no additional restrictions on sending messages to this guild. + /// + None = 0, + /// + /// Users must have a verified email on their account. + /// + Low = 1, + /// + /// Users must fulfill the requirements of Low and be registered on Discord for at least 5 minutes. + /// + Medium = 2, + /// + /// Users must fulfill the requirements of Medium and be a member of this guild for at least 10 minutes. + /// + High = 3, + /// + /// Users must fulfill the requirements of High and must have a verified phone on their Discord account. + /// + Extreme = 4 + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/IApplication.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/IApplication.cs new file mode 100644 index 0000000..78a87dc --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/IApplication.cs @@ -0,0 +1,31 @@ +namespace Discord +{ + /// + /// Represents a Discord application created via the developer portal. + /// + public interface IApplication : ISnowflakeEntity + { + /// + /// Gets the name of the application. + /// + string Name { get; } + /// + /// Gets the description of the application. + /// + string Description { get; } + /// + /// Gets the RPC origins of the application. + /// + string[] RPCOrigins { get; } + ulong Flags { get; } + /// + /// Gets the icon URL of the application. + /// + string IconUrl { get; } + + /// + /// Gets the partial user object containing info on the owner of the application. + /// + IUser Owner { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/IDeletable.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/IDeletable.cs new file mode 100644 index 0000000..9696eb8 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/IDeletable.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Determines whether the object is deletable or not. + /// + public interface IDeletable + { + /// + /// Deletes this object and all its children. + /// + /// The options to be used when sending the request. + Task DeleteAsync(RequestOptions options = null); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/IEntity.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/IEntity.cs new file mode 100644 index 0000000..0cd692a --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/IEntity.cs @@ -0,0 +1,17 @@ +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/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/IMentionable.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/IMentionable.cs new file mode 100644 index 0000000..2258067 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/IMentionable.cs @@ -0,0 +1,16 @@ +namespace Discord +{ + /// + /// Determines whether the object is mentionable or not. + /// + public interface IMentionable + { + /// + /// Returns a special string used to mention this object. + /// + /// + /// A string that is recognized by Discord as a mention (e.g. <@168693960628371456>). + /// + string Mention { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/ISnowflakeEntity.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/ISnowflakeEntity.cs new file mode 100644 index 0000000..6f2c751 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/ISnowflakeEntity.cs @@ -0,0 +1,16 @@ +using System; + +namespace Discord +{ + /// Represents a Discord snowflake entity. + public interface ISnowflakeEntity : IEntity + { + /// + /// Gets when the snowflake was created. + /// + /// + /// A representing when the entity was first created. + /// + DateTimeOffset CreatedAt { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/IUpdateable.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/IUpdateable.cs new file mode 100644 index 0000000..3ae4613 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/IUpdateable.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Defines whether the object is updateable or not. + /// + public interface IUpdateable + { + /// + /// Updates this object's properties with its current state. + /// + Task UpdateAsync(RequestOptions options = null); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Image.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Image.cs new file mode 100644 index 0000000..3f5a01f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Image.cs @@ -0,0 +1,79 @@ +using System; +using System.IO; + +namespace Discord +{ + /// + /// An image that will be uploaded to Discord. + /// + public struct Image : IDisposable + { + private bool _isDisposed; + + /// + /// Gets the stream to be uploaded to Discord. + /// +#pragma warning disable IDISP008 + public Stream Stream { get; } +#pragma warning restore IDISP008 + /// + /// Create the image with a . + /// + /// + /// The to create the image with. Note that this must be some type of stream + /// with the contents of a file in it. + /// + public Image(Stream stream) + { + _isDisposed = false; + Stream = stream; + } + + /// + /// Create the image from a file path. + /// + /// + /// This file path is NOT validated and is passed directly into a + /// . + /// + /// The path to the file. + /// + /// is a zero-length string, contains only white space, or contains one or more invalid + /// characters as defined by . + /// + /// is null. + /// + /// The specified path, file name, or both exceed the system-defined maximum length. For example, on + /// Windows-based platforms, paths must be less than 248 characters, and file names must be less than 260 + /// characters. + /// + /// is in an invalid format. + /// + /// The specified is invalid, (for example, it is on an unmapped drive). + /// + /// + /// specified a directory.-or- The caller does not have the required permission. + /// + /// The file specified in was not found. + /// + /// An I/O error occurred while opening the file. + public Image(string path) + { + _isDisposed = false; + Stream = File.OpenRead(path); + } + + /// + public void Dispose() + { + if (!_isDisposed) + { +#pragma warning disable IDISP007 + Stream?.Dispose(); +#pragma warning restore IDISP007 + + _isDisposed = true; + } + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/ImageFormat.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/ImageFormat.cs new file mode 100644 index 0000000..9c04328 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/ImageFormat.cs @@ -0,0 +1,29 @@ +namespace Discord +{ + /// + /// Specifies the type of format the image should return in. + /// + public enum ImageFormat + { + /// + /// Use automatically detected format. + /// + Auto, + /// + /// Use Google's WebP image format. + /// + WebP, + /// + /// Use PNG. + /// + Png, + /// + /// Use JPEG. + /// + Jpeg, + /// + /// Use GIF. + /// + Gif, + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Invites/IInvite.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Invites/IInvite.cs new file mode 100644 index 0000000..993f1f0 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Invites/IInvite.cs @@ -0,0 +1,87 @@ +namespace Discord +{ + /// + /// Represents a generic invite object. + /// + public interface IInvite : IEntity, IDeletable + { + /// + /// Gets the unique identifier for this invite. + /// + /// + /// A string containing the invite code (e.g. FTqNnyS). + /// + string Code { get; } + /// + /// Gets the URL used to accept this invite using . + /// + /// + /// A string containing the full invite URL (e.g. https://discord.gg/FTqNnyS). + /// + string Url { get; } + + /// + /// Gets the channel this invite is linked to. + /// + /// + /// A generic channel that the invite points to. + /// + IChannel Channel { get; } + /// + /// Gets the type of the channel this invite is linked to. + /// + ChannelType ChannelType { get; } + /// + /// Gets the ID of the channel this invite is linked to. + /// + /// + /// An representing the channel snowflake identifier that the invite points to. + /// + ulong ChannelId { get; } + /// + /// Gets the name of the channel this invite is linked to. + /// + /// + /// A string containing the name of the channel that the invite points to. + /// + string ChannelName { get; } + + /// + /// Gets the guild this invite is linked to. + /// + /// + /// A guild object representing the guild that the invite points to. + /// + IGuild Guild { get; } + /// + /// Gets the ID of the guild this invite is linked to. + /// + /// + /// An representing the guild snowflake identifier that the invite points to. + /// + ulong? GuildId { get; } + /// + /// Gets the name of the guild this invite is linked to. + /// + /// + /// A string containing the name of the guild that the invite points to. + /// + string GuildName { get; } + /// + /// Gets the approximated count of online members in the guild. + /// + /// + /// An representing the approximated online member count of the guild that the + /// invite points to; null if one cannot be obtained. + /// + int? PresenceCount { get; } + /// + /// Gets the approximated count of total members in the guild. + /// + /// + /// An representing the approximated total member count of the guild that the + /// invite points to; null if one cannot be obtained. + /// + int? MemberCount { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs new file mode 100644 index 0000000..471dc37 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs @@ -0,0 +1,63 @@ +using System; + +namespace Discord +{ + /// + /// Represents additional information regarding the generic invite object. + /// + public interface IInviteMetadata : IInvite + { + /// + /// Gets the user that created this invite. + /// + /// + /// A user that created this invite. + /// + IUser Inviter { get; } + /// + /// Gets a value that indicates whether the invite has been revoked. + /// + /// + /// true if this invite was revoked; otherwise false. + /// + bool IsRevoked { get; } + /// + /// Gets a value that indicates whether the invite is a temporary one. + /// + /// + /// true if users accepting this invite will be removed from the guild when they log off; otherwise + /// false. + /// + bool IsTemporary { get; } + /// + /// Gets the time (in seconds) until the invite expires. + /// + /// + /// An representing the time in seconds until this invite expires; null if this + /// invite never expires. + /// + int? MaxAge { get; } + /// + /// Gets the max number of uses this invite may have. + /// + /// + /// An representing the number of uses this invite may be accepted until it is removed + /// from the guild; null if none is set. + /// + int? MaxUses { get; } + /// + /// Gets the number of times this invite has been used. + /// + /// + /// An representing the number of times this invite has been used. + /// + int? Uses { get; } + /// + /// Gets when this invite was created. + /// + /// + /// A representing the time of which the invite was first created. + /// + DateTimeOffset? CreatedAt { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/Embed.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/Embed.cs new file mode 100644 index 0000000..7fa6f6f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/Embed.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; + +namespace Discord +{ + /// + /// Represents an embed object seen in an . + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class Embed : IEmbed + { + /// + public EmbedType Type { get; } + + /// + public string Description { get; internal set; } + /// + public string Url { get; internal set; } + /// + public string Title { get; internal set; } + /// + public DateTimeOffset? Timestamp { get; internal set; } + /// + public Color? Color { get; internal set; } + /// + public EmbedImage? Image { get; internal set; } + /// + public EmbedVideo? Video { get; internal set; } + /// + public EmbedAuthor? Author { get; internal set; } + /// + public EmbedFooter? Footer { get; internal set; } + /// + public EmbedProvider? Provider { get; internal set; } + /// + public EmbedThumbnail? Thumbnail { get; internal set; } + /// + public ImmutableArray Fields { get; internal set; } + + internal Embed(EmbedType type) + { + Type = type; + Fields = ImmutableArray.Create(); + } + internal Embed(EmbedType type, + string title, + string description, + string url, + DateTimeOffset? timestamp, + Color? color, + EmbedImage? image, + EmbedVideo? video, + EmbedAuthor? author, + EmbedFooter? footer, + EmbedProvider? provider, + EmbedThumbnail? thumbnail, + ImmutableArray fields) + { + Type = type; + Title = title; + Description = description; + Url = url; + Color = color; + Timestamp = timestamp; + Image = image; + Video = video; + Author = author; + Footer = footer; + Provider = provider; + Thumbnail = thumbnail; + Fields = fields; + } + + /// + /// Gets the total length of all embed properties. + /// + public int Length + { + get + { + int titleLength = Title?.Length ?? 0; + int authorLength = Author?.Name?.Length ?? 0; + int descriptionLength = Description?.Length ?? 0; + int footerLength = Footer?.Text?.Length ?? 0; + int fieldSum = Fields.Sum(f => f.Name?.Length + f.Value?.ToString().Length) ?? 0; + return titleLength + authorLength + descriptionLength + footerLength + fieldSum; + } + } + + /// + /// Gets the title of the embed. + /// + public override string ToString() => Title; + private string DebuggerDisplay => $"{Title} ({Type})"; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs new file mode 100644 index 0000000..3b11f6a --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs @@ -0,0 +1,45 @@ +using System.Diagnostics; + +namespace Discord +{ + /// + /// A author field of an . + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public struct EmbedAuthor + { + /// + /// Gets the name of the author field. + /// + public string Name { get; internal set; } + /// + /// Gets the URL of the author field. + /// + public string Url { get; internal set; } + /// + /// Gets the icon URL of the author field. + /// + public string IconUrl { get; internal set; } + /// + /// Gets the proxified icon URL of the author field. + /// + public string ProxyIconUrl { get; internal set; } + + internal EmbedAuthor(string name, string url, string iconUrl, string proxyIconUrl) + { + Name = name; + Url = url; + IconUrl = iconUrl; + ProxyIconUrl = proxyIconUrl; + } + + private string DebuggerDisplay => $"{Name} ({Url})"; + /// + /// Gets the name of the author field. + /// + /// + /// + /// + public override string ToString() => Name; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs new file mode 100644 index 0000000..555fd95 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs @@ -0,0 +1,756 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Discord +{ + /// + /// Represents a builder class for creating a . + /// + public class EmbedBuilder + { + private string _title; + private string _description; + private string _url; + private EmbedImage? _image; + private EmbedThumbnail? _thumbnail; + private List _fields; + + /// + /// Returns the maximum number of fields allowed by Discord. + /// + public const int MaxFieldCount = 25; + /// + /// Returns the maximum length of title allowed by Discord. + /// + public const int MaxTitleLength = 256; + /// + /// Returns the maximum length of description allowed by Discord. + /// + public const int MaxDescriptionLength = 2048; + /// + /// Returns the maximum length of total characters allowed by Discord. + /// + public const int MaxEmbedLength = 6000; + + /// Initializes a new class. + public EmbedBuilder() + { + Fields = new List(); + } + + /// Gets or sets the title of an . + /// Title length exceeds . + /// + /// The title of the embed. + public string Title + { + get => _title; + set + { + if (value?.Length > MaxTitleLength) throw new ArgumentException(message: $"Title length must be less than or equal to {MaxTitleLength}.", paramName: nameof(Title)); + _title = value; + } + } + + /// Gets or sets the description of an . + /// Description length exceeds . + /// The description of the embed. + public string Description + { + get => _description; + set + { + if (value?.Length > MaxDescriptionLength) throw new ArgumentException(message: $"Description length must be less than or equal to {MaxDescriptionLength}.", paramName: nameof(Description)); + _description = value; + } + } + + /// Gets or sets the URL of an . + /// Url is not a well-formed . + /// The URL of the embed. + public string Url + { + get => _url; + set + { + if (!value.IsNullOrUri()) throw new ArgumentException(message: "Url must be a well-formed URI.", paramName: nameof(Url)); + _url = value; + } + } + /// Gets or sets the thumbnail URL of an . + /// Url is not a well-formed . + /// The thumbnail URL of the embed. + public string ThumbnailUrl + { + get => _thumbnail?.Url; + set + { + if (!value.IsNullOrUri()) throw new ArgumentException(message: "Url must be a well-formed URI.", paramName: nameof(ThumbnailUrl)); + _thumbnail = new EmbedThumbnail(value, null, null, null); + } + } + /// Gets or sets the image URL of an . + /// Url is not a well-formed . + /// The image URL of the embed. + public string ImageUrl + { + get => _image?.Url; + set + { + if (!value.IsNullOrUri()) throw new ArgumentException(message: "Url must be a well-formed URI.", paramName: nameof(ImageUrl)); + _image = new EmbedImage(value, null, null, null); + } + } + + /// Gets or sets the list of of an . + /// An embed builder's fields collection is set to + /// null. + /// Description length exceeds . + /// + /// The list of existing . + public List Fields + { + get => _fields; + set + { + if (value == null) throw new ArgumentNullException(paramName: nameof(Fields), message: "Cannot set an embed builder's fields collection to null."); + if (value.Count > MaxFieldCount) throw new ArgumentException(message: $"Field count must be less than or equal to {MaxFieldCount}.", paramName: nameof(Fields)); + _fields = value; + } + } + + /// + /// Gets or sets the timestamp of an . + /// + /// + /// The timestamp of the embed, or null if none is set. + /// + public DateTimeOffset? Timestamp { get; set; } + /// + /// Gets or sets the sidebar color of an . + /// + /// + /// The color of the embed, or null if none is set. + /// + public Color? Color { get; set; } + /// + /// Gets or sets the of an . + /// + /// + /// The author field builder of the embed, or null if none is set. + /// + public EmbedAuthorBuilder Author { get; set; } + /// + /// Gets or sets the of an . + /// + /// + /// The footer field builder of the embed, or null if none is set. + /// + public EmbedFooterBuilder Footer { get; set; } + + /// + /// Gets the total length of all embed properties. + /// + /// + /// The combined length of , , , + /// , , and . + /// + public int Length + { + get + { + int titleLength = Title?.Length ?? 0; + int authorLength = Author?.Name?.Length ?? 0; + int descriptionLength = Description?.Length ?? 0; + int footerLength = Footer?.Text?.Length ?? 0; + int fieldSum = Fields.Sum(f => f.Name.Length + f.Value.ToString().Length); + + return titleLength + authorLength + descriptionLength + footerLength + fieldSum; + } + } + + /// + /// Sets the title of an . + /// + /// The title to be set. + /// + /// The current builder. + /// + public EmbedBuilder WithTitle(string title) + { + Title = title; + return this; + } + /// + /// Sets the description of an . + /// + /// The description to be set. + /// + /// The current builder. + /// + public EmbedBuilder WithDescription(string description) + { + Description = description; + return this; + } + /// + /// Sets the URL of an . + /// + /// The URL to be set. + /// + /// The current builder. + /// + public EmbedBuilder WithUrl(string url) + { + Url = url; + return this; + } + /// + /// Sets the thumbnail URL of an . + /// + /// The thumbnail URL to be set. + /// + /// The current builder. + /// + public EmbedBuilder WithThumbnailUrl(string thumbnailUrl) + { + ThumbnailUrl = thumbnailUrl; + return this; + } + /// + /// Sets the image URL of an . + /// + /// The image URL to be set. + /// + /// The current builder. + /// + public EmbedBuilder WithImageUrl(string imageUrl) + { + ImageUrl = imageUrl; + return this; + } + /// + /// Sets the timestamp of an to the current time. + /// + /// + /// The current builder. + /// + public EmbedBuilder WithCurrentTimestamp() + { + Timestamp = DateTimeOffset.UtcNow; + return this; + } + /// + /// Sets the timestamp of an . + /// + /// The timestamp to be set. + /// + /// The current builder. + /// + public EmbedBuilder WithTimestamp(DateTimeOffset dateTimeOffset) + { + Timestamp = dateTimeOffset; + return this; + } + /// + /// Sets the sidebar color of an . + /// + /// The color to be set. + /// + /// The current builder. + /// + public EmbedBuilder WithColor(Color color) + { + Color = color; + return this; + } + + /// + /// Sets the of an . + /// + /// The author builder class containing the author field properties. + /// + /// The current builder. + /// + public EmbedBuilder WithAuthor(EmbedAuthorBuilder author) + { + Author = author; + return this; + } + /// + /// Sets the author field of an with the provided properties. + /// + /// The delegate containing the author field properties. + /// + /// The current builder. + /// + public EmbedBuilder WithAuthor(Action action) + { + var author = new EmbedAuthorBuilder(); + action(author); + Author = author; + return this; + } + /// + /// Sets the author field of an with the provided name, icon URL, and URL. + /// + /// The title of the author field. + /// The icon URL of the author field. + /// The URL of the author field. + /// + /// The current builder. + /// + public EmbedBuilder WithAuthor(string name, string iconUrl = null, string url = null) + { + var author = new EmbedAuthorBuilder + { + Name = name, + IconUrl = iconUrl, + Url = url + }; + Author = author; + return this; + } + /// + /// Sets the of an . + /// + /// The footer builder class containing the footer field properties. + /// + /// The current builder. + /// + public EmbedBuilder WithFooter(EmbedFooterBuilder footer) + { + Footer = footer; + return this; + } + /// + /// Sets the footer field of an with the provided properties. + /// + /// The delegate containing the footer field properties. + /// + /// The current builder. + /// + public EmbedBuilder WithFooter(Action action) + { + var footer = new EmbedFooterBuilder(); + action(footer); + Footer = footer; + return this; + } + /// + /// Sets the footer field of an with the provided name, icon URL. + /// + /// The title of the footer field. + /// The icon URL of the footer field. + /// + /// The current builder. + /// + public EmbedBuilder WithFooter(string text, string iconUrl = null) + { + var footer = new EmbedFooterBuilder + { + Text = text, + IconUrl = iconUrl + }; + Footer = footer; + return this; + } + + /// + /// Adds an field with the provided name and value. + /// + /// The title of the field. + /// The value of the field. + /// Indicates whether the field is in-line or not. + /// + /// The current builder. + /// + public EmbedBuilder AddField(string name, object value, bool inline = false) + { + var field = new EmbedFieldBuilder() + .WithIsInline(inline) + .WithName(name) + .WithValue(value); + AddField(field); + return this; + } + + /// + /// Adds a field with the provided to an + /// . + /// + /// The field builder class containing the field properties. + /// Field count exceeds . + /// + /// The current builder. + /// + public EmbedBuilder AddField(EmbedFieldBuilder field) + { + if (Fields.Count >= MaxFieldCount) + { + throw new ArgumentException(message: $"Field count must be less than or equal to {MaxFieldCount}.", paramName: nameof(field)); + } + + Fields.Add(field); + return this; + } + /// + /// Adds an field with the provided properties. + /// + /// The delegate containing the field properties. + /// + /// The current builder. + /// + public EmbedBuilder AddField(Action action) + { + var field = new EmbedFieldBuilder(); + action(field); + AddField(field); + return this; + } + + /// + /// Builds the into a Rich Embed ready to be sent. + /// + /// + /// The built embed object. + /// + /// Total embed length exceeds . + public Embed Build() + { + if (Length > MaxEmbedLength) + throw new InvalidOperationException($"Total embed length must be less than or equal to {MaxEmbedLength}."); + + var fields = ImmutableArray.CreateBuilder(Fields.Count); + for (int i = 0; i < Fields.Count; i++) + fields.Add(Fields[i].Build()); + + return new Embed(EmbedType.Rich, Title, Description, Url, Timestamp, Color, _image, null, Author?.Build(), Footer?.Build(), null, _thumbnail, fields.ToImmutable()); + } + } + + /// + /// Represents a builder class for an embed field. + /// + public class EmbedFieldBuilder + { + private string _name; + private string _value; + /// + /// Gets the maximum field length for name allowed by Discord. + /// + public const int MaxFieldNameLength = 256; + /// + /// Gets the maximum field length for value allowed by Discord. + /// + public const int MaxFieldValueLength = 1024; + + /// + /// Gets or sets the field name. + /// + /// + /// Field name is null, empty or entirely whitespace. + /// - or - + /// Field name length exceeds . + /// + /// + /// The name of the field. + /// + public string Name + { + get => _name; + set + { + if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException(message: "Field name must not be null, empty or entirely whitespace.", paramName: nameof(Name)); + if (value.Length > MaxFieldNameLength) throw new ArgumentException(message: $"Field name length must be less than or equal to {MaxFieldNameLength}.", paramName: nameof(Name)); + _name = value; + } + } + + /// + /// Gets or sets the field value. + /// + /// + /// Field value is null, empty or entirely whitespace. + /// - or - + /// Field value length exceeds . + /// + /// + /// The value of the field. + /// + public object Value + { + get => _value; + set + { + var stringValue = value?.ToString(); + if (string.IsNullOrWhiteSpace(stringValue)) throw new ArgumentException(message: "Field value must not be null or empty.", paramName: nameof(Value)); + if (stringValue.Length > MaxFieldValueLength) throw new ArgumentException(message: $"Field value length must be less than or equal to {MaxFieldValueLength}.", paramName: nameof(Value)); + _value = stringValue; + } + } + /// + /// Gets or sets a value that indicates whether the field should be in-line with each other. + /// + public bool IsInline { get; set; } + + /// + /// Sets the field name. + /// + /// The name to set the field name to. + /// + /// The current builder. + /// + public EmbedFieldBuilder WithName(string name) + { + Name = name; + return this; + } + /// + /// Sets the field value. + /// + /// The value to set the field value to. + /// + /// The current builder. + /// + public EmbedFieldBuilder WithValue(object value) + { + Value = value; + return this; + } + /// + /// Determines whether the field should be in-line with each other. + /// + /// + /// The current builder. + /// + public EmbedFieldBuilder WithIsInline(bool isInline) + { + IsInline = isInline; + return this; + } + + /// + /// Builds the field builder into a class. + /// + /// + /// The current builder. + /// + /// + /// or is null, empty or entirely whitespace. + /// - or - + /// or exceeds the maximum length allowed by Discord. + /// + public EmbedField Build() + => new EmbedField(Name, Value.ToString(), IsInline); + } + + /// + /// Represents a builder class for a author field. + /// + public class EmbedAuthorBuilder + { + private string _name; + private string _url; + private string _iconUrl; + /// + /// Gets the maximum author name length allowed by Discord. + /// + public const int MaxAuthorNameLength = 256; + + /// + /// Gets or sets the author name. + /// + /// + /// Author name length is longer than . + /// + /// + /// The author name. + /// + public string Name + { + get => _name; + set + { + if (value?.Length > MaxAuthorNameLength) throw new ArgumentException(message: $"Author name length must be less than or equal to {MaxAuthorNameLength}.", paramName: nameof(Name)); + _name = value; + } + } + /// + /// Gets or sets the URL of the author field. + /// + /// Url is not a well-formed . + /// + /// The URL of the author field. + /// + public string Url + { + get => _url; + set + { + if (!value.IsNullOrUri()) throw new ArgumentException(message: "Url must be a well-formed URI.", paramName: nameof(Url)); + _url = value; + } + } + /// + /// Gets or sets the icon URL of the author field. + /// + /// Url is not a well-formed . + /// + /// The icon URL of the author field. + /// + public string IconUrl + { + get => _iconUrl; + set + { + if (!value.IsNullOrUri()) throw new ArgumentException(message: "Url must be a well-formed URI.", paramName: nameof(IconUrl)); + _iconUrl = value; + } + } + + /// + /// Sets the name of the author field. + /// + /// The name of the author field. + /// + /// The current builder. + /// + public EmbedAuthorBuilder WithName(string name) + { + Name = name; + return this; + } + /// + /// Sets the URL of the author field. + /// + /// The URL of the author field. + /// + /// The current builder. + /// + public EmbedAuthorBuilder WithUrl(string url) + { + Url = url; + return this; + } + /// + /// Sets the icon URL of the author field. + /// + /// The icon URL of the author field. + /// + /// The current builder. + /// + public EmbedAuthorBuilder WithIconUrl(string iconUrl) + { + IconUrl = iconUrl; + return this; + } + + /// + /// Builds the author field to be used. + /// + /// + /// Author name length is longer than . + /// - or - + /// is not a well-formed . + /// - or - + /// is not a well-formed . + /// + /// + /// The built author field. + /// + public EmbedAuthor Build() + => new EmbedAuthor(Name, Url, IconUrl, null); + } + + /// + /// Represents a builder class for an embed footer. + /// + public class EmbedFooterBuilder + { + private string _text; + private string _iconUrl; + + /// + /// Gets the maximum footer length allowed by Discord. + /// + public const int MaxFooterTextLength = 2048; + + /// + /// Gets or sets the footer text. + /// + /// + /// Author name length is longer than . + /// + /// + /// The footer text. + /// + public string Text + { + get => _text; + set + { + if (value?.Length > MaxFooterTextLength) throw new ArgumentException(message: $"Footer text length must be less than or equal to {MaxFooterTextLength}.", paramName: nameof(Text)); + _text = value; + } + } + /// + /// Gets or sets the icon URL of the footer field. + /// + /// Url is not a well-formed . + /// + /// The icon URL of the footer field. + /// + public string IconUrl + { + get => _iconUrl; + set + { + if (!value.IsNullOrUri()) throw new ArgumentException(message: "Url must be a well-formed URI.", paramName: nameof(IconUrl)); + _iconUrl = value; + } + } + + /// + /// Sets the name of the footer field. + /// + /// The text of the footer field. + /// + /// The current builder. + /// + public EmbedFooterBuilder WithText(string text) + { + Text = text; + return this; + } + /// + /// Sets the icon URL of the footer field. + /// + /// The icon URL of the footer field. + /// + /// The current builder. + /// + public EmbedFooterBuilder WithIconUrl(string iconUrl) + { + IconUrl = iconUrl; + return this; + } + + /// + /// Builds the footer field to be used. + /// + /// + /// + /// length is longer than . + /// - or - + /// is not a well-formed . + /// + /// + /// A built footer field. + /// + public EmbedFooter Build() + => new EmbedFooter(Text, IconUrl, null); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/EmbedField.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/EmbedField.cs new file mode 100644 index 0000000..f6aa2af --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/EmbedField.cs @@ -0,0 +1,40 @@ +using System.Diagnostics; + +namespace Discord +{ + /// + /// A field for an . + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public struct EmbedField + { + /// + /// Gets the name of the field. + /// + public string Name { get; internal set; } + /// + /// Gets the value of the field. + /// + public string Value { get; internal set; } + /// + /// Gets a value that indicates whether the field should be in-line with each other. + /// + public bool Inline { get; internal set; } + + internal EmbedField(string name, string value, bool inline) + { + Name = name; + Value = value; + Inline = inline; + } + + private string DebuggerDisplay => $"{Name} ({Value}"; + /// + /// Gets the name of the field. + /// + /// + /// A string that resolves to . + /// + public override string ToString() => Name; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/EmbedFooter.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/EmbedFooter.cs new file mode 100644 index 0000000..4c507d0 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/EmbedFooter.cs @@ -0,0 +1,47 @@ +using System.Diagnostics; + +namespace Discord +{ + /// A footer field for an . + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public struct EmbedFooter + { + /// + /// Gets the text of the footer field. + /// + /// + /// A string containing the text of the footer field. + /// + public string Text { get; } + /// + /// Gets the URL of the footer icon. + /// + /// + /// A string containing the URL of the footer icon. + /// + public string IconUrl { get; } + /// + /// Gets the proxied URL of the footer icon link. + /// + /// + /// A string containing the proxied URL of the footer icon. + /// + public string ProxyUrl { get; } + + internal EmbedFooter(string text, string iconUrl, string proxyUrl) + { + Text = text; + IconUrl = iconUrl; + ProxyUrl = proxyUrl; + } + + private string DebuggerDisplay => $"{Text} ({IconUrl})"; + /// + /// Gets the text of the footer field. + /// + /// + /// A string that resolves to . + /// + public override string ToString() => Text; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/EmbedImage.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/EmbedImage.cs new file mode 100644 index 0000000..9ce2bfe --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/EmbedImage.cs @@ -0,0 +1,57 @@ +using System.Diagnostics; + +namespace Discord +{ + /// An image for an . + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public struct EmbedImage + { + /// + /// Gets the URL of the image. + /// + /// + /// A string containing the URL of the image. + /// + public string Url { get; } + /// + /// Gets a proxied URL of this image. + /// + /// + /// A string containing the proxied URL of this image. + /// + public string ProxyUrl { get; } + /// + /// Gets the height of this image. + /// + /// + /// A representing the height of this image if it can be retrieved; otherwise + /// null. + /// + public int? Height { get; } + /// + /// Gets the width of this image. + /// + /// + /// A representing the width of this image if it can be retrieved; otherwise + /// null. + /// + public int? Width { get; } + + internal EmbedImage(string url, string proxyUrl, int? height, int? width) + { + Url = url; + ProxyUrl = proxyUrl; + Height = height; + Width = width; + } + + private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})"; + /// + /// Gets the URL of the thumbnail. + /// + /// + /// A string that resolves to . + /// + public override string ToString() => Url; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/EmbedProvider.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/EmbedProvider.cs new file mode 100644 index 0000000..960fb3d --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/EmbedProvider.cs @@ -0,0 +1,39 @@ +using System.Diagnostics; + +namespace Discord +{ + /// A provider field for an . + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public struct EmbedProvider + { + /// + /// Gets the name of the provider. + /// + /// + /// A string representing the name of the provider. + /// + public string Name { get; } + /// + /// Gets the URL of the provider. + /// + /// + /// A string representing the link to the provider. + /// + public string Url { get; } + + internal EmbedProvider(string name, string url) + { + Name = name; + Url = url; + } + + private string DebuggerDisplay => $"{Name} ({Url})"; + /// + /// Gets the name of the provider. + /// + /// + /// A string that resolves to . + /// + public override string ToString() => Name; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs new file mode 100644 index 0000000..7f7b582 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs @@ -0,0 +1,57 @@ +using System.Diagnostics; + +namespace Discord +{ + /// A thumbnail featured in an . + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public struct EmbedThumbnail + { + /// + /// Gets the URL of the thumbnail. + /// + /// + /// A string containing the URL of the thumbnail. + /// + public string Url { get; } + /// + /// Gets a proxied URL of this thumbnail. + /// + /// + /// A string containing the proxied URL of this thumbnail. + /// + public string ProxyUrl { get; } + /// + /// Gets the height of this thumbnail. + /// + /// + /// A representing the height of this thumbnail if it can be retrieved; otherwise + /// null. + /// + public int? Height { get; } + /// + /// Gets the width of this thumbnail. + /// + /// + /// A representing the width of this thumbnail if it can be retrieved; otherwise + /// null. + /// + public int? Width { get; } + + internal EmbedThumbnail(string url, string proxyUrl, int? height, int? width) + { + Url = url; + ProxyUrl = proxyUrl; + Height = height; + Width = width; + } + + private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})"; + /// + /// Gets the URL of the thumbnail. + /// + /// + /// A string that resolves to . + /// + public override string ToString() => Url; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/EmbedType.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/EmbedType.cs new file mode 100644 index 0000000..978f45b --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/EmbedType.cs @@ -0,0 +1,45 @@ +namespace Discord +{ + /// + /// Specifies the type of embed. + /// + public enum EmbedType + { + /// + /// An unknown embed type. + /// + Unknown = -1, + /// + /// A rich embed type. + /// + Rich, + /// + /// A link embed type. + /// + Link, + /// + /// A video embed type. + /// + Video, + /// + /// An image embed type. + /// + Image, + /// + /// A GIFV embed type. + /// + Gifv, + /// + /// An article embed type. + /// + Article, + /// + /// A tweet embed type. + /// + Tweet, + /// + /// A HTML embed type. + /// + Html, + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/EmbedVideo.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/EmbedVideo.cs new file mode 100644 index 0000000..ca0300e --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/EmbedVideo.cs @@ -0,0 +1,51 @@ +using System.Diagnostics; + +namespace Discord +{ + /// + /// A video featured in an . + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public struct EmbedVideo + { + /// + /// Gets the URL of the video. + /// + /// + /// A string containing the URL of the image. + /// + public string Url { get; } + /// + /// Gets the height of the video. + /// + /// + /// A representing the height of this video if it can be retrieved; otherwise + /// null. + /// + public int? Height { get; } + /// + /// Gets the weight of the video. + /// + /// + /// A representing the width of this video if it can be retrieved; otherwise + /// null. + /// + public int? Width { get; } + + internal EmbedVideo(string url, int? height, int? width) + { + Url = url; + Height = height; + Width = width; + } + + private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})"; + /// + /// Gets the URL of the video. + /// + /// + /// A string that resolves to . + /// + public override string ToString() => Url; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/IAttachment.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/IAttachment.cs new file mode 100644 index 0000000..6557779 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/IAttachment.cs @@ -0,0 +1,59 @@ +namespace Discord +{ + /// + /// Represents a message attachment found in a . + /// + public interface IAttachment + { + /// + /// Gets the ID of this attachment. + /// + /// + /// A snowflake ID associated with this attachment. + /// + ulong Id { get; } + + /// + /// Gets the filename of this attachment. + /// + /// + /// A string containing the full filename of this attachment (e.g. textFile.txt). + /// + string Filename { get; } + /// + /// Gets the URL of this attachment. + /// + /// + /// A string containing the URL of this attachment. + /// + string Url { get; } + /// + /// Gets a proxied URL of this attachment. + /// + /// + /// A string containing the proxied URL of this attachment. + /// + string ProxyUrl { get; } + /// + /// Gets the file size of this attachment. + /// + /// + /// The size of this attachment in bytes. + /// + int Size { get; } + /// + /// Gets the height of this attachment. + /// + /// + /// The height of this attachment if it is a picture; otherwise null. + /// + int? Height { get; } + /// + /// Gets the width of this attachment. + /// + /// + /// The width of this attachment if it is a picture; otherwise null. + /// + int? Width { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/IEmbed.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/IEmbed.cs new file mode 100644 index 0000000..4c1029a --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/IEmbed.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Immutable; + +namespace Discord +{ + /// + /// Represents a Discord embed object. + /// + public interface IEmbed + { + /// + /// Gets the title URL of this embed. + /// + /// + /// A string containing the URL set in a title of the embed. + /// + string Url { get; } + /// + /// Gets the title of this embed. + /// + /// + /// The title of the embed. + /// + string Title { get; } + /// + /// Gets the description of this embed. + /// + /// + /// The description field of the embed. + /// + string Description { get; } + /// + /// Gets the type of this embed. + /// + /// + /// The type of the embed. + /// + EmbedType Type { get; } + /// + /// Gets the timestamp of this embed. + /// + /// + /// A based on the timestamp present at the bottom left of the embed, or + /// null if none is set. + /// + DateTimeOffset? Timestamp { get; } + /// + /// Gets the color of this embed. + /// + /// + /// The color of the embed present on the side of the embed, or null if none is set. + /// + Color? Color { get; } + /// + /// Gets the image of this embed. + /// + /// + /// The image of the embed, or null if none is set. + /// + EmbedImage? Image { get; } + /// + /// Gets the video of this embed. + /// + /// + /// The video of the embed, or null if none is set. + /// + EmbedVideo? Video { get; } + /// + /// Gets the author field of this embed. + /// + /// + /// The author field of the embed, or null if none is set. + /// + EmbedAuthor? Author { get; } + /// + /// Gets the footer field of this embed. + /// + /// + /// The author field of the embed, or null if none is set. + /// + EmbedFooter? Footer { get; } + /// + /// Gets the provider of this embed. + /// + /// + /// The source of the embed, or null if none is set. + /// + EmbedProvider? Provider { get; } + /// + /// Gets the thumbnail featured in this embed. + /// + /// + /// The thumbnail featured in the embed, or null if none is set. + /// + EmbedThumbnail? Thumbnail { get; } + /// + /// Gets the fields of the embed. + /// + /// + /// An array of the fields of the embed. + /// + ImmutableArray Fields { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/IMessage.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/IMessage.cs new file mode 100644 index 0000000..05f5052 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/IMessage.cs @@ -0,0 +1,239 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a message object. + /// + public interface IMessage : ISnowflakeEntity, IDeletable + { + /// + /// Gets the type of this system message. + /// + MessageType Type { get; } + /// + /// Gets the source type of this message. + /// + MessageSource Source { get; } + /// + /// Gets the value that indicates whether this message was meant to be read-aloud by Discord. + /// + /// + /// true if this message was sent as a text-to-speech message; otherwise false. + /// + bool IsTTS { get; } + /// + /// Gets the value that indicates whether this message is pinned. + /// + /// + /// true if this message was added to its channel's pinned messages; otherwise false. + /// + bool IsPinned { get; } + /// + /// Gets the value that indicates whether or not this message's embeds are suppressed. + /// + /// + /// true if the embeds in this message have been suppressed (made invisible); otherwise false. + /// + bool IsSuppressed { get; } + /// + /// Gets the content for this message. + /// + /// + /// A string that contains the body of the message; note that this field may be empty if there is an embed. + /// + string Content { get; } + /// + /// Gets the time this message was sent. + /// + /// + /// Time of when the message was sent. + /// + DateTimeOffset Timestamp { get; } + /// + /// Gets the time of this message's last edit. + /// + /// + /// Time of when the message was last edited; null if the message is never edited. + /// + DateTimeOffset? EditedTimestamp { get; } + + /// + /// Gets the source channel of the message. + /// + IMessageChannel Channel { get; } + /// + /// Gets the author of this message. + /// + IUser Author { get; } + + /// + /// Gets all attachments included in this message. + /// + /// + /// This property gets a read-only collection of attachments associated with this message. Depending on the + /// user's end-client, a sent message may contain one or more attachments. For example, mobile users may + /// attach more than one file in their message, while the desktop client only allows for one. + /// + /// + /// A read-only collection of attachments. + /// + IReadOnlyCollection Attachments { get; } + /// + /// Gets all embeds included in this message. + /// + /// + /// + /// This property gets a read-only collection of embeds associated with this message. Depending on the + /// message, a sent message may contain one or more embeds. This is usually true when multiple link previews + /// are generated; however, only one can be featured. + /// + /// A read-only collection of embed objects. + /// + IReadOnlyCollection Embeds { get; } + /// + /// Gets all tags included in this message's content. + /// + IReadOnlyCollection Tags { get; } + /// + /// Gets the IDs of channels mentioned in this message. + /// + /// + /// A read-only collection of channel IDs. + /// + IReadOnlyCollection MentionedChannelIds { get; } + /// + /// Gets the IDs of roles mentioned in this message. + /// + /// + /// A read-only collection of role IDs. + /// + IReadOnlyCollection MentionedRoleIds { get; } + /// + /// Gets the IDs of users mentioned in this message. + /// + /// + /// A read-only collection of user IDs. + /// + IReadOnlyCollection MentionedUserIds { get; } + /// + /// Gets the activity associated with a message. + /// + /// + /// Sent with Rich Presence-related chat embeds. This often refers to activity that requires end-user's + /// interaction, such as a Spotify Invite activity. + /// + /// + /// A message's activity, if any is associated. + /// + MessageActivity Activity { get; } + /// + /// Gets the application associated with a message. + /// + /// + /// Sent with Rich-Presence-related chat embeds. + /// + /// + /// A message's application, if any is associated. + /// + MessageApplication Application { get; } + + /// + /// Gets the reference to the original message if it was crossposted. + /// + /// + /// Sent with Cross-posted messages, meaning they were published from news channels + /// and received by subscriber channels. + /// + /// + /// A message's reference, if any is associated. + /// + MessageReference Reference { get; } + + /// + /// Gets all reactions included in this message. + /// + IReadOnlyDictionary Reactions { get; } + + /// + /// Adds a reaction to this message. + /// + /// + /// The following example adds the reaction, 💕, to the message. + /// + /// await msg.AddReactionAsync(new Emoji("\U0001f495")); + /// + /// + /// The emoji used to react to this message. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous operation for adding a reaction to this message. + /// + /// + Task AddReactionAsync(IEmote emote, RequestOptions options = null); + /// + /// Removes a reaction from message. + /// + /// + /// The following example removes the reaction, 💕, added by the message author from the message. + /// + /// await msg.RemoveReactionAsync(new Emoji("\U0001f495"), msg.Author); + /// + /// + /// The emoji used to react to this message. + /// The user that added the emoji. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous operation for removing a reaction to this message. + /// + /// + Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null); + /// + /// Removes a reaction from message. + /// + /// + /// The following example removes the reaction, 💕, added by the user with ID 84291986575613952 from the message. + /// + /// await msg.RemoveReactionAsync(new Emoji("\U0001f495"), 84291986575613952); + /// + /// + /// The emoji used to react to this message. + /// The ID of the user that added the emoji. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous operation for removing a reaction to this message. + /// + /// + Task RemoveReactionAsync(IEmote emote, ulong userId, RequestOptions options = null); + /// + /// Removes all reactions from this message. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous removal operation. + /// + Task RemoveAllReactionsAsync(RequestOptions options = null); + + /// + /// Gets all users that reacted to a message with a given emote. + /// + /// + /// The following example gets the users that have reacted with the emoji 💕 to the message. + /// + /// var emoji = new Emoji("\U0001f495"); + /// var reactedUsers = await message.GetReactionUsersAsync(emoji, 100).FlattenAsync(); + /// + /// + /// The emoji that represents the reaction that you wish to get. + /// The number of users to request. + /// The options to be used when sending the request. + /// + /// A paged collection containing a read-only collection of users that has reacted to this message. + /// Flattening the paginated response into a collection of users with + /// is required if you wish to access the users. + /// + IAsyncEnumerable> GetReactionUsersAsync(IEmote emoji, int limit, RequestOptions options = null); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/IReaction.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/IReaction.cs new file mode 100644 index 0000000..b7d7128 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/IReaction.cs @@ -0,0 +1,13 @@ +namespace Discord +{ + /// + /// Represents a generic reaction object. + /// + public interface IReaction + { + /// + /// The used in the reaction. + /// + IEmote Emote { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/ISystemMessage.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/ISystemMessage.cs new file mode 100644 index 0000000..89cd17a --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/ISystemMessage.cs @@ -0,0 +1,9 @@ +namespace Discord +{ + /// + /// Represents a generic message sent by the system. + /// + public interface ISystemMessage : IMessage + { + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/ITag.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/ITag.cs new file mode 100644 index 0000000..27824e6 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/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/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/IUserMessage.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/IUserMessage.cs new file mode 100644 index 0000000..be2523b --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/IUserMessage.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a generic message sent by a user. + /// + public interface IUserMessage : IMessage + { + /// + /// Modifies this message. + /// + /// + /// This method modifies this message with the specified properties. To see an example of this + /// method and what properties are available, please refer to . + /// + /// + /// The following example replaces the content of the message with Hello World!. + /// + /// await msg.ModifyAsync(x => x.Content = "Hello World!"); + /// + /// + /// A delegate containing the properties to modify the message with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + Task ModifyAsync(Action func, RequestOptions options = null); + /// + /// Modifies the suppression of this message. + /// + /// + /// This method modifies whether or not embeds in this message are suppressed (hidden). + /// + /// Whether or not embeds in this message should be suppressed. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + Task ModifySuppressionAsync(bool suppressEmbeds, RequestOptions options = null); + /// + /// Adds this message to its channel's pinned messages. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous operation for pinning this message. + /// + Task PinAsync(RequestOptions options = null); + /// + /// Removes this message from its channel's pinned messages. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous operation for unpinning this message. + /// + Task UnpinAsync(RequestOptions options = null); + + /// + /// Transforms this message's text into a human-readable form by resolving its tags. + /// + /// Determines how the user tag should be handled. + /// Determines how the channel tag should be handled. + /// Determines how the role tag should be handled. + /// Determines how the @everyone tag should be handled. + /// Determines how the emoji tag should be handled. + 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/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/MessageActivity.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/MessageActivity.cs new file mode 100644 index 0000000..ff4ae40 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/MessageActivity.cs @@ -0,0 +1,32 @@ +using System.Diagnostics; + +namespace Discord +{ + /// + /// An activity object found in a sent message. + /// + /// + /// + /// This class refers to an activity object, visually similar to an embed within a message. However, a message + /// activity is interactive as opposed to a standard static embed. + /// + /// For example, a Spotify party invitation counts as a message activity. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class MessageActivity + { + /// + /// Gets the type of activity of this message. + /// + public MessageActivityType Type { get; internal set; } + /// + /// Gets the party ID of this activity, if any. + /// + public string PartyId { get; internal set; } + + private string DebuggerDisplay + => $"{Type}{(string.IsNullOrWhiteSpace(PartyId) ? "" : $" {PartyId}")}"; + + public override string ToString() => DebuggerDisplay; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/MessageActivityType.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/MessageActivityType.cs new file mode 100644 index 0000000..68b99a9 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/MessageActivityType.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public enum MessageActivityType + { + Join = 1, + Spectate = 2, + Listen = 3, + JoinRequest = 5 + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/MessageApplication.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/MessageApplication.cs new file mode 100644 index 0000000..39a599d --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/MessageApplication.cs @@ -0,0 +1,38 @@ +using System.Diagnostics; + +namespace Discord +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class MessageApplication + { + /// + /// Gets the snowflake ID of the application. + /// + public ulong Id { get; internal set; } + /// + /// Gets the ID of the embed's image asset. + /// + public string CoverImage { get; internal set; } + /// + /// Gets the application's description. + /// + public string Description { get; internal set; } + /// + /// Gets the ID of the application's icon. + /// + public string Icon { get; internal set; } + /// + /// Gets the Url of the application's icon. + /// + public string IconUrl + => $"https://cdn.discordapp.com/app-icons/{Id}/{Icon}"; + /// + /// Gets the name of the application. + /// + public string Name { get; internal set; } + private string DebuggerDisplay + => $"{Name} ({Id}): {Description}"; + public override string ToString() + => DebuggerDisplay; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/MessageProperties.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/MessageProperties.cs new file mode 100644 index 0000000..b632d6a --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/MessageProperties.cs @@ -0,0 +1,25 @@ +namespace Discord +{ + /// + /// Properties that are used to modify an with the specified changes. + /// + /// + /// The content of a message can be cleared with if and only if an + /// is present. + /// + /// + public class MessageProperties + { + /// + /// Gets or sets the content of the message. + /// + /// + /// This must be less than the constant defined by . + /// + public Optional Content { get; set; } + /// + /// Gets or sets the embed the message should display. + /// + public Optional Embed { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/MessageReference.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/MessageReference.cs new file mode 100644 index 0000000..57a508a --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/MessageReference.cs @@ -0,0 +1,33 @@ +using System.Diagnostics; + +namespace Discord +{ + /// + /// Contains the IDs sent from a crossposted message. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class MessageReference + { + /// + /// Gets the Message ID of the original message. + /// + public Optional MessageId { get; internal set; } + + /// + /// Gets the Channel ID of the original message. + /// + public ulong ChannelId { get; internal set; } + + /// + /// Gets the Guild ID of the original message. + /// + public Optional GuildId { get; internal set; } + + private string DebuggerDisplay + => $"Channel ID: ({ChannelId}){(GuildId.IsSpecified ? $", Guild ID: ({GuildId.Value})" : "")}" + + $"{(MessageId.IsSpecified ? $", Message ID: ({MessageId.Value})" : "")}"; + + public override string ToString() + => DebuggerDisplay; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/MessageSource.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/MessageSource.cs new file mode 100644 index 0000000..bd4f237 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/MessageSource.cs @@ -0,0 +1,25 @@ +namespace Discord +{ + /// + /// Specifies the source of the Discord message. + /// + public enum MessageSource + { + /// + /// The message is sent by the system. + /// + System, + /// + /// The message is sent by a user. + /// + User, + /// + /// The message is sent by a bot. + /// + Bot, + /// + /// The message is sent by a webhook. + /// + Webhook + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/MessageType.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/MessageType.cs new file mode 100644 index 0000000..ee5f4fb --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/MessageType.cs @@ -0,0 +1,61 @@ +namespace Discord +{ + /// + /// Specifies the type of message. + /// + public enum MessageType + { + /// + /// The default message type. + /// + Default = 0, + /// + /// The message when a recipient is added. + /// + RecipientAdd = 1, + /// + /// The message when a recipient is removed. + /// + RecipientRemove = 2, + /// + /// The message when a user is called. + /// + Call = 3, + /// + /// The message when a channel name is changed. + /// + ChannelNameChange = 4, + /// + /// The message when a channel icon is changed. + /// + ChannelIconChange = 5, + /// + /// The message when another message is pinned. + /// + ChannelPinnedMessage = 6, + /// + /// The message when a new member joined. + /// + GuildMemberJoin = 7, + /// + /// The message for when a user boosts a guild. + /// + UserPremiumGuildSubscription = 8, + /// + /// The message for when a guild reaches Tier 1 of Nitro boosts. + /// + UserPremiumGuildSubscriptionTier1 = 9, + /// + /// The message for when a guild reaches Tier 2 of Nitro boosts. + /// + UserPremiumGuildSubscriptionTier2 = 10, + /// + /// The message for when a guild reaches Tier 3 of Nitro boosts. + /// + UserPremiumGuildSubscriptionTier3 = 11, + /// + /// The message for when a news channel subscription is added to a text channel. + /// + ChannelFollowAdd = 12, + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/ReactionMetadata.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/ReactionMetadata.cs new file mode 100644 index 0000000..8506669 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/ReactionMetadata.cs @@ -0,0 +1,24 @@ +namespace Discord +{ + /// + /// A metadata containing reaction information. + /// + public struct ReactionMetadata + { + /// + /// Gets the number of reactions. + /// + /// + /// An representing the number of this reactions that has been added to this message. + /// + public int ReactionCount { get; internal set; } + + /// + /// Gets a value that indicates whether the current user has reacted to this. + /// + /// + /// true if the user has reacted to the message; otherwise false. + /// + public bool IsMe { get; internal set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/Tag.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/Tag.cs new file mode 100644 index 0000000..06d995e --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/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/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/TagHandling.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/TagHandling.cs new file mode 100644 index 0000000..eaadd64 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/TagHandling.cs @@ -0,0 +1,39 @@ +namespace Discord +{ + /// + /// Specifies the handling type the tag should use. + /// + /// + /// + public enum TagHandling + { + /// + /// Tag handling is ignored (e.g. <@53905483156684800> -> <@53905483156684800>). + /// + Ignore = 0, + /// + /// Removes the tag entirely. + /// + Remove, + /// + /// Resolves to username (e.g. <@53905483156684800> -> @Voltana). + /// + Name, + /// + /// Resolves to username without mention prefix (e.g. <@53905483156684800> -> Voltana). + /// + NameNoPrefix, + /// + /// Resolves to username with discriminator value. (e.g. <@53905483156684800> -> @Voltana#8252). + /// + FullName, + /// + /// Resolves to username with discriminator value without mention prefix. (e.g. <@53905483156684800> -> Voltana#8252). + /// + FullNameNoPrefix, + /// + /// Sanitizes the tag (e.g. <@53905483156684800> -> <@53905483156684800> (w/ nbsp)). + /// + Sanitize + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/TagType.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/TagType.cs new file mode 100644 index 0000000..1771572 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Messages/TagType.cs @@ -0,0 +1,19 @@ +namespace Discord +{ + /// Specifies the type of Discord tag. + public enum TagType + { + /// The object is an user mention. + UserMention, + /// The object is a channel mention. + ChannelMention, + /// The object is a role mention. + RoleMention, + /// The object is an everyone mention. + EveryoneMention, + /// The object is a here mention. + HereMention, + /// The object is an emoji. + Emoji + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs new file mode 100644 index 0000000..e1f7837 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs @@ -0,0 +1,108 @@ +using System; + +namespace Discord +{ + /// Defines the available permissions for a channel. + [Flags] + public enum ChannelPermission : ulong + { + // General + /// + /// Allows creation of instant invites. + /// + CreateInstantInvite = 0x00_00_00_01, + /// + /// Allows management and editing of channels. + /// + ManageChannels = 0x00_00_00_10, + + // Text + /// + /// Allows for the addition of reactions to messages. + /// + AddReactions = 0x00_00_00_40, + /// + /// Allows for reading of messages. This flag is obsolete, use instead. + /// + [Obsolete("Use ViewChannel instead.")] + ReadMessages = ViewChannel, + /// + /// Allows guild members to view a channel, which includes reading messages in text channels. + /// + ViewChannel = 0x00_00_04_00, + /// + /// Allows for sending messages in a channel. + /// + SendMessages = 0x00_00_08_00, + /// + /// Allows for sending of text-to-speech messages. + /// + SendTTSMessages = 0x00_00_10_00, + /// + /// Allows for deletion of other users messages. + /// + ManageMessages = 0x00_00_20_00, + /// + /// Allows links sent by users with this permission will be auto-embedded. + /// + EmbedLinks = 0x00_00_40_00, + /// + /// Allows for uploading images and files. + /// + AttachFiles = 0x00_00_80_00, + /// + /// Allows for reading of message history. + /// + ReadMessageHistory = 0x00_01_00_00, + /// + /// Allows for using the @everyone tag to notify all users in a channel, and the @here tag to notify all + /// online users in a channel. + /// + MentionEveryone = 0x00_02_00_00, + /// + /// Allows the usage of custom emojis from other servers. + /// + UseExternalEmojis = 0x00_04_00_00, + + // Voice + /// + /// Allows for joining of a voice channel. + /// + Connect = 0x00_10_00_00, + /// + /// Allows for speaking in a voice channel. + /// + Speak = 0x00_20_00_00, + /// + /// Allows for muting members in a voice channel. + /// + MuteMembers = 0x00_40_00_00, + /// + /// Allows for deafening of members in a voice channel. + /// + DeafenMembers = 0x00_80_00_00, + /// + /// Allows for moving of members between voice channels. + /// + MoveMembers = 0x01_00_00_00, + /// + /// Allows for using voice-activity-detection in a voice channel. + /// + UseVAD = 0x02_00_00_00, + PrioritySpeaker = 0x00_00_01_00, + /// + /// Allows video streaming in a voice channel. + /// + Stream = 0x00_00_02_00, + + // More General + /// + /// Allows management and editing of roles. + /// + ManageRoles = 0x10_00_00_00, + /// + /// Allows management and editing of webhooks. + /// + ManageWebhooks = 0x20_00_00_00, + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs new file mode 100644 index 0000000..99885b0 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs @@ -0,0 +1,242 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Discord +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public struct ChannelPermissions + { + /// Gets a blank that grants no permissions. + /// A structure that does not contain any set permissions. + public static readonly ChannelPermissions None = new ChannelPermissions(); + /// Gets a that grants all permissions for text channels. + public static readonly ChannelPermissions Text = new ChannelPermissions(0b01100_0000000_1111111110001_010001); + /// Gets a that grants all permissions for voice channels. + public static readonly ChannelPermissions Voice = new ChannelPermissions(0b00100_1111110_0000000011100_010001); + /// Gets a that grants all permissions for category channels. + public static readonly ChannelPermissions Category = new ChannelPermissions(0b01100_1111110_1111111110001_010001); + /// Gets a that grants all permissions for direct message channels. + public static readonly ChannelPermissions DM = new ChannelPermissions(0b00000_1000110_1011100110000_000000); + /// Gets a that grants all permissions for group channels. + public static readonly ChannelPermissions Group = new ChannelPermissions(0b00000_1000110_0001101100000_000000); + /// Gets a that grants all permissions for a given channel type. + /// Unknown channel type. + public static ChannelPermissions All(IChannel channel) + { + switch (channel) + { + case ITextChannel _: return Text; + case IVoiceChannel _: return Voice; + case ICategoryChannel _: return Category; + case IDMChannel _: return DM; + case IGroupChannel _: return Group; + default: throw new ArgumentException(message: "Unknown channel type.", paramName: nameof(channel)); + } + } + + /// Gets a packed value representing all the permissions in this . + public ulong RawValue { get; } + + /// If true, a user may create invites. + public bool CreateInstantInvite => Permissions.GetValue(RawValue, ChannelPermission.CreateInstantInvite); + /// If true, a user may create, delete and modify this channel. + public bool ManageChannel => Permissions.GetValue(RawValue, ChannelPermission.ManageChannels); + + /// If true, a user may add reactions. + public bool AddReactions => Permissions.GetValue(RawValue, ChannelPermission.AddReactions); + /// If true, a user may join channels. + [Obsolete("Use ViewChannel instead.")] + public bool ReadMessages => ViewChannel; + /// If true, a user may view channels. + public bool ViewChannel => Permissions.GetValue(RawValue, ChannelPermission.ViewChannel); + + /// If true, a user may send messages. + public bool SendMessages => Permissions.GetValue(RawValue, ChannelPermission.SendMessages); + /// If true, a user may send text-to-speech messages. + public bool SendTTSMessages => Permissions.GetValue(RawValue, ChannelPermission.SendTTSMessages); + /// If true, a user may delete messages. + public bool ManageMessages => Permissions.GetValue(RawValue, ChannelPermission.ManageMessages); + /// If true, Discord will auto-embed links sent by this user. + public bool EmbedLinks => Permissions.GetValue(RawValue, ChannelPermission.EmbedLinks); + /// If true, a user may send files. + public bool AttachFiles => Permissions.GetValue(RawValue, ChannelPermission.AttachFiles); + /// If true, a user may read previous messages. + public bool ReadMessageHistory => Permissions.GetValue(RawValue, ChannelPermission.ReadMessageHistory); + /// If true, a user may mention @everyone. + public bool MentionEveryone => Permissions.GetValue(RawValue, ChannelPermission.MentionEveryone); + /// If true, a user may use custom emoji from other guilds. + public bool UseExternalEmojis => Permissions.GetValue(RawValue, ChannelPermission.UseExternalEmojis); + + /// If true, a user may connect to a voice channel. + public bool Connect => Permissions.GetValue(RawValue, ChannelPermission.Connect); + /// If true, a user may speak in a voice channel. + public bool Speak => Permissions.GetValue(RawValue, ChannelPermission.Speak); + /// If true, a user may mute users. + public bool MuteMembers => Permissions.GetValue(RawValue, ChannelPermission.MuteMembers); + /// If true, a user may deafen users. + public bool DeafenMembers => Permissions.GetValue(RawValue, ChannelPermission.DeafenMembers); + /// If true, a user may move other users between voice channels. + public bool MoveMembers => Permissions.GetValue(RawValue, ChannelPermission.MoveMembers); + /// If true, a user may use voice-activity-detection rather than push-to-talk. + public bool UseVAD => Permissions.GetValue(RawValue, ChannelPermission.UseVAD); + /// If true, a user may use priority speaker in a voice channel. + public bool PrioritySpeaker => Permissions.GetValue(RawValue, ChannelPermission.PrioritySpeaker); + /// If true, a user may stream video in a voice channel. + public bool Stream => Permissions.GetValue(RawValue, ChannelPermission.Stream); + + /// If true, a user may adjust role permissions. This also implictly grants all other permissions. + public bool ManageRoles => Permissions.GetValue(RawValue, ChannelPermission.ManageRoles); + /// If true, a user may edit the webhooks for this channel. + public bool ManageWebhooks => Permissions.GetValue(RawValue, ChannelPermission.ManageWebhooks); + + /// Creates a new with the provided packed value. + public ChannelPermissions(ulong rawValue) { RawValue = rawValue; } + + private ChannelPermissions(ulong initialValue, + bool? createInstantInvite = null, + bool? manageChannel = null, + bool? addReactions = null, + bool? viewChannel = null, + bool? sendMessages = null, + bool? sendTTSMessages = null, + bool? manageMessages = null, + 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? prioritySpeaker = null, + bool? stream = null, + bool? manageRoles = null, + bool? manageWebhooks = null) + { + ulong value = initialValue; + + Permissions.SetValue(ref value, createInstantInvite, ChannelPermission.CreateInstantInvite); + Permissions.SetValue(ref value, manageChannel, ChannelPermission.ManageChannels); + Permissions.SetValue(ref value, addReactions, ChannelPermission.AddReactions); + Permissions.SetValue(ref value, viewChannel, ChannelPermission.ViewChannel); + Permissions.SetValue(ref value, sendMessages, ChannelPermission.SendMessages); + Permissions.SetValue(ref value, sendTTSMessages, ChannelPermission.SendTTSMessages); + Permissions.SetValue(ref value, manageMessages, ChannelPermission.ManageMessages); + Permissions.SetValue(ref value, embedLinks, ChannelPermission.EmbedLinks); + Permissions.SetValue(ref value, attachFiles, ChannelPermission.AttachFiles); + Permissions.SetValue(ref value, readMessageHistory, ChannelPermission.ReadMessageHistory); + Permissions.SetValue(ref value, mentionEveryone, ChannelPermission.MentionEveryone); + Permissions.SetValue(ref value, useExternalEmojis, ChannelPermission.UseExternalEmojis); + Permissions.SetValue(ref value, connect, ChannelPermission.Connect); + Permissions.SetValue(ref value, speak, ChannelPermission.Speak); + Permissions.SetValue(ref value, muteMembers, ChannelPermission.MuteMembers); + Permissions.SetValue(ref value, deafenMembers, ChannelPermission.DeafenMembers); + Permissions.SetValue(ref value, moveMembers, ChannelPermission.MoveMembers); + Permissions.SetValue(ref value, useVoiceActivation, ChannelPermission.UseVAD); + Permissions.SetValue(ref value, prioritySpeaker, ChannelPermission.PrioritySpeaker); + Permissions.SetValue(ref value, stream, ChannelPermission.Stream); + Permissions.SetValue(ref value, manageRoles, ChannelPermission.ManageRoles); + Permissions.SetValue(ref value, manageWebhooks, ChannelPermission.ManageWebhooks); + + RawValue = value; + } + + /// Creates a new with the provided permissions. + public ChannelPermissions( + bool createInstantInvite = false, + bool manageChannel = false, + bool addReactions = false, + bool viewChannel = false, + bool sendMessages = false, + bool sendTTSMessages = false, + bool manageMessages = false, + 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 prioritySpeaker = false, + bool stream = false, + bool manageRoles = false, + bool manageWebhooks = false) + : this(0, createInstantInvite, manageChannel, addReactions, viewChannel, sendMessages, sendTTSMessages, manageMessages, + embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, + speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, prioritySpeaker, stream, manageRoles, manageWebhooks) + { } + + /// Creates a new from this one, changing the provided non-null permissions. + public ChannelPermissions Modify( + bool? createInstantInvite = null, + bool? manageChannel = null, + bool? addReactions = null, + bool? viewChannel = null, + bool? sendMessages = null, + bool? sendTTSMessages = null, + bool? manageMessages = null, + 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? prioritySpeaker = null, + bool? stream = null, + bool? manageRoles = null, + bool? manageWebhooks = null) + => new ChannelPermissions(RawValue, + createInstantInvite, + manageChannel, + addReactions, + viewChannel, + sendMessages, + sendTTSMessages, + manageMessages, + embedLinks, + attachFiles, + readMessageHistory, + mentionEveryone, + useExternalEmojis, + connect, + speak, + muteMembers, + deafenMembers, + moveMembers, + useVoiceActivation, + prioritySpeaker, + stream, + manageRoles, + manageWebhooks); + + public bool Has(ChannelPermission permission) => Permissions.GetValue(RawValue, permission); + + public List ToList() + { + var perms = new List(); + for (byte i = 0; i < Permissions.MaxBits; i++) + { + ulong flag = ((ulong)1 << i); + if ((RawValue & flag) != 0) + perms.Add((ChannelPermission)flag); + } + return perms; + } + + public override string ToString() => RawValue.ToString(); + private string DebuggerDisplay => $"{string.Join(", ", ToList())}"; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Permissions/GuildPermission.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Permissions/GuildPermission.cs new file mode 100644 index 0000000..3c8a5e8 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Permissions/GuildPermission.cs @@ -0,0 +1,168 @@ +using System; + +namespace Discord +{ + /// Defines the available permissions for a channel. + [Flags] + public enum GuildPermission : ulong + { + // General + /// + /// Allows creation of instant invites. + /// + CreateInstantInvite = 0x00_00_00_01, + /// + /// Allows kicking members. + /// + /// + /// This permission requires the owner account to use two-factor + /// authentication when used on a guild that has server-wide 2FA enabled. + /// + KickMembers = 0x00_00_00_02, + /// + /// Allows banning members. + /// + /// + /// This permission requires the owner account to use two-factor + /// authentication when used on a guild that has server-wide 2FA enabled. + /// + BanMembers = 0x00_00_00_04, + /// + /// Allows all permissions and bypasses channel permission overwrites. + /// + /// + /// This permission requires the owner account to use two-factor + /// authentication when used on a guild that has server-wide 2FA enabled. + /// + Administrator = 0x00_00_00_08, + /// + /// Allows management and editing of channels. + /// + /// + /// This permission requires the owner account to use two-factor + /// authentication when used on a guild that has server-wide 2FA enabled. + /// + ManageChannels = 0x00_00_00_10, + /// + /// Allows management and editing of the guild. + /// + /// + /// This permission requires the owner account to use two-factor + /// authentication when used on a guild that has server-wide 2FA enabled. + /// + ManageGuild = 0x00_00_00_20, + + // Text + /// + /// Allows for the addition of reactions to messages. + /// + AddReactions = 0x00_00_00_40, + /// + /// Allows for viewing of audit logs. + /// + ViewAuditLog = 0x00_00_00_80, + [Obsolete("Use ViewChannel instead.")] + ReadMessages = ViewChannel, + ViewChannel = 0x00_00_04_00, + SendMessages = 0x00_00_08_00, + /// + /// Allows for sending of text-to-speech messages. + /// + SendTTSMessages = 0x00_00_10_00, + /// + /// Allows for deletion of other users messages. + /// + /// + /// This permission requires the owner account to use two-factor + /// authentication when used on a guild that has server-wide 2FA enabled. + /// + ManageMessages = 0x00_00_20_00, + /// + /// Allows links sent by users with this permission will be auto-embedded. + /// + EmbedLinks = 0x00_00_40_00, + /// + /// Allows for uploading images and files. + /// + AttachFiles = 0x00_00_80_00, + /// + /// Allows for reading of message history. + /// + ReadMessageHistory = 0x00_01_00_00, + /// + /// Allows for using the @everyone tag to notify all users in a channel, and the @here tag to notify all + /// online users in a channel. + /// + MentionEveryone = 0x00_02_00_00, + /// + /// Allows the usage of custom emojis from other servers. + /// + UseExternalEmojis = 0x00_04_00_00, + + + // Voice + /// + /// Allows for joining of a voice channel. + /// + Connect = 0x00_10_00_00, + /// + /// Allows for speaking in a voice channel. + /// + Speak = 0x00_20_00_00, + /// + /// Allows for muting members in a voice channel. + /// + MuteMembers = 0x00_40_00_00, + /// + /// Allows for deafening of members in a voice channel. + /// + DeafenMembers = 0x00_80_00_00, + /// + /// Allows for moving of members between voice channels. + /// + MoveMembers = 0x01_00_00_00, + /// + /// Allows for using voice-activity-detection in a voice channel. + /// + UseVAD = 0x02_00_00_00, + PrioritySpeaker = 0x00_00_01_00, + /// + /// Allows video streaming in a voice channel. + /// + Stream = 0x00_00_02_00, + + // General 2 + /// + /// Allows for modification of own nickname. + /// + ChangeNickname = 0x04_00_00_00, + /// + /// Allows for modification of other users nicknames. + /// + ManageNicknames = 0x08_00_00_00, + /// + /// Allows management and editing of roles. + /// + /// + /// This permission requires the owner account to use two-factor + /// authentication when used on a guild that has server-wide 2FA enabled. + /// + ManageRoles = 0x10_00_00_00, + /// + /// Allows management and editing of webhooks. + /// + /// + /// This permission requires the owner account to use two-factor + /// authentication when used on a guild that has server-wide 2FA enabled. + /// + ManageWebhooks = 0x20_00_00_00, + /// + /// Allows management and editing of emojis. + /// + /// + /// This permission requires the owner account to use two-factor + /// authentication when used on a guild that has server-wide 2FA enabled. + /// + ManageEmojis = 0x40_00_00_00 + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs new file mode 100644 index 0000000..a5adad4 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs @@ -0,0 +1,292 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Discord +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public struct GuildPermissions + { + /// Gets a blank that grants no permissions. + public static readonly GuildPermissions None = new GuildPermissions(); + /// Gets a that grants all guild permissions for webhook users. + public static readonly GuildPermissions Webhook = new GuildPermissions(0b00000_0000000_0001101100000_000000); + /// Gets a that grants all guild permissions. + public static readonly GuildPermissions All = new GuildPermissions(0b11111_1111110_1111111111111_111111); + + /// Gets a packed value representing all the permissions in this . + public ulong RawValue { get; } + + /// If true, a user may create invites. + public bool CreateInstantInvite => Permissions.GetValue(RawValue, GuildPermission.CreateInstantInvite); + /// If true, a user may ban users from the guild. + public bool BanMembers => Permissions.GetValue(RawValue, GuildPermission.BanMembers); + /// If true, a user may kick users from the guild. + public bool KickMembers => Permissions.GetValue(RawValue, GuildPermission.KickMembers); + /// If true, a user is granted all permissions, and cannot have them revoked via channel permissions. + public bool Administrator => Permissions.GetValue(RawValue, GuildPermission.Administrator); + /// If true, a user may create, delete and modify channels. + public bool ManageChannels => Permissions.GetValue(RawValue, GuildPermission.ManageChannels); + /// If true, a user may adjust guild properties. + public bool ManageGuild => Permissions.GetValue(RawValue, GuildPermission.ManageGuild); + + /// If true, a user may add reactions. + public bool AddReactions => Permissions.GetValue(RawValue, GuildPermission.AddReactions); + /// If true, a user may view the audit log. + public bool ViewAuditLog => Permissions.GetValue(RawValue, GuildPermission.ViewAuditLog); + + /// If True, a user may join channels. + [Obsolete("Use ViewChannel instead.")] + public bool ReadMessages => ViewChannel; + /// If True, a user may view channels. + public bool ViewChannel => Permissions.GetValue(RawValue, GuildPermission.ViewChannel); + /// If True, a user may send messages. + public bool SendMessages => Permissions.GetValue(RawValue, GuildPermission.SendMessages); + /// If true, a user may send text-to-speech messages. + public bool SendTTSMessages => Permissions.GetValue(RawValue, GuildPermission.SendTTSMessages); + /// If true, a user may delete messages. + public bool ManageMessages => Permissions.GetValue(RawValue, GuildPermission.ManageMessages); + /// If true, Discord will auto-embed links sent by this user. + public bool EmbedLinks => Permissions.GetValue(RawValue, GuildPermission.EmbedLinks); + /// If true, a user may send files. + public bool AttachFiles => Permissions.GetValue(RawValue, GuildPermission.AttachFiles); + /// If true, a user may read previous messages. + public bool ReadMessageHistory => Permissions.GetValue(RawValue, GuildPermission.ReadMessageHistory); + /// If true, a user may mention @everyone. + public bool MentionEveryone => Permissions.GetValue(RawValue, GuildPermission.MentionEveryone); + /// If true, a user may use custom emoji from other guilds. + public bool UseExternalEmojis => Permissions.GetValue(RawValue, GuildPermission.UseExternalEmojis); + + /// If true, a user may connect to a voice channel. + public bool Connect => Permissions.GetValue(RawValue, GuildPermission.Connect); + /// If true, a user may speak in a voice channel. + public bool Speak => Permissions.GetValue(RawValue, GuildPermission.Speak); + /// If true, a user may mute users. + public bool MuteMembers => Permissions.GetValue(RawValue, GuildPermission.MuteMembers); + /// If true, a user may deafen users. + public bool DeafenMembers => Permissions.GetValue(RawValue, GuildPermission.DeafenMembers); + /// If true, a user may move other users between voice channels. + public bool MoveMembers => Permissions.GetValue(RawValue, GuildPermission.MoveMembers); + /// If true, a user may use voice-activity-detection rather than push-to-talk. + public bool UseVAD => Permissions.GetValue(RawValue, GuildPermission.UseVAD); + /// If True, a user may use priority speaker in a voice channel. + public bool PrioritySpeaker => Permissions.GetValue(RawValue, GuildPermission.PrioritySpeaker); + /// If True, a user may stream video in a voice channel. + public bool Stream => Permissions.GetValue(RawValue, GuildPermission.Stream); + + /// If true, a user may change their own nickname. + public bool ChangeNickname => Permissions.GetValue(RawValue, GuildPermission.ChangeNickname); + /// If true, a user may change the nickname of other users. + 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 webhooks 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 with the provided packed value. + public GuildPermissions(ulong rawValue) { RawValue = rawValue; } + + private GuildPermissions(ulong initialValue, + bool? createInstantInvite = null, + bool? kickMembers = null, + bool? banMembers = null, + bool? administrator = null, + bool? manageChannels = null, + bool? manageGuild = null, + bool? addReactions = null, + bool? viewAuditLog = null, + bool? viewChannel = null, + bool? sendMessages = null, + bool? sendTTSMessages = null, + bool? manageMessages = null, + 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? prioritySpeaker = null, + bool? stream = null, + bool? changeNickname = null, + bool? manageNicknames = null, + bool? manageRoles = null, + bool? manageWebhooks = null, + bool? manageEmojis = null) + { + ulong value = initialValue; + + Permissions.SetValue(ref value, createInstantInvite, GuildPermission.CreateInstantInvite); + Permissions.SetValue(ref value, banMembers, GuildPermission.BanMembers); + Permissions.SetValue(ref value, kickMembers, GuildPermission.KickMembers); + Permissions.SetValue(ref value, administrator, GuildPermission.Administrator); + Permissions.SetValue(ref value, manageChannels, GuildPermission.ManageChannels); + Permissions.SetValue(ref value, manageGuild, GuildPermission.ManageGuild); + Permissions.SetValue(ref value, addReactions, GuildPermission.AddReactions); + Permissions.SetValue(ref value, viewAuditLog, GuildPermission.ViewAuditLog); + Permissions.SetValue(ref value, viewChannel, GuildPermission.ViewChannel); + Permissions.SetValue(ref value, sendMessages, GuildPermission.SendMessages); + Permissions.SetValue(ref value, sendTTSMessages, GuildPermission.SendTTSMessages); + Permissions.SetValue(ref value, manageMessages, GuildPermission.ManageMessages); + Permissions.SetValue(ref value, embedLinks, GuildPermission.EmbedLinks); + Permissions.SetValue(ref value, attachFiles, GuildPermission.AttachFiles); + Permissions.SetValue(ref value, readMessageHistory, GuildPermission.ReadMessageHistory); + Permissions.SetValue(ref value, mentionEveryone, GuildPermission.MentionEveryone); + Permissions.SetValue(ref value, useExternalEmojis, GuildPermission.UseExternalEmojis); + Permissions.SetValue(ref value, connect, GuildPermission.Connect); + Permissions.SetValue(ref value, speak, GuildPermission.Speak); + Permissions.SetValue(ref value, muteMembers, GuildPermission.MuteMembers); + Permissions.SetValue(ref value, deafenMembers, GuildPermission.DeafenMembers); + Permissions.SetValue(ref value, moveMembers, GuildPermission.MoveMembers); + Permissions.SetValue(ref value, useVoiceActivation, GuildPermission.UseVAD); + Permissions.SetValue(ref value, prioritySpeaker, GuildPermission.PrioritySpeaker); + Permissions.SetValue(ref value, stream, GuildPermission.Stream); + 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; + } + + /// Creates a new structure with the provided permissions. + public GuildPermissions( + bool createInstantInvite = false, + bool kickMembers = false, + bool banMembers = false, + bool administrator = false, + bool manageChannels = false, + bool manageGuild = false, + bool addReactions = false, + bool viewAuditLog = false, + bool viewChannel = false, + bool sendMessages = false, + bool sendTTSMessages = false, + bool manageMessages = false, + 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 prioritySpeaker = false, + bool stream = false, + bool changeNickname = false, + bool manageNicknames = false, + bool manageRoles = false, + bool manageWebhooks = false, + bool manageEmojis = false) + : this(0, + createInstantInvite: createInstantInvite, + manageRoles: manageRoles, + kickMembers: kickMembers, + banMembers: banMembers, + administrator: administrator, + manageChannels: manageChannels, + manageGuild: manageGuild, + addReactions: addReactions, + viewAuditLog: viewAuditLog, + viewChannel: viewChannel, + sendMessages: sendMessages, + sendTTSMessages: sendTTSMessages, + manageMessages: manageMessages, + embedLinks: embedLinks, + attachFiles: attachFiles, + readMessageHistory: readMessageHistory, + mentionEveryone: mentionEveryone, + useExternalEmojis: useExternalEmojis, + connect: connect, + speak: speak, + muteMembers: muteMembers, + deafenMembers: deafenMembers, + moveMembers: moveMembers, + useVoiceActivation: useVoiceActivation, + prioritySpeaker: prioritySpeaker, + stream: stream, + changeNickname: changeNickname, + manageNicknames: manageNicknames, + manageWebhooks: manageWebhooks, + manageEmojis: manageEmojis) + { } + + /// Creates a new from this one, changing the provided non-null permissions. + public GuildPermissions Modify( + bool? createInstantInvite = null, + bool? kickMembers = null, + bool? banMembers = null, + bool? administrator = null, + bool? manageChannels = null, + bool? manageGuild = null, + bool? addReactions = null, + bool? viewAuditLog = null, + bool? viewChannel = null, + bool? sendMessages = null, + bool? sendTTSMessages = null, + bool? manageMessages = null, + 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? prioritySpeaker = null, + bool? stream = null, + bool? changeNickname = null, + bool? manageNicknames = null, + bool? manageRoles = null, + bool? manageWebhooks = null, + bool? manageEmojis = null) + => new GuildPermissions(RawValue, createInstantInvite, kickMembers, banMembers, administrator, manageChannels, manageGuild, addReactions, + viewAuditLog, viewChannel, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, + readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, moveMembers, + useVoiceActivation, prioritySpeaker, stream, changeNickname, manageNicknames, manageRoles, manageWebhooks, manageEmojis); + + /// + /// Returns a value that indicates if a specific is enabled + /// in these permissions. + /// + /// The permission value to check for. + /// true if the permission is enabled, false otherwise. + public bool Has(GuildPermission permission) => Permissions.GetValue(RawValue, permission); + + /// + /// Returns a containing all of the + /// flags that are enabled. + /// + /// A containing flags. Empty if none are enabled. + public List ToList() + { + var perms = new List(); + + // bitwise operations on raw value + // each of the GuildPermissions increments by 2^i from 0 to MaxBits + for (byte i = 0; i < Permissions.MaxBits; i++) + { + ulong flag = ((ulong)1 << i); + if ((RawValue & flag) != 0) + perms.Add((GuildPermission)flag); + } + return perms; + } + + public override string ToString() => RawValue.ToString(); + private string DebuggerDisplay => $"{string.Join(", ", ToList())}"; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Permissions/Overwrite.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Permissions/Overwrite.cs new file mode 100644 index 0000000..f8f3fff --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Permissions/Overwrite.cs @@ -0,0 +1,31 @@ +namespace Discord +{ + /// + /// Represent a permission object. + /// + public struct Overwrite + { + /// + /// Gets the unique identifier for the object this overwrite is targeting. + /// + public ulong TargetId { get; } + /// + /// Gets the type of object this overwrite is targeting. + /// + public PermissionTarget TargetType { get; } + /// + /// Gets the permissions associated with this overwrite entry. + /// + public OverwritePermissions Permissions { get; } + + /// + /// Initializes a new with provided target information and modified permissions. + /// + public Overwrite(ulong targetId, PermissionTarget targetType, OverwritePermissions permissions) + { + TargetId = targetId; + TargetType = targetType; + Permissions = permissions; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs new file mode 100644 index 0000000..04bb2f6 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Discord +{ + /// + /// Represents a container for a series of overwrite permissions. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public struct OverwritePermissions + { + /// + /// Gets a blank that inherits all permissions. + /// + public static OverwritePermissions InheritAll { get; } = new OverwritePermissions(); + /// + /// Gets a that grants all permissions for the given channel. + /// + /// Unknown channel type. + public static OverwritePermissions AllowAll(IChannel channel) + => new OverwritePermissions(ChannelPermissions.All(channel).RawValue, 0); + /// + /// Gets a that denies all permissions for the given channel. + /// + /// Unknown channel type. + public static OverwritePermissions DenyAll(IChannel channel) + => new OverwritePermissions(0, ChannelPermissions.All(channel).RawValue); + + /// + /// Gets a packed value representing all the allowed permissions in this . + /// + public ulong AllowValue { get; } + /// + /// Gets a packed value representing all the denied permissions in this . + /// + public ulong DenyValue { get; } + + /// If Allowed, a user may create invites. + public PermValue CreateInstantInvite => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.CreateInstantInvite); + /// If Allowed, a user may create, delete and modify this channel. + public PermValue ManageChannel => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ManageChannels); + /// If Allowed, a user may add reactions. + public PermValue AddReactions => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.AddReactions); + /// If Allowed, a user may join channels. + [Obsolete("Use ViewChannel instead.")] + public PermValue ReadMessages => ViewChannel; + /// If Allowed, a user may join channels. + public PermValue ViewChannel => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ViewChannel); + /// If Allowed, a user may send messages. + public PermValue SendMessages => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.SendMessages); + /// If Allowed, a user may send text-to-speech messages. + public PermValue SendTTSMessages => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.SendTTSMessages); + /// If Allowed, a user may delete messages. + public PermValue ManageMessages => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ManageMessages); + /// If Allowed, Discord will auto-embed links sent by this user. + public PermValue EmbedLinks => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.EmbedLinks); + /// If Allowed, a user may send files. + public PermValue AttachFiles => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.AttachFiles); + /// If Allowed, a user may read previous messages. + public PermValue ReadMessageHistory => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ReadMessageHistory); + /// If Allowed, a user may mention @everyone. + public PermValue MentionEveryone => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.MentionEveryone); + /// If Allowed, a user may use custom emoji from other guilds. + public PermValue UseExternalEmojis => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.UseExternalEmojis); + + /// If Allowed, a user may connect to a voice channel. + public PermValue Connect => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.Connect); + /// If Allowed, a user may speak in a voice channel. + public PermValue Speak => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.Speak); + /// If Allowed, a user may mute users. + public PermValue MuteMembers => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.MuteMembers); + /// If Allowed, a user may deafen users. + public PermValue DeafenMembers => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.DeafenMembers); + /// If Allowed, a user may move other users between voice channels. + public PermValue MoveMembers => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.MoveMembers); + /// If Allowed, a user may use voice-activity-detection rather than push-to-talk. + public PermValue UseVAD => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.UseVAD); + + /// If Allowed, a user may adjust role permissions. This also implicitly grants all other permissions. + public PermValue ManageRoles => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ManageRoles); + /// If True, a user may edit the webhooks for this channel. + public PermValue ManageWebhooks => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ManageWebhooks); + + /// Creates a new OverwritePermissions with the provided allow and deny packed values. + public OverwritePermissions(ulong allowValue, ulong denyValue) + { + AllowValue = allowValue; + DenyValue = denyValue; + } + + private OverwritePermissions(ulong allowValue, ulong denyValue, + PermValue? createInstantInvite = null, + PermValue? manageChannel = null, + PermValue? addReactions = null, + PermValue? viewChannel = null, + PermValue? sendMessages = null, + PermValue? sendTTSMessages = null, + PermValue? manageMessages = null, + PermValue? embedLinks = null, + PermValue? attachFiles = null, + PermValue? readMessageHistory = null, + PermValue? mentionEveryone = null, + PermValue? useExternalEmojis = null, + PermValue? connect = null, + PermValue? speak = null, + PermValue? muteMembers = null, + PermValue? deafenMembers = null, + PermValue? moveMembers = null, + PermValue? useVoiceActivation = null, + PermValue? manageRoles = null, + PermValue? manageWebhooks = null) + { + Permissions.SetValue(ref allowValue, ref denyValue, createInstantInvite, ChannelPermission.CreateInstantInvite); + Permissions.SetValue(ref allowValue, ref denyValue, manageChannel, ChannelPermission.ManageChannels); + Permissions.SetValue(ref allowValue, ref denyValue, addReactions, ChannelPermission.AddReactions); + Permissions.SetValue(ref allowValue, ref denyValue, viewChannel, ChannelPermission.ViewChannel); + Permissions.SetValue(ref allowValue, ref denyValue, sendMessages, ChannelPermission.SendMessages); + Permissions.SetValue(ref allowValue, ref denyValue, sendTTSMessages, ChannelPermission.SendTTSMessages); + Permissions.SetValue(ref allowValue, ref denyValue, manageMessages, ChannelPermission.ManageMessages); + Permissions.SetValue(ref allowValue, ref denyValue, embedLinks, ChannelPermission.EmbedLinks); + Permissions.SetValue(ref allowValue, ref denyValue, attachFiles, ChannelPermission.AttachFiles); + Permissions.SetValue(ref allowValue, ref denyValue, readMessageHistory, ChannelPermission.ReadMessageHistory); + Permissions.SetValue(ref allowValue, ref denyValue, mentionEveryone, ChannelPermission.MentionEveryone); + Permissions.SetValue(ref allowValue, ref denyValue, useExternalEmojis, ChannelPermission.UseExternalEmojis); + Permissions.SetValue(ref allowValue, ref denyValue, connect, ChannelPermission.Connect); + Permissions.SetValue(ref allowValue, ref denyValue, speak, ChannelPermission.Speak); + Permissions.SetValue(ref allowValue, ref denyValue, muteMembers, ChannelPermission.MuteMembers); + Permissions.SetValue(ref allowValue, ref denyValue, deafenMembers, ChannelPermission.DeafenMembers); + Permissions.SetValue(ref allowValue, ref denyValue, moveMembers, ChannelPermission.MoveMembers); + Permissions.SetValue(ref allowValue, ref denyValue, useVoiceActivation, ChannelPermission.UseVAD); + Permissions.SetValue(ref allowValue, ref denyValue, manageRoles, ChannelPermission.ManageRoles); + Permissions.SetValue(ref allowValue, ref denyValue, manageWebhooks, ChannelPermission.ManageWebhooks); + + AllowValue = allowValue; + DenyValue = denyValue; + } + + /// + /// Initializes a new struct with the provided permissions. + /// + public OverwritePermissions( + PermValue createInstantInvite = PermValue.Inherit, + PermValue manageChannel = PermValue.Inherit, + PermValue addReactions = PermValue.Inherit, + PermValue viewChannel = PermValue.Inherit, + PermValue sendMessages = PermValue.Inherit, + PermValue sendTTSMessages = PermValue.Inherit, + PermValue manageMessages = PermValue.Inherit, + PermValue embedLinks = PermValue.Inherit, + PermValue attachFiles = PermValue.Inherit, + PermValue readMessageHistory = PermValue.Inherit, + PermValue mentionEveryone = PermValue.Inherit, + PermValue useExternalEmojis = PermValue.Inherit, + PermValue connect = PermValue.Inherit, + PermValue speak = PermValue.Inherit, + PermValue muteMembers = PermValue.Inherit, + PermValue deafenMembers = PermValue.Inherit, + PermValue moveMembers = PermValue.Inherit, + PermValue useVoiceActivation = PermValue.Inherit, + PermValue manageRoles = PermValue.Inherit, + PermValue manageWebhooks = PermValue.Inherit) + : this(0, 0, createInstantInvite, manageChannel, addReactions, viewChannel, sendMessages, sendTTSMessages, manageMessages, + embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, + moveMembers, useVoiceActivation, manageRoles, manageWebhooks) { } + + /// + /// Initializes a new from the current one, changing the provided + /// non-null permissions. + /// + public OverwritePermissions Modify( + PermValue? createInstantInvite = null, + PermValue? manageChannel = null, + PermValue? addReactions = null, + PermValue? viewChannel = null, + PermValue? sendMessages = null, + PermValue? sendTTSMessages = null, + PermValue? manageMessages = null, + PermValue? embedLinks = null, + PermValue? attachFiles = null, + PermValue? readMessageHistory = null, + PermValue? mentionEveryone = null, + PermValue? useExternalEmojis = null, + PermValue? connect = null, + PermValue? speak = null, + PermValue? muteMembers = null, + PermValue? deafenMembers = null, + PermValue? moveMembers = null, + PermValue? useVoiceActivation = null, + PermValue? manageRoles = null, + PermValue? manageWebhooks = null) + => new OverwritePermissions(AllowValue, DenyValue, createInstantInvite, manageChannel, addReactions, viewChannel, sendMessages, sendTTSMessages, manageMessages, + embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, + moveMembers, useVoiceActivation, manageRoles, manageWebhooks); + + /// + /// Creates a of all the values that are allowed. + /// + /// A of all allowed flags. If none, the list will be empty. + public List ToAllowList() + { + var perms = new List(); + for (byte i = 0; i < Permissions.MaxBits; i++) + { + // first operand must be long or ulong to shift >31 bits + ulong flag = ((ulong)1 << i); + if ((AllowValue & flag) != 0) + perms.Add((ChannelPermission)flag); + } + return perms; + } + + /// + /// Creates a of all the values that are denied. + /// + /// A of all denied flags. If none, the list will be empty. + public List ToDenyList() + { + var perms = new List(); + for (byte i = 0; i < Permissions.MaxBits; i++) + { + ulong flag = ((ulong)1 << i); + if ((DenyValue & flag) != 0) + perms.Add((ChannelPermission)flag); + } + return perms; + } + + public override string ToString() => $"Allow {AllowValue}, Deny {DenyValue}"; + private string DebuggerDisplay => + $"Allow {string.Join(", ", ToAllowList())}, " + + $"Deny {string.Join(", ", ToDenyList())}"; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Permissions/PermValue.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Permissions/PermValue.cs new file mode 100644 index 0000000..6cea827 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Permissions/PermValue.cs @@ -0,0 +1,13 @@ +namespace Discord +{ + /// Specifies the permission value. + public enum PermValue + { + /// Allows this permission. + Allow, + /// Denies this permission. + Deny, + /// Inherits the permission settings. + Inherit + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Roles/Color.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Roles/Color.cs new file mode 100644 index 0000000..7c2d152 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Roles/Color.cs @@ -0,0 +1,208 @@ +using System; +using System.Diagnostics; +using StandardColor = System.Drawing.Color; + +namespace Discord +{ + /// + /// Represents a color used in Discord. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public struct Color + { + /// Gets the default user color value. + public static readonly Color Default = new Color(0); + /// Gets the teal color value. + /// A color struct with the hex value of 1ABC9C. + public static readonly Color Teal = new Color(0x1ABC9C); + /// Gets the dark teal color value. + /// A color struct with the hex value of 11806A. + public static readonly Color DarkTeal = new Color(0x11806A); + /// Gets the green color value. + /// A color struct with the hex value of 2ECC71. + public static readonly Color Green = new Color(0x2ECC71); + /// Gets the dark green color value. + /// A color struct with the hex value of 1F8B4C. + public static readonly Color DarkGreen = new Color(0x1F8B4C); + /// Gets the blue color value. + /// A color struct with the hex value of 3498DB. + public static readonly Color Blue = new Color(0x3498DB); + /// Gets the dark blue color value. + /// A color struct with the hex value of 206694. + public static readonly Color DarkBlue = new Color(0x206694); + /// Gets the purple color value. + /// A color struct with the hex value of 9B59B6. + public static readonly Color Purple = new Color(0x9B59B6); + /// Gets the dark purple color value. + /// A color struct with the hex value of 71368A. + public static readonly Color DarkPurple = new Color(0x71368A); + /// Gets the magenta color value. + /// A color struct with the hex value of E91E63. + public static readonly Color Magenta = new Color(0xE91E63); + /// Gets the dark magenta color value. + /// A color struct with the hex value of AD1457. + public static readonly Color DarkMagenta = new Color(0xAD1457); + /// Gets the gold color value. + /// A color struct with the hex value of F1C40F. + public static readonly Color Gold = new Color(0xF1C40F); + /// Gets the light orange color value. + /// A color struct with the hex value of C27C0E. + public static readonly Color LightOrange = new Color(0xC27C0E); + /// Gets the orange color value. + /// A color struct with the hex value of E67E22. + public static readonly Color Orange = new Color(0xE67E22); + /// Gets the dark orange color value. + /// A color struct with the hex value of A84300. + public static readonly Color DarkOrange = new Color(0xA84300); + /// Gets the red color value. + /// A color struct with the hex value of E74C3C. + public static readonly Color Red = new Color(0xE74C3C); + /// Gets the dark red color value. + /// A color struct with the hex value of 992D22. + public static readonly Color DarkRed = new Color(0x992D22); + /// Gets the light grey color value. + /// A color struct with the hex value of 979C9F. + public static readonly Color LightGrey = new Color(0x979C9F); + /// Gets the lighter grey color value. + /// A color struct with the hex value of 95A5A6. + public static readonly Color LighterGrey = new Color(0x95A5A6); + /// Gets the dark grey color value. + /// A color struct with the hex value of 607D8B. + public static readonly Color DarkGrey = new Color(0x607D8B); + /// Gets the darker grey color value. + /// A color struct with the hex value of 546E7A. + public static readonly Color DarkerGrey = new Color(0x546E7A); + + /// Gets the encoded value for this color. + /// + /// This value is encoded as an unsigned integer value. The most-significant 8 bits contain the red value, + /// the middle 8 bits contain the green value, and the least-significant 8 bits contain the blue value. + /// + public uint RawValue { get; } + + /// Gets the red component for this color. + public byte R => (byte)(RawValue >> 16); + /// Gets the green component for this color. + public byte G => (byte)(RawValue >> 8); + /// Gets the blue component for this color. + public byte B => (byte)(RawValue); + + /// + /// Initializes a struct with the given raw value. + /// + /// + /// The following will create a color that has a hex value of + /// #607D8B. + /// + /// Color darkGrey = new Color(0x607D8B); + /// + /// + /// The raw value of the color (e.g. 0x607D8B). + public Color(uint rawValue) + { + RawValue = rawValue; + } + /// + /// Initializes a struct with the given RGB bytes. + /// + /// + /// The following will create a color that has a value of + /// #607D8B. + /// + /// Color darkGrey = new Color((byte)0b_01100000, (byte)0b_01111101, (byte)0b_10001011); + /// + /// + /// The byte that represents the red color. + /// The byte that represents the green color. + /// The byte that represents the blue color. + public Color(byte r, byte g, byte b) + { + RawValue = + ((uint)r << 16) | + ((uint)g << 8) | + (uint)b; + } + + /// + /// Initializes a struct with the given RGB value. + /// + /// + /// The following will create a color that has a value of + /// #607D8B. + /// + /// Color darkGrey = new Color(96, 125, 139); + /// + /// + /// The value that represents the red color. Must be within 0~255. + /// The value that represents the green color. Must be within 0~255. + /// The value that represents the blue color. Must be within 0~255. + /// The argument value is not between 0 to 255. + public Color(int r, int g, int b) + { + if (r < 0 || r > 255) + throw new ArgumentOutOfRangeException(nameof(r), "Value must be within [0,255]."); + if (g < 0 || g > 255) + throw new ArgumentOutOfRangeException(nameof(g), "Value must be within [0,255]."); + if (b < 0 || b > 255) + throw new ArgumentOutOfRangeException(nameof(b), "Value must be within [0,255]."); + RawValue = + ((uint)r << 16) | + ((uint)g << 8) | + (uint)b; + } + /// + /// Initializes a struct with the given RGB float value. + /// + /// + /// The following will create a color that has a value of + /// #607c8c. + /// + /// Color darkGrey = new Color(0.38f, 0.49f, 0.55f); + /// + /// + /// The value that represents the red color. Must be within 0~1. + /// The value that represents the green color. Must be within 0~1. + /// The value that represents the blue color. Must be within 0~1. + /// The argument value is not between 0 to 1. + public Color(float r, float g, float b) + { + if (r < 0.0f || r > 1.0f) + throw new ArgumentOutOfRangeException(nameof(r), "Value must be within [0,1]."); + if (g < 0.0f || g > 1.0f) + throw new ArgumentOutOfRangeException(nameof(g), "Value must be within [0,1]."); + if (b < 0.0f || b > 1.0f) + throw new ArgumentOutOfRangeException(nameof(b), "Value must be within [0,1]."); + RawValue = + ((uint)(r * 255.0f) << 16) | + ((uint)(g * 255.0f) << 8) | + (uint)(b * 255.0f); + } + + public static bool operator ==(Color lhs, Color rhs) + => lhs.RawValue == rhs.RawValue; + + public static bool operator !=(Color lhs, Color rhs) + => lhs.RawValue != rhs.RawValue; + + public override bool Equals(object obj) + => (obj is Color c && RawValue == c.RawValue); + + public override int GetHashCode() => RawValue.GetHashCode(); + + public static implicit operator StandardColor(Color color) => + StandardColor.FromArgb((int)color.RawValue); + public static explicit operator Color(StandardColor color) => + new Color((uint)color.ToArgb() << 8 >> 8); + + /// + /// Gets the hexadecimal representation of the color (e.g. #000ccc). + /// + /// + /// A hexadecimal string of the color. + /// + public override string ToString() => + string.Format("#{0:X6}", RawValue); + private string DebuggerDisplay => + string.Format("#{0:X6} ({0})", RawValue); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Roles/IRole.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Roles/IRole.cs new file mode 100644 index 0000000..66556fc --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Roles/IRole.cs @@ -0,0 +1,83 @@ +using System; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a generic role object to be given to a guild user. + /// + public interface IRole : ISnowflakeEntity, IDeletable, IMentionable, IComparable + { + /// + /// Gets the guild that owns this role. + /// + /// + /// A guild representing the parent guild of this role. + /// + IGuild Guild { get; } + + /// + /// Gets the color given to users of this role. + /// + /// + /// A struct representing the color of this role. + /// + Color Color { get; } + /// + /// Gets a value that indicates whether the role can be separated in the user list. + /// + /// + /// true if users of this role are separated in the user list; otherwise false. + /// + bool IsHoisted { get; } + /// + /// Gets a value that indicates whether the role is managed by Discord. + /// + /// + /// true if this role is automatically managed by Discord; otherwise false. + /// + bool IsManaged { get; } + /// + /// Gets a value that indicates whether the role is mentionable. + /// + /// + /// true if this role may be mentioned in messages; otherwise false. + /// + bool IsMentionable { get; } + /// + /// Gets the name of this role. + /// + /// + /// A string containing the name of this role. + /// + string Name { get; } + /// + /// Gets the permissions granted to members of this role. + /// + /// + /// A struct that this role possesses. + /// + GuildPermissions Permissions { get; } + /// + /// Gets this role's position relative to other roles in the same guild. + /// + /// + /// An representing the position of the role in the role list of the guild. + /// + int Position { get; } + + /// + /// Modifies this role. + /// + /// + /// This method modifies this role with the specified properties. To see an example of this + /// method and what properties are available, please refer to . + /// + /// A delegate containing the properties to modify the role with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + Task ModifyAsync(Action func, RequestOptions options = null); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Roles/ReorderRoleProperties.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Roles/ReorderRoleProperties.cs new file mode 100644 index 0000000..0074c0a --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Roles/ReorderRoleProperties.cs @@ -0,0 +1,34 @@ +namespace Discord +{ + /// + /// Properties that are used to reorder an . + /// + public class ReorderRoleProperties + { + /// + /// Gets the identifier of the role to be edited. + /// + /// + /// A representing the snowflake identifier of the role to be modified. + /// + public ulong Id { get; } + /// + /// Gets the new zero-based position of the role. + /// + /// + /// An representing the new zero-based position of the role. + /// + public int Position { get; } + + /// + /// Initializes a with the given role ID and position. + /// + /// The ID of the role to be edited. + /// The new zero-based position of the role. + public ReorderRoleProperties(ulong id, int pos) + { + Id = id; + Position = pos; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Roles/RoleProperties.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Roles/RoleProperties.cs new file mode 100644 index 0000000..a58112b --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Roles/RoleProperties.cs @@ -0,0 +1,61 @@ +namespace Discord +{ + /// + /// Properties that are used to modify an with the specified changes. + /// + /// + /// The following example modifies the role to a mentionable one, renames the role into Sonic, and + /// changes the color to a light-blue. + /// + /// await role.ModifyAsync(x => + /// { + /// x.Name = "Sonic"; + /// x.Color = new Color(0x1A50BC); + /// x.Mentionable = true; + /// }); + /// + /// + /// + public class RoleProperties + { + /// + /// Gets or sets the name of the role. + /// + /// + /// This value may not be set if the role is an @everyone role. + /// + public Optional Name { get; set; } + /// + /// Gets or sets the role's . + /// + public Optional Permissions { get; set; } + /// + /// Gets or sets the position of the role. This is 0-based! + /// + /// + /// This value may not be set if the role is an @everyone role. + /// + public Optional Position { get; set; } + /// + /// Gets or sets the color of the role. + /// + /// + /// This value may not be set if the role is an @everyone role. + /// + public Optional Color { get; set; } + /// + /// Gets or sets whether or not this role should be displayed independently in the user list. + /// + /// + /// This value may not be set if the role is an @everyone role. + /// + public Optional Hoist { get; set; } + /// + /// Gets or sets whether or not this role can be mentioned. + /// + /// + /// This value may not be set if the role is an @everyone role. + /// + public Optional Mentionable { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/AddGuildUserProperties.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/AddGuildUserProperties.cs new file mode 100644 index 0000000..e380d90 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/AddGuildUserProperties.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; + +namespace Discord +{ + /// + /// Properties that are used to add a new to the guild with the following parameters. + /// + /// + public class AddGuildUserProperties + { + /// + /// Gets or sets the user's nickname. + /// + /// + /// To clear the user's nickname, this value can be set to null or + /// . + /// + public Optional Nickname { get; set; } + /// + /// Gets or sets whether the user should be muted in a voice channel. + /// + /// + /// If this value is set to true, no user will be able to hear this user speak in the guild. + /// + public Optional Mute { get; set; } + /// + /// Gets or sets whether the user should be deafened in a voice channel. + /// + /// + /// If this value is set to true, this user will not be able to hear anyone speak in the guild. + /// + public Optional Deaf { get; set; } + /// + /// Gets or sets the roles the user should have. + /// + /// + /// + /// To add a role to a user: + /// + /// + /// + /// To remove a role from a user: + /// + /// + /// + public Optional> Roles { get; set; } + /// + /// Gets or sets the roles the user should have. + /// + /// + /// + /// To add a role to a user: + /// + /// + /// + /// To remove a role from a user: + /// + /// + /// + public Optional> RoleIds { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/ClientType.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/ClientType.cs new file mode 100644 index 0000000..d4afe39 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/ClientType.cs @@ -0,0 +1,21 @@ +namespace Discord +{ + /// + /// Defines the types of clients a user can be active on. + /// + public enum ClientType + { + /// + /// The user is active using the mobile application. + /// + Mobile, + /// + /// The user is active using the desktop application. + /// + Desktop, + /// + /// The user is active using the web application. + /// + Web + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/GuildUserProperties.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/GuildUserProperties.cs new file mode 100644 index 0000000..8f2d211 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/GuildUserProperties.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; + +namespace Discord +{ + /// + /// Properties that are used to modify an with the following parameters. + /// + /// + public class GuildUserProperties + { + /// + /// Gets or sets whether the user should be muted in a voice channel. + /// + /// + /// If this value is set to true, no user will be able to hear this user speak in the guild. + /// + public Optional Mute { get; set; } + /// + /// Gets or sets whether the user should be deafened in a voice channel. + /// + /// + /// If this value is set to true, this user will not be able to hear anyone speak in the guild. + /// + public Optional Deaf { get; set; } + /// + /// Gets or sets the user's nickname. + /// + /// + /// To clear the user's nickname, this value can be set to null or + /// . + /// + public Optional Nickname { get; set; } + /// + /// Gets or sets the roles the user should have. + /// + /// + /// + /// To add a role to a user: + /// + /// + /// + /// To remove a role from a user: + /// + /// + /// + public Optional> Roles { get; set; } + /// + /// Gets or sets the roles the user should have. + /// + /// + /// + /// To add a role to a user: + /// + /// + /// + /// To remove a role from a user: + /// + /// + /// + public Optional> RoleIds { get; set; } + /// + /// Moves a user to a voice channel. If null, this user will be disconnected from their current voice channel. + /// + /// + /// This user MUST already be in a for this to work. + /// When set, this property takes precedence over . + /// + public Optional Channel { get; set; } + /// + /// Moves a user to a voice channel. Set to null to disconnect this user from their current voice channel. + /// + /// + /// This user MUST already be in a for this to work. + /// + public Optional ChannelId { get; set; } // TODO: v3 breaking change, change ChannelId to ulong? to allow for kicking users from voice + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/IConnection.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/IConnection.cs new file mode 100644 index 0000000..1e65d97 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/IConnection.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace Discord +{ + public interface IConnection + { + /// Gets the ID of the connection account. + /// A representing the unique identifier value of this connection. + string Id { get; } + /// Gets the service of the connection (twitch, youtube). + /// A string containing the name of this type of connection. + string Type { get; } + /// Gets the username of the connection account. + /// A string containing the name of this connection. + string Name { get; } + /// Gets whether the connection is revoked. + /// A value which if true indicates that this connection has been revoked, otherwise false. + bool IsRevoked { get; } + + /// Gets a of integration IDs. + /// + /// An containing + /// representations of unique identifier values of integrations. + /// + IReadOnlyCollection IntegrationIds { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/IGroupUser.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/IGroupUser.cs new file mode 100644 index 0000000..ecf01f7 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/IGroupUser.cs @@ -0,0 +1,11 @@ +namespace Discord +{ + /// + /// Represents a Discord user that is in a group. + /// + public interface IGroupUser : IUser, IVoiceState + { + ///// Kicks this user from this group. + //Task KickAsync(RequestOptions options = null); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/IGuildUser.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/IGuildUser.cs new file mode 100644 index 0000000..ae682af --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/IGuildUser.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a generic guild user. + /// + public interface IGuildUser : IUser, IVoiceState + { + /// + /// Gets when this user joined the guild. + /// + /// + /// A representing the time of which the user has joined the guild; + /// null when it cannot be obtained. + /// + DateTimeOffset? JoinedAt { get; } + /// + /// Gets the nickname for this user. + /// + /// + /// A string representing the nickname of the user; null if none is set. + /// + string Nickname { get; } + /// + /// Gets the guild-level permissions for this user. + /// + /// + /// A structure for this user, representing what + /// permissions this user has in the guild. + /// + GuildPermissions GuildPermissions { get; } + + /// + /// Gets the guild for this user. + /// + /// + /// A guild object that this user belongs to. + /// + IGuild Guild { get; } + /// + /// Gets the ID of the guild for this user. + /// + /// + /// An representing the snowflake identifier of the guild that this user belongs to. + /// + ulong GuildId { get; } + /// + /// Gets the date and time for when this user's guild boost began. + /// + /// + /// A for when the user began boosting this guild; null if they are not boosting the guild. + /// + DateTimeOffset? PremiumSince { get; } + /// + /// Gets a collection of IDs for the roles that this user currently possesses in the guild. + /// + /// + /// This property returns a read-only collection of the identifiers of the roles that this user possesses. + /// For WebSocket users, a Roles property can be found in place of this property. Due to the REST + /// implementation, only a collection of identifiers can be retrieved instead of the full role objects. + /// + /// + /// A read-only collection of , each representing a snowflake identifier for a role that + /// this user possesses. + /// + IReadOnlyCollection RoleIds { get; } + + /// + /// Gets the level permissions granted to this user to a given channel. + /// + /// + /// The following example checks if the current user has the ability to send a message with attachment in + /// this channel; if so, uploads a file via . + /// + /// if (currentUser?.GetPermissions(targetChannel)?.AttachFiles) + /// await targetChannel.SendFileAsync("fortnite.png"); + /// + /// + /// The channel to get the permission from. + /// + /// A structure representing the permissions that a user has in the + /// specified channel. + /// + ChannelPermissions GetPermissions(IGuildChannel channel); + + /// + /// Kicks this user from this guild. + /// + /// The reason for the kick which will be recorded in the audit log. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous kick operation. + /// + Task KickAsync(string reason = null, RequestOptions options = null); + /// + /// Modifies this user's properties in this guild. + /// + /// + /// This method modifies the current guild user with the specified properties. To see an example of this + /// method and what properties are available, please refer to . + /// + /// The delegate containing the properties to modify the user with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + Task ModifyAsync(Action func, RequestOptions options = null); + + /// + /// Adds the specified role to this user in the guild. + /// + /// The role to be added to the user. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous role addition operation. + /// + Task AddRoleAsync(IRole role, RequestOptions options = null); + /// + /// Adds the specified to this user in the guild. + /// + /// The roles to be added to the user. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous role addition operation. + /// + Task AddRolesAsync(IEnumerable roles, RequestOptions options = null); + /// + /// Removes the specified from this user in the guild. + /// + /// The role to be removed from the user. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous role removal operation. + /// + Task RemoveRoleAsync(IRole role, RequestOptions options = null); + /// + /// Removes the specified from this user in the guild. + /// + /// The roles to be removed from the user. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous role removal operation. + /// + Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/IPresence.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/IPresence.cs new file mode 100644 index 0000000..620eb90 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/IPresence.cs @@ -0,0 +1,23 @@ +using System.Collections.Immutable; + +namespace Discord +{ + /// + /// Represents the user's presence status. This may include their online status and their activity. + /// + public interface IPresence + { + /// + /// Gets the activity this user is currently doing. + /// + IActivity Activity { get; } + /// + /// Gets the current status of this user. + /// + UserStatus Status { get; } + /// + /// Gets the set of clients where this user is currently active. + /// + IImmutableSet ActiveClients { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/ISelfUser.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/ISelfUser.cs new file mode 100644 index 0000000..04c6552 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/ISelfUser.cs @@ -0,0 +1,63 @@ +using System; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents the logged-in Discord user. + /// + public interface ISelfUser : IUser + { + /// + /// Gets the email associated with this user. + /// + string Email { get; } + /// + /// Indicates whether or not this user has their email verified. + /// + /// + /// true if this user's email has been verified; false if not. + /// + bool IsVerified { get; } + /// + /// Indicates whether or not this user has MFA enabled on their account. + /// + /// + /// true if this user has enabled multi-factor authentication on their account; false if not. + /// + bool IsMfaEnabled { get; } + /// + /// Gets the flags that are applied to a user's account. + /// + /// + /// This value is determined by bitwise OR-ing values together. + /// + /// + /// The value of flags for this user. + /// + UserProperties Flags { get; } + /// + /// Gets the type of Nitro subscription that is active on this user's account. + /// + /// + /// This information may only be available with the identify OAuth scope. + /// + /// + /// The type of Nitro subscription the user subscribes to, if any. + /// + PremiumType PremiumType { get; } + /// + /// Gets the user's chosen language option. + /// + /// + /// The IETF language tag of the user's chosen region, if provided. + /// For example, a locale of "English, US" is "en-US", "Chinese (Taiwan)" is "zh-TW", etc. + /// + string Locale { get; } + + /// + /// Modifies the user's properties. + /// + Task ModifyAsync(Action func, RequestOptions options = null); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/IUser.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/IUser.cs new file mode 100644 index 0000000..c59a75d --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/IUser.cs @@ -0,0 +1,105 @@ +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a generic user. + /// + public interface IUser : ISnowflakeEntity, IMentionable, IPresence + { + /// + /// Gets the identifier of this user's avatar. + /// + string AvatarId { get; } + /// + /// Gets the avatar URL for this user. + /// + /// + /// This property retrieves a URL for this user's avatar. In event that the user does not have a valid avatar + /// (i.e. their avatar identifier is not set), this property will return null. If you wish to + /// retrieve the default avatar for this user, consider using (see + /// example). + /// + /// + /// The following example attempts to retrieve the user's current avatar and send it to a channel; if one is + /// not set, a default avatar for this user will be returned instead. + /// + /// + /// The format to return. + /// The size of the image to return in. This can be any power of two between 16 and 2048. + /// + /// + /// A string representing the user's avatar URL; null if the user does not have an avatar in place. + /// + string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128); + /// + /// Gets the default avatar URL for this user. + /// + /// + /// This property retrieves a URL for this user's default avatar generated by Discord (Discord logo followed + /// by a random color as its background). This property will always return a value as it is calculated based + /// on the user's (discriminator % 5). + /// + /// + /// A string representing the user's avatar URL. + /// + string GetDefaultAvatarUrl(); + /// + /// Gets the per-username unique ID for this user. + /// + string Discriminator { get; } + /// + /// Gets the per-username unique ID for this user. + /// + ushort DiscriminatorValue { get; } + /// + /// Gets a value that indicates whether this user is identified as a bot. + /// + /// + /// This property retrieves a value that indicates whether this user is a registered bot application + /// (indicated by the blue BOT tag within the official chat client). + /// + /// + /// true if the user is a bot application; otherwise false. + /// + bool IsBot { get; } + /// + /// Gets a value that indicates whether this user is a webhook user. + /// + /// + /// true if the user is a webhook; otherwise false. + /// + bool IsWebhook { get; } + /// + /// Gets the username for this user. + /// + string Username { get; } + + /// + /// Gets the direct message channel of this user, or create one if it does not already exist. + /// + /// + /// This method is used to obtain or create a channel used to send a direct message. + /// + /// In event that the current user cannot send a message to the target user, a channel can and will + /// still be created by Discord. However, attempting to send a message will yield a + /// with a 403 as its + /// . There are currently no official workarounds by + /// Discord. + /// + /// + /// + /// The following example attempts to send a direct message to the target user and logs the incident should + /// it fail. + /// + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous operation for getting or creating a DM channel. The task result + /// contains the DM channel associated with this user. + /// + Task GetOrCreateDMChannelAsync(RequestOptions options = null); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/IVoiceState.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/IVoiceState.cs new file mode 100644 index 0000000..a9b3470 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/IVoiceState.cs @@ -0,0 +1,66 @@ +namespace Discord +{ + /// + /// Represents a user's voice connection status. + /// + public interface IVoiceState + { + /// + /// Gets a value that indicates whether this user is deafened by the guild. + /// + /// + /// true if the user is deafened (i.e. not permitted to listen to or speak to others) by the guild; + /// otherwise false. + /// + bool IsDeafened { get; } + /// + /// Gets a value that indicates whether this user is muted (i.e. not permitted to speak via voice) by the + /// guild. + /// + /// + /// true if this user is muted by the guild; otherwise false. + /// + bool IsMuted { get; } + /// + /// Gets a value that indicates whether this user has marked themselves as deafened. + /// + /// + /// true if this user has deafened themselves (i.e. not permitted to listen to or speak to others); otherwise false. + /// + bool IsSelfDeafened { get; } + /// + /// Gets a value that indicates whether this user has marked themselves as muted (i.e. not permitted to + /// speak via voice). + /// + /// + /// true if this user has muted themselves; otherwise false. + /// + bool IsSelfMuted { get; } + /// + /// Gets a value that indicates whether the user is muted by the current user. + /// + /// + /// true if the guild is temporarily blocking audio to/from this user; otherwise false. + /// + bool IsSuppressed { get; } + /// + /// Gets the voice channel this user is currently in. + /// + /// + /// A generic voice channel object representing the voice channel that the user is currently in; null + /// if none. + /// + IVoiceChannel VoiceChannel { get; } + /// + /// Gets the unique identifier for this user's voice session. + /// + string VoiceSessionId { get; } + /// + /// Gets a value that indicates if this user is streaming in a voice channel. + /// + /// + /// true if the user is streaming; otherwise false. + /// + bool IsStreaming { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/IWebhookUser.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/IWebhookUser.cs new file mode 100644 index 0000000..7a10c6b --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/IWebhookUser.cs @@ -0,0 +1,9 @@ +namespace Discord +{ + /// Represents a Webhook Discord user. + public interface IWebhookUser : IGuildUser + { + /// Gets the ID of a webhook. + ulong WebhookId { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/PremiumType.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/PremiumType.cs new file mode 100644 index 0000000..2b41e0b --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/PremiumType.cs @@ -0,0 +1,21 @@ +namespace Discord +{ + /// + /// Specifies the type of subscription a user is subscribed to. + /// + public enum PremiumType + { + /// + /// No subscription. + /// + None = 0, + /// + /// Nitro Classic subscription. Includes app perks like animated emojis and avatars, but not games. + /// + NitroClassic = 1, + /// + /// Nitro subscription. Includes app perks as well as the games subscription service. + /// + Nitro = 2 + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/SelfUserProperties.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/SelfUserProperties.cs new file mode 100644 index 0000000..e2ae12b --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/SelfUserProperties.cs @@ -0,0 +1,18 @@ +namespace Discord +{ + /// + /// Properties that are used to modify the with the specified changes. + /// + /// + public class SelfUserProperties + { + /// + /// Gets or sets the username. + /// + public Optional Username { get; set; } + /// + /// Gets or sets the avatar. + /// + public Optional Avatar { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/UserProperties.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/UserProperties.cs new file mode 100644 index 0000000..4f7272d --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/UserProperties.cs @@ -0,0 +1,41 @@ +using System; + +namespace Discord +{ + [Flags] + public enum UserProperties + { + /// + /// Default value for flags, when none are given to an account. + /// + None = 0, + /// + /// Flag given to Discord staff. + /// + Staff = 0b1, + /// + /// Flag given to Discord partners. + /// + Partner = 0b10, + /// + /// Flag given to users who have participated in the bug report program. + /// + BugHunter = 0b1000, + /// + /// Flag given to users who are in the HypeSquad House of Bravery. + /// + HypeSquadBravery = 0b100_0000, + /// + /// Flag given to users who are in the HypeSquad House of Brilliance. + /// + HypeSquadBrilliance = 0b1000_0000, + /// + /// Flag given to users who are in the HypeSquad House of Balance. + /// + HypeSquadBalance = 0b1_0000_0000, + /// + /// Flag given to users who subscribed to Nitro before games were added. + /// + EarlySupporter = 0b10_0000_0000, + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/UserStatus.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/UserStatus.cs new file mode 100644 index 0000000..0903326 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Users/UserStatus.cs @@ -0,0 +1,33 @@ +namespace Discord +{ + /// + /// Defines the available Discord user status. + /// + public enum UserStatus + { + /// + /// The user is offline. + /// + Offline, + /// + /// The user is online. + /// + Online, + /// + /// The user is idle. + /// + Idle, + /// + /// The user is AFK. + /// + AFK, + /// + /// The user is busy. + /// + DoNotDisturb, + /// + /// The user is invisible. + /// + Invisible, + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Webhooks/IWebhook.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Webhooks/IWebhook.cs new file mode 100644 index 0000000..b2d0173 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Webhooks/IWebhook.cs @@ -0,0 +1,57 @@ +using System; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a webhook object on Discord. + /// + public interface IWebhook : IDeletable, ISnowflakeEntity + { + /// + /// Gets the token of this webhook. + /// + string Token { get; } + + /// + /// Gets the default name of this webhook. + /// + string Name { get; } + /// + /// Gets the ID of this webhook's default avatar. + /// + string AvatarId { get; } + /// + /// Gets the URL to this webhook's default avatar. + /// + string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128); + + /// + /// Gets the channel for this webhook. + /// + ITextChannel Channel { get; } + /// + /// Gets the ID of the channel for this webhook. + /// + ulong ChannelId { get; } + + /// + /// Gets the guild owning this webhook. + /// + IGuild Guild { get; } + /// + /// Gets the ID of the guild owning this webhook. + /// + ulong? GuildId { get; } + + /// + /// Gets the user that created this webhook. + /// + IUser Creator { get; } + + /// + /// Modifies this webhook. + /// + Task ModifyAsync(Action func, RequestOptions options = null); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs new file mode 100644 index 0000000..e5ee4d6 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs @@ -0,0 +1,32 @@ +namespace Discord +{ + /// + /// Properties used to modify an with the specified changes. + /// + /// + public class WebhookProperties + { + /// + /// Gets or sets the default name of the webhook. + /// + public Optional Name { get; set; } + /// + /// Gets or sets the default avatar of the webhook. + /// + public Optional Image { get; set; } + /// + /// Gets or sets the channel for this webhook. + /// + /// + /// This field is not used when authenticated with . + /// + public Optional Channel { get; set; } + /// + /// Gets or sets the channel ID for this webhook. + /// + /// + /// This field is not used when authenticated with . + /// + public Optional ChannelId { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Webhooks/WebhookType.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Webhooks/WebhookType.cs new file mode 100644 index 0000000..0ddfa39 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Entities/Webhooks/WebhookType.cs @@ -0,0 +1,14 @@ +namespace Discord +{ + /// + /// Represents the type of a webhook. + /// + /// + /// This type is currently unused, and is only returned in audit log responses. + /// + public enum WebhookType + { + /// An incoming webhook + Incoming = 1 + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs new file mode 100644 index 0000000..d960762 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord +{ + /// An extension class for squashing . + /// + /// This set of extension methods will squash an into a + /// single . This is often associated with requests that has a + /// set limit when requesting. + /// + public static class AsyncEnumerableExtensions + { + /// Flattens the specified pages into one asynchronously. + public static async Task> FlattenAsync(this IAsyncEnumerable> source) + { + return await source.Flatten().ToArrayAsync().ConfigureAwait(false); + } + /// Flattens the specified pages into one . + public static IAsyncEnumerable Flatten(this IAsyncEnumerable> source) + { + return source.SelectMany(enumerable => enumerable.ToAsyncEnumerable()); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Extensions/AttachmentExtensions.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Extensions/AttachmentExtensions.cs new file mode 100644 index 0000000..6054107 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Extensions/AttachmentExtensions.cs @@ -0,0 +1,15 @@ +namespace Discord +{ + public static class AttachmentExtensions + { + /// + /// The prefix applied to files to indicate that it is a spoiler. + /// + public const string SpoilerPrefix = "SPOILER_"; + /// + /// Gets whether the message's attachments are spoilers or not. + /// + public static bool IsSpoiler(this IAttachment attachment) + => attachment.Filename.StartsWith(SpoilerPrefix); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Extensions/CollectionExtensions.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Extensions/CollectionExtensions.cs new file mode 100644 index 0000000..e5d6025 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Extensions/CollectionExtensions.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Discord +{ + internal static class CollectionExtensions + { + //public static IReadOnlyCollection ToReadOnlyCollection(this IReadOnlyCollection source) + // => new CollectionWrapper(source, () => source.Count); + public static IReadOnlyCollection ToReadOnlyCollection(this ICollection source) + => new CollectionWrapper(source, () => source.Count); + //public static IReadOnlyCollection ToReadOnlyCollection(this IReadOnlyDictionary source) + // => new CollectionWrapper(source.Select(x => x.Value), () => source.Count); + public static IReadOnlyCollection ToReadOnlyCollection(this IDictionary source) + => new CollectionWrapper(source.Select(x => x.Value), () => source.Count); + public static IReadOnlyCollection ToReadOnlyCollection(this IEnumerable query, IReadOnlyCollection source) + => new CollectionWrapper(query, () => source.Count); + public static IReadOnlyCollection ToReadOnlyCollection(this IEnumerable query, Func countFunc) + => new CollectionWrapper(query, countFunc); + } + + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + internal struct CollectionWrapper : IReadOnlyCollection + { + private readonly IEnumerable _query; + private readonly Func _countFunc; + + //It's okay that this count is affected by race conditions - we're wrapping a concurrent collection and that's to be expected + public int Count => _countFunc(); + + public CollectionWrapper(IEnumerable query, Func countFunc) + { + _query = query; + _countFunc = countFunc; + } + + private string DebuggerDisplay => $"Count = {Count}"; + + public IEnumerator GetEnumerator() => _query.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => _query.GetEnumerator(); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Extensions/DiscordClientExtensions.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Extensions/DiscordClientExtensions.cs new file mode 100644 index 0000000..6ebdbac --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Extensions/DiscordClientExtensions.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord +{ + /// An extension class for the Discord client. + public static class DiscordClientExtensions + { + /// Gets the private channel with the provided ID. + public static async Task GetPrivateChannelAsync(this IDiscordClient client, ulong id) + => await client.GetChannelAsync(id).ConfigureAwait(false) as IPrivateChannel; + + /// Gets the DM channel with the provided ID. + public static async Task GetDMChannelAsync(this IDiscordClient client, ulong id) + => await client.GetPrivateChannelAsync(id).ConfigureAwait(false) as IDMChannel; + /// Gets all available DM channels for the client. + public static async Task> GetDMChannelsAsync(this IDiscordClient client) + => (await client.GetPrivateChannelsAsync().ConfigureAwait(false)).OfType(); + + /// Gets the group channel with the provided ID. + public static async Task GetGroupChannelAsync(this IDiscordClient client, ulong id) + => await client.GetPrivateChannelAsync(id).ConfigureAwait(false) as IGroupChannel; + /// Gets all available group channels for the client. + public static async Task> GetGroupChannelsAsync(this IDiscordClient client) + => (await client.GetPrivateChannelsAsync().ConfigureAwait(false)).OfType(); + + /// Gets the most optimal voice region for the client. + public static async Task GetOptimalVoiceRegionAsync(this IDiscordClient discord) + { + var regions = await discord.GetVoiceRegionsAsync().ConfigureAwait(false); + return regions.FirstOrDefault(x => x.IsOptimal); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs new file mode 100644 index 0000000..a3b8ddd --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Discord +{ + /// An extension class for building an embed. + public static class EmbedBuilderExtensions + { + /// Adds embed color based on the provided raw value. + public static EmbedBuilder WithColor(this EmbedBuilder builder, uint rawValue) => + builder.WithColor(new Color(rawValue)); + + /// Adds embed color based on the provided RGB value. + public static EmbedBuilder WithColor(this EmbedBuilder builder, byte r, byte g, byte b) => + builder.WithColor(new Color(r, g, b)); + + /// Adds embed color based on the provided RGB value. + /// The argument value is not between 0 to 255. + public static EmbedBuilder WithColor(this EmbedBuilder builder, int r, int g, int b) => + builder.WithColor(new Color(r, g, b)); + + /// Adds embed color based on the provided RGB value. + /// The argument value is not between 0 to 1. + public static EmbedBuilder WithColor(this EmbedBuilder builder, float r, float g, float b) => + builder.WithColor(new Color(r, g, b)); + + /// Fills the embed author field with the provided user's full username and avatar URL. + public static EmbedBuilder WithAuthor(this EmbedBuilder builder, IUser user) => + builder.WithAuthor($"{user.Username}#{user.Discriminator}", user.GetAvatarUrl()); + + /// Converts a object to a . + /// The embed type is not . + public static EmbedBuilder ToEmbedBuilder(this IEmbed embed) + { + if (embed.Type != EmbedType.Rich) + throw new InvalidOperationException($"Only {nameof(EmbedType.Rich)} embeds may be built."); + + var builder = new EmbedBuilder + { + Author = new EmbedAuthorBuilder + { + Name = embed.Author?.Name, + IconUrl = embed.Author?.IconUrl, + Url = embed.Author?.Url + }, + Color = embed.Color, + Description = embed.Description, + Footer = new EmbedFooterBuilder + { + Text = embed.Footer?.Text, + IconUrl = embed.Footer?.IconUrl + }, + ImageUrl = embed.Image?.Url, + ThumbnailUrl = embed.Thumbnail?.Url, + Timestamp = embed.Timestamp, + Title = embed.Title, + Url = embed.Url + }; + + foreach (var field in embed.Fields) + builder.AddField(field.Name, field.Value, field.Inline); + + return builder; + } + + /// + /// Adds the specified fields into this . + /// + /// Field count exceeds . + public static EmbedBuilder WithFields(this EmbedBuilder builder, IEnumerable fields) + { + foreach (var field in fields) + builder.AddField(field); + + return builder; + } + /// + /// Adds the specified fields into this . + /// + public static EmbedBuilder WithFields(this EmbedBuilder builder, params EmbedFieldBuilder[] fields) + => WithFields(builder, fields.AsEnumerable()); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Extensions/GuildExtensions.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Extensions/GuildExtensions.cs new file mode 100644 index 0000000..58b749c --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Extensions/GuildExtensions.cs @@ -0,0 +1,24 @@ +namespace Discord +{ + /// + /// An extension class for . + /// + public static class GuildExtensions + { + /// + /// Gets if welcome system messages are enabled. + /// + /// The guild to check. + /// A bool indicating if the welcome messages are enabled in the system channel. + public static bool GetWelcomeMessagesEnabled(this IGuild guild) + => !guild.SystemChannelFlags.HasFlag(SystemChannelMessageDeny.WelcomeMessage); + + /// + /// Gets if guild boost system messages are enabled. + /// + /// The guild to check. + /// A bool indicating if the guild boost messages are enabled in the system channel. + public static bool GetGuildBoostMessagesEnabled(this IGuild guild) + => !guild.SystemChannelFlags.HasFlag(SystemChannelMessageDeny.GuildBoost); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Extensions/MessageExtensions.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Extensions/MessageExtensions.cs new file mode 100644 index 0000000..90ebea9 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Extensions/MessageExtensions.cs @@ -0,0 +1,75 @@ +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Provides extension methods for . + /// + public static class MessageExtensions + { + /// + /// Gets a URL that jumps to the message. + /// + /// The message to jump to. + /// + /// A string that contains a URL for jumping to the message in chat. + /// + public static string GetJumpUrl(this IMessage msg) + { + var channel = msg.Channel; + return $"https://discordapp.com/channels/{(channel is IDMChannel ? "@me" : $"{(channel as ITextChannel).GuildId}")}/{channel.Id}/{msg.Id}"; + } + + /// + /// Add multiple reactions to a message. + /// + /// + /// This method does not bulk add reactions! It will send a request for each reaction inculded. + /// + /// + /// + /// IEmote A = new Emoji("🅰"); + /// IEmote B = new Emoji("🅱"); + /// await msg.AddReactionsAsync(new[] { A, B }); + /// + /// + /// The message to add reactions to. + /// An array of reactions to add to the message + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous operation for adding a reaction to this message. + /// + /// + /// + public static async Task AddReactionsAsync(this IUserMessage msg, IEmote[] reactions, RequestOptions options = null) + { + foreach (var rxn in reactions) + await msg.AddReactionAsync(rxn, options).ConfigureAwait(false); + } + /// + /// Remove multiple reactions from a message. + /// + /// + /// This method does not bulk remove reactions! If you want to clear reactions from a message, + /// + /// + /// + /// + /// await msg.RemoveReactionsAsync(currentUser, new[] { A, B }); + /// + /// + /// The message to remove reactions from. + /// An array of reactions to remove from the message + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous operation for removing a reaction to this message. + /// + /// + /// + public static async Task RemoveReactionsAsync(this IUserMessage msg, IUser user, IEmote[] reactions, RequestOptions options = null) + { + foreach (var rxn in reactions) + await msg.RemoveReactionAsync(rxn, user, options).ConfigureAwait(false); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Extensions/StringExtensions.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Extensions/StringExtensions.cs new file mode 100644 index 0000000..c0ebb26 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Extensions/StringExtensions.cs @@ -0,0 +1,10 @@ +using System; + +namespace Discord +{ + internal static class StringExtensions + { + public static bool IsNullOrUri(this string url) => + string.IsNullOrEmpty(url) || Uri.IsWellFormedUriString(url, UriKind.Absolute); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Extensions/TaskCompletionSourceExtensions.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Extensions/TaskCompletionSourceExtensions.cs new file mode 100644 index 0000000..a5a715b --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Extensions/TaskCompletionSourceExtensions.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading.Tasks; + +namespace Discord +{ + internal static class TaskCompletionSourceExtensions + { + public static Task SetResultAsync(this TaskCompletionSource source, T result) + => Task.Run(() => source.SetResult(result)); + public static Task TrySetResultAsync(this TaskCompletionSource source, T result) + => Task.Run(() => source.TrySetResult(result)); + + public static Task SetExceptionAsync(this TaskCompletionSource source, Exception ex) + => Task.Run(() => source.SetException(ex)); + public static Task TrySetExceptionAsync(this TaskCompletionSource source, Exception ex) + => Task.Run(() => source.TrySetException(ex)); + + public static Task SetCanceledAsync(this TaskCompletionSource source) + => Task.Run(() => source.SetCanceled()); + public static Task TrySetCanceledAsync(this TaskCompletionSource source) + => Task.Run(() => source.TrySetCanceled()); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Extensions/UserExtensions.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Extensions/UserExtensions.cs new file mode 100644 index 0000000..f98bf72 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Extensions/UserExtensions.cs @@ -0,0 +1,164 @@ +using System; +using System.Threading.Tasks; +using System.IO; + +namespace Discord +{ + /// An extension class for various Discord user objects. + public static class UserExtensions + { + /// + /// Sends a message via DM. + /// + /// + /// This method attempts to send a direct-message to the user. + /// + /// + /// Please note that this method will throw an + /// if the user cannot receive DMs due to privacy reasons or if the user has the sender blocked. + /// + /// + /// You may want to consider catching for + /// 50007 when using this method. + /// + /// + /// + /// The user to send the DM to. + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous send operation. The task result contains the sent message. + /// + public static async Task SendMessageAsync(this IUser user, + string text = null, + bool isTTS = false, + Embed embed = null, + RequestOptions options = null) + { + return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + } + + /// + /// Sends a file to this message channel with an optional caption. + /// + /// + /// The following example uploads a streamed image that will be called b1nzy.jpg embedded inside a + /// rich embed to the channel. + /// + /// await channel.SendFileAsync(b1nzyStream, "b1nzy.jpg", + /// embed: new EmbedBuilder {ImageUrl = "attachment://b1nzy.jpg"}.Build()); + /// + /// + /// + /// This method attempts to send an attachment as a direct-message to the user. + /// + /// + /// Please note that this method will throw an + /// if the user cannot receive DMs due to privacy reasons or if the user has the sender blocked. + /// + /// + /// You may want to consider catching for + /// 50007 when using this method. + /// + /// + /// + /// If you wish to upload an image and have it embedded in a embed, + /// you may upload the file and refer to the file with "attachment://filename.ext" in the + /// . See the example section for its usage. + /// + /// + /// The user to send the DM to. + /// The of the file to be sent. + /// The name of the attachment. + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + public static async Task SendFileAsync(this IUser user, + Stream stream, + string filename, + string text = null, + bool isTTS = false, + Embed embed = null, + RequestOptions options = null + ) + { + return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false); + } + + /// + /// Sends a file via DM with an optional caption. + /// + /// + /// The following example uploads a local file called wumpus.txt along with the text + /// good discord boi to the channel. + /// + /// await channel.SendFileAsync("wumpus.txt", "good discord boi"); + /// + /// + /// The following example uploads a local image called b1nzy.jpg embedded inside a rich embed to the + /// channel. + /// + /// await channel.SendFileAsync("b1nzy.jpg", + /// embed: new EmbedBuilder {ImageUrl = "attachment://b1nzy.jpg"}.Build()); + /// + /// + /// + /// This method attempts to send an attachment as a direct-message to the user. + /// + /// + /// Please note that this method will throw an + /// if the user cannot receive DMs due to privacy reasons or if the user has the sender blocked. + /// + /// + /// You may want to consider catching for + /// 50007 when using this method. + /// + /// + /// + /// If you wish to upload an image and have it embedded in a embed, + /// you may upload the file and refer to the file with "attachment://filename.ext" in the + /// . See the example section for its usage. + /// + /// + /// The user to send the DM to. + /// The file path of the file. + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + public static async Task SendFileAsync(this IUser user, + string filePath, + string text = null, + bool isTTS = false, + Embed embed = null, + RequestOptions options = null) + { + return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false); + } + + /// + /// Bans the user from the guild and optionally prunes their recent messages. + /// + /// The user to ban. + /// The number of days to remove messages from this for - must be between [0, 7] + /// The reason of the ban to be written in the audit log. + /// The options to be used when sending the request. + /// is not between 0 to 7. + /// + /// A task that represents the asynchronous operation for banning a user. + /// + public static Task BanAsync(this IGuildUser user, int pruneDays = 0, string reason = null, RequestOptions options = null) + => user.Guild.AddBanAsync(user, pruneDays, reason, options); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Format.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Format.cs new file mode 100644 index 0000000..0ab70f8 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Format.cs @@ -0,0 +1,95 @@ +using System.Text; + +namespace Discord +{ + /// A helper class for formatting characters. + public static class Format + { + // Characters which need escaping + private static readonly string[] SensitiveCharacters = { "\\", "*", "_", "~", "`", "|", ">" }; + + /// Returns a markdown-formatted string with bold formatting. + public static string Bold(string text) => $"**{text}**"; + /// Returns a markdown-formatted string with italics formatting. + public static string Italics(string text) => $"*{text}*"; + /// Returns a markdown-formatted string with underline formatting. + public static string Underline(string text) => $"__{text}__"; + /// Returns a markdown-formatted string with strikethrough formatting. + public static string Strikethrough(string text) => $"~~{text}~~"; + /// Returns a string with spoiler formatting. + public static string Spoiler(string text) => $"||{text}||"; + /// Returns a markdown-formatted URL. Only works in descriptions and fields. + public static string Url(string text, string url) => $"[{text}]({url})"; + /// Escapes a URL so that a preview is not generated. + public static string EscapeUrl(string url) => $"<{url}>"; + + /// Returns a markdown-formatted string with codeblock formatting. + public static string Code(string text, string language = null) + { + if (language != null || text.Contains("\n")) + return $"```{language ?? ""}\n{text}\n```"; + else + return $"`{text}`"; + } + + /// Sanitizes the string, safely escaping any Markdown sequences. + public static string Sanitize(string text) + { + foreach (string unsafeChar in SensitiveCharacters) + text = text.Replace(unsafeChar, $"\\{unsafeChar}"); + return text; + } + + /// + /// Formats a string as a quote. + /// + /// The text to format. + /// Gets the formatted quote text. + public static string Quote(string text) + { + // do not modify null or whitespace text + // whitespace does not get quoted properly + if (string.IsNullOrWhiteSpace(text)) + return text; + + StringBuilder result = new StringBuilder(); + + int startIndex = 0; + int newLineIndex; + do + { + newLineIndex = text.IndexOf('\n', startIndex); + if (newLineIndex == -1) + { + // read the rest of the string + var str = text.Substring(startIndex); + result.Append($"> {str}"); + } + else + { + // read until the next newline + var str = text.Substring(startIndex, newLineIndex - startIndex); + result.Append($"> {str}\n"); + } + startIndex = newLineIndex + 1; + } + while (newLineIndex != -1 && startIndex != text.Length); + + return result.ToString(); + } + + /// + /// Formats a string as a block quote. + /// + /// The text to format. + /// Gets the formatted block quote text. + public static string BlockQuote(string text) + { + // do not modify null or whitespace + if (string.IsNullOrWhiteSpace(text)) + return text; + + return $">>> {text}"; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/IDiscordClient.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/IDiscordClient.cs new file mode 100644 index 0000000..e1c9006 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/IDiscordClient.cs @@ -0,0 +1,278 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a generic Discord client. + /// + public interface IDiscordClient : IDisposable + { + /// + /// Gets the current state of connection. + /// + ConnectionState ConnectionState { get; } + /// + /// Gets the currently logged-in user. + /// + ISelfUser CurrentUser { get; } + /// + /// Gets the token type of the logged-in user. + /// + TokenType TokenType { get; } + + /// + /// Starts the connection between Discord and the client.. + /// + /// + /// This method will initialize the connection between the client and Discord. + /// + /// This method will immediately return after it is called, as it will initialize the connection on + /// another thread. + /// + /// + /// + /// A task that represents the asynchronous start operation. + /// + Task StartAsync(); + /// + /// Stops the connection between Discord and the client. + /// + /// + /// A task that represents the asynchronous stop operation. + /// + Task StopAsync(); + + /// + /// Gets a Discord application information for the logged-in user. + /// + /// + /// This method reflects your application information you submitted when creating a Discord application via + /// the Developer Portal. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the application + /// information. + /// + Task GetApplicationInfoAsync(RequestOptions options = null); + + /// + /// Gets a generic channel. + /// + /// + /// + /// var channel = await _client.GetChannelAsync(381889909113225237); + /// if (channel != null && channel is IMessageChannel msgChannel) + /// { + /// await msgChannel.SendMessageAsync($"{msgChannel} is created at {msgChannel.CreatedAt}"); + /// } + /// + /// + /// The snowflake identifier of the channel (e.g. `381889909113225237`). + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the channel associated + /// with the snowflake identifier; null when the channel cannot be found. + /// + Task GetChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a collection of private channels opened in this session. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// This method will retrieve all private channels (including direct-message, group channel and such) that + /// are currently opened in this session. + /// + /// This method will not return previously opened private channels outside of the current session! If + /// you have just started the client, this may return an empty collection. + /// + /// + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of private channels that the user currently partakes in. + /// + Task> GetPrivateChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a collection of direct message channels opened in this session. + /// + /// + /// This method returns a collection of currently opened direct message channels. + /// + /// This method will not return previously opened DM channels outside of the current session! If you + /// have just started the client, this may return an empty collection. + /// + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of direct-message channels that the user currently partakes in. + /// + Task> GetDMChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a collection of group channels opened in this session. + /// + /// + /// This method returns a collection of currently opened group channels. + /// + /// This method will not return previously opened group channels outside of the current session! If you + /// have just started the client, this may return an empty collection. + /// + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of group channels that the user currently partakes in. + /// + Task> GetGroupChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + + /// + /// Gets the connections that the user has set up. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of connections. + /// + Task> GetConnectionsAsync(RequestOptions options = null); + + /// + /// Gets a guild. + /// + /// The guild snowflake identifier. + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the guild associated + /// with the snowflake identifier; null when the guild cannot be found. + /// + Task GetGuildAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a collection of guilds that the user is currently in. + /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of guilds that the current user is in. + /// + Task> GetGuildsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Creates a guild for the logged-in user who is in less than 10 active guilds. + /// + /// + /// This method creates a new guild on behalf of the logged-in user. + /// + /// Due to Discord's limitation, this method will only work for users that are in less than 10 guilds. + /// + /// + /// The name of the new guild. + /// The voice region to create the guild with. + /// The icon of the guild. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the created guild. + /// + Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null, RequestOptions options = null); + + /// + /// Gets an invite. + /// + /// The invitation identifier. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the invite information. + /// + Task GetInviteAsync(string inviteId, RequestOptions options = null); + + /// + /// Gets a user. + /// + /// + /// + /// var user = await _client.GetUserAsync(168693960628371456); + /// if (user != null) + /// Console.WriteLine($"{user} is created at {user.CreatedAt}."; + /// + /// + /// The snowflake identifier of the user (e.g. `168693960628371456`). + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the user associated with + /// the snowflake identifier; null if the user is not found. + /// + Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a user. + /// + /// + /// + /// var user = await _client.GetUserAsync("Still", "2876"); + /// if (user != null) + /// Console.WriteLine($"{user} is created at {user.CreatedAt}."; + /// + /// + /// The name of the user (e.g. `Still`). + /// The discriminator value of the user (e.g. `2876`). + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the user associated with + /// the name and the discriminator; null if the user is not found. + /// + Task GetUserAsync(string username, string discriminator, RequestOptions options = null); + + /// + /// Gets a collection of the available voice regions. + /// + /// + /// The following example gets the most optimal voice region from the collection. + /// + /// var regions = await client.GetVoiceRegionsAsync(); + /// var optimalRegion = regions.FirstOrDefault(x => x.IsOptimal); + /// + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// with all of the available voice regions in this session. + /// + Task> GetVoiceRegionsAsync(RequestOptions options = null); + /// + /// Gets a voice region. + /// + /// The identifier of the voice region (e.g. eu-central ). + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the voice region + /// associated with the identifier; null if the voice region is not found. + /// + Task GetVoiceRegionAsync(string id, RequestOptions options = null); + + /// + /// Gets a webhook available. + /// + /// The identifier of the webhook. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a webhook associated + /// with the identifier; null if the webhook is not found. + /// + Task GetWebhookAsync(ulong id, RequestOptions options = null); + + /// + /// Gets the recommended shard count as suggested by Discord. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains an + /// that represents the number of shards that should be used with this account. + /// + Task GetRecommendedShardCountAsync(RequestOptions options = null); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Logging/LogManager.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Logging/LogManager.cs new file mode 100644 index 0000000..a99c45b --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Logging/LogManager.cs @@ -0,0 +1,105 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Logging +{ + internal class LogManager + { + public LogSeverity Level { get; } + private Logger ClientLogger { get; } + + public event Func Message { add { _messageEvent.Add(value); } remove { _messageEvent.Remove(value); } } + private readonly AsyncEvent> _messageEvent = new AsyncEvent>(); + + public LogManager(LogSeverity minSeverity) + { + Level = minSeverity; + ClientLogger = new Logger(this, "Discord"); + } + + public async Task LogAsync(LogSeverity severity, string source, Exception ex) + { + try + { + if (severity <= Level) + await _messageEvent.InvokeAsync(new LogMessage(severity, source, null, ex)).ConfigureAwait(false); + } + catch + { + // ignored + } + } + public async Task LogAsync(LogSeverity severity, string source, string message, Exception ex = null) + { + try + { + if (severity <= Level) + await _messageEvent.InvokeAsync(new LogMessage(severity, source, message, ex)).ConfigureAwait(false); + } + catch + { + // ignored + } + } + + public async Task LogAsync(LogSeverity severity, string source, FormattableString message, Exception ex = null) + { + try + { + if (severity <= Level) + await _messageEvent.InvokeAsync(new LogMessage(severity, source, message.ToString(), ex)).ConfigureAwait(false); + } + catch { } + } + + + public Task ErrorAsync(string source, Exception ex) + => LogAsync(LogSeverity.Error, source, ex); + public Task ErrorAsync(string source, string message, Exception ex = null) + => LogAsync(LogSeverity.Error, source, message, ex); + + public Task ErrorAsync(string source, FormattableString message, Exception ex = null) + => LogAsync(LogSeverity.Error, source, message, ex); + + + public Task WarningAsync(string source, Exception ex) + => LogAsync(LogSeverity.Warning, source, ex); + public Task WarningAsync(string source, string message, Exception ex = null) + => LogAsync(LogSeverity.Warning, source, message, ex); + + public Task WarningAsync(string source, FormattableString message, Exception ex = null) + => LogAsync(LogSeverity.Warning, source, message, ex); + + + public Task InfoAsync(string source, Exception ex) + => LogAsync(LogSeverity.Info, source, ex); + public Task InfoAsync(string source, string message, Exception ex = null) + => LogAsync(LogSeverity.Info, source, message, ex); + public Task InfoAsync(string source, FormattableString message, Exception ex = null) + => LogAsync(LogSeverity.Info, source, message, ex); + + + public Task VerboseAsync(string source, Exception ex) + => LogAsync(LogSeverity.Verbose, source, ex); + public Task VerboseAsync(string source, string message, Exception ex = null) + => LogAsync(LogSeverity.Verbose, source, message, ex); + public Task VerboseAsync(string source, FormattableString message, Exception ex = null) + => LogAsync(LogSeverity.Verbose, source, message, ex); + + + public Task DebugAsync(string source, Exception ex) + => LogAsync(LogSeverity.Debug, source, ex); + public Task DebugAsync(string source, string message, Exception ex = null) + => LogAsync(LogSeverity.Debug, source, message, ex); + public Task DebugAsync(string source, FormattableString message, Exception ex = null) + => LogAsync(LogSeverity.Debug, source, message, ex); + + + public Logger CreateLogger(string name) => new Logger(this, name); + + public async Task WriteInitialLog() + { + await ClientLogger.InfoAsync($"Discord.Net v{DiscordConfig.Version} (API v{DiscordConfig.APIVersion})").ConfigureAwait(false); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Logging/LogMessage.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Logging/LogMessage.cs new file mode 100644 index 0000000..715cac6 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Logging/LogMessage.cs @@ -0,0 +1,136 @@ +using System; +using System.Text; + +namespace Discord +{ + /// + /// Provides a message object used for logging purposes. + /// + public struct LogMessage + { + /// + /// Gets the severity of the log entry. + /// + /// + /// A enum to indicate the severeness of the incident or event. + /// + public LogSeverity Severity { get; } + /// + /// Gets the source of the log entry. + /// + /// + /// A string representing the source of the log entry. + /// + public string Source { get; } + /// + /// Gets the message of this log entry. + /// + /// + /// A string containing the message of this log entry. + /// + public string Message { get; } + /// + /// Gets the exception of this log entry. + /// + /// + /// An object associated with an incident; otherwise null. + /// + public Exception Exception { get; } + + /// + /// Initializes a new struct with the severity, source, message of the event, and + /// optionally, an exception. + /// + /// The severity of the event. + /// The source of the event. + /// The message of the event. + /// The exception of the event. + public LogMessage(LogSeverity severity, string source, string message, Exception exception = null) + { + Severity = severity; + Source = source; + Message = message; + Exception = exception; + } + + public override string ToString() => ToString(); + public string ToString(StringBuilder builder = null, bool fullException = true, bool prependTimestamp = true, DateTimeKind timestampKind = DateTimeKind.Local, int? padSource = 11) + { + string sourceName = Source; + string message = Message; + string exMessage = fullException ? Exception?.ToString() : Exception?.Message; + + int maxLength = 1 + + (prependTimestamp ? 8 : 0) + 1 + + (padSource.HasValue ? padSource.Value : sourceName?.Length ?? 0) + 1 + + (message?.Length ?? 0) + + (exMessage?.Length ?? 0) + 3; + + if (builder == null) + builder = new StringBuilder(maxLength); + else + { + builder.Clear(); + builder.EnsureCapacity(maxLength); + } + + if (prependTimestamp) + { + DateTime now; + if (timestampKind == DateTimeKind.Utc) + now = DateTime.UtcNow; + else + now = DateTime.Now; + if (now.Hour < 10) + builder.Append('0'); + builder.Append(now.Hour); + builder.Append(':'); + if (now.Minute < 10) + builder.Append('0'); + builder.Append(now.Minute); + builder.Append(':'); + if (now.Second < 10) + builder.Append('0'); + builder.Append(now.Second); + builder.Append(' '); + } + if (sourceName != null) + { + if (padSource.HasValue) + { + if (sourceName.Length < padSource.Value) + { + builder.Append(sourceName); + builder.Append(' ', padSource.Value - sourceName.Length); + } + else if (sourceName.Length > padSource.Value) + builder.Append(sourceName.Substring(0, padSource.Value)); + else + builder.Append(sourceName); + } + builder.Append(' '); + } + if (!string.IsNullOrEmpty(Message)) + { + for (int i = 0; i < message.Length; i++) + { + //Strip control chars + char c = message[i]; + if (!char.IsControl(c)) + builder.Append(c); + } + } + if (exMessage != null) + { + if (!string.IsNullOrEmpty(Message)) + { + builder.Append(':'); + builder.AppendLine(); + } + builder.Append(exMessage); + } + + return builder.ToString(); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Logging/LogSeverity.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Logging/LogSeverity.cs new file mode 100644 index 0000000..f9b518c --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Logging/LogSeverity.cs @@ -0,0 +1,34 @@ +namespace Discord +{ + /// + /// Specifies the severity of the log message. + /// + public enum LogSeverity + { + /// + /// Logs that contain the most severe level of error. This type of error indicate that immediate attention + /// may be required. + /// + Critical = 0, + /// + /// Logs that highlight when the flow of execution is stopped due to a failure. + /// + Error = 1, + /// + /// Logs that highlight an abnormal activity in the flow of execution. + /// + Warning = 2, + /// + /// Logs that track the general flow of the application. + /// + Info = 3, + /// + /// Logs that are used for interactive investigation during development. + /// + Verbose = 4, + /// + /// Logs that contain the most detailed messages. + /// + Debug = 5 + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Logging/Logger.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Logging/Logger.cs new file mode 100644 index 0000000..e71c569 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Logging/Logger.cs @@ -0,0 +1,72 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Logging +{ + internal class Logger + { + private readonly LogManager _manager; + + public string Name { get; } + public LogSeverity Level => _manager.Level; + + public Logger(LogManager manager, string name) + { + _manager = manager; + Name = name; + } + + public Task LogAsync(LogSeverity severity, Exception exception = null) + => _manager.LogAsync(severity, Name, exception); + public Task LogAsync(LogSeverity severity, string message, Exception exception = null) + => _manager.LogAsync(severity, Name, message, exception); + public Task LogAsync(LogSeverity severity, FormattableString message, Exception exception = null) + => _manager.LogAsync(severity, Name, message, exception); + + + public Task ErrorAsync(Exception exception) + => _manager.ErrorAsync(Name, exception); + public Task ErrorAsync(string message, Exception exception = null) + => _manager.ErrorAsync(Name, message, exception); + + public Task ErrorAsync(FormattableString message, Exception exception = null) + => _manager.ErrorAsync(Name, message, exception); + + + public Task WarningAsync(Exception exception) + => _manager.WarningAsync(Name, exception); + public Task WarningAsync(string message, Exception exception = null) + => _manager.WarningAsync(Name, message, exception); + + public Task WarningAsync(FormattableString message, Exception exception = null) + => _manager.WarningAsync(Name, message, exception); + + + public Task InfoAsync(Exception exception) + => _manager.InfoAsync(Name, exception); + public Task InfoAsync(string message, Exception exception = null) + => _manager.InfoAsync(Name, message, exception); + + public Task InfoAsync(FormattableString message, Exception exception = null) + => _manager.InfoAsync(Name, message, exception); + + + public Task VerboseAsync(Exception exception) + => _manager.VerboseAsync(Name, exception); + public Task VerboseAsync(string message, Exception exception = null) + => _manager.VerboseAsync(Name, message, exception); + + public Task VerboseAsync(FormattableString message, Exception exception = null) + => _manager.VerboseAsync(Name, message, exception); + + + public Task DebugAsync(Exception exception) + => _manager.DebugAsync(Name, exception); + public Task DebugAsync(string message, Exception exception = null) + => _manager.DebugAsync(Name, message, exception); + + public Task DebugAsync(FormattableString message, Exception exception = null) + => _manager.DebugAsync(Name, message, exception); + + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/LoginState.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/LoginState.cs new file mode 100644 index 0000000..49f86c9 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/LoginState.cs @@ -0,0 +1,15 @@ +namespace Discord +{ + /// Specifies the state of the client's login status. + public enum LoginState : byte + { + /// The client is currently logged out. + LoggedOut, + /// The client is currently logging in. + LoggingIn, + /// The client is currently logged in. + LoggedIn, + /// The client is currently logging out. + LoggingOut + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/HttpException.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/HttpException.cs new file mode 100644 index 0000000..d36bd66 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/HttpException.cs @@ -0,0 +1,74 @@ +using System; +using System.Net; + +namespace Discord.Net +{ + /// + /// The exception that is thrown if an error occurs while processing an Discord HTTP request. + /// + public class HttpException : Exception + { + /// + /// Gets the HTTP status code returned by Discord. + /// + /// + /// An + /// HTTP status code + /// from Discord. + /// + public HttpStatusCode HttpCode { get; } + /// + /// Gets the JSON error code returned by Discord. + /// + /// + /// A + /// JSON error code + /// from Discord, or null if none. + /// + public int? DiscordCode { get; } + /// + /// Gets the reason of the exception. + /// + public string Reason { get; } + /// + /// Gets the request object used to send the request. + /// + public IRequest Request { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP status code returned. + /// The request that was sent prior to the exception. + /// The Discord status code returned. + /// The reason behind the exception. + public HttpException(HttpStatusCode httpCode, IRequest request, int? discordCode = null, string reason = null) + : base(CreateMessage(httpCode, discordCode, reason)) + { + HttpCode = httpCode; + Request = request; + DiscordCode = discordCode; + Reason = reason; + } + + private static string CreateMessage(HttpStatusCode httpCode, int? discordCode = null, string reason = null) + { + string msg; + if (discordCode != null && discordCode != 0) + { + if (reason != null) + msg = $"The server responded with error {(int)discordCode}: {reason}"; + else + msg = $"The server responded with error {(int)discordCode}: {httpCode}"; + } + else + { + if (reason != null) + msg = $"The server responded with error {(int)httpCode}: {reason}"; + else + msg = $"The server responded with error {(int)httpCode}: {httpCode}"; + } + return msg; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/IRequest.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/IRequest.cs new file mode 100644 index 0000000..1f23e65 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/IRequest.cs @@ -0,0 +1,13 @@ +using System; + +namespace Discord.Net +{ + /// + /// Represents a generic request to be sent to Discord. + /// + public interface IRequest + { + DateTimeOffset? TimeoutAt { get; } + RequestOptions Options { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/RateLimitedException.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/RateLimitedException.cs new file mode 100644 index 0000000..c19487f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/RateLimitedException.cs @@ -0,0 +1,25 @@ +using System; + +namespace Discord.Net +{ + /// + /// The exception that is thrown when the user is being rate limited by Discord. + /// + public class RateLimitedException : TimeoutException + { + /// + /// Gets the request object used to send the request. + /// + public IRequest Request { get; } + + /// + /// Initializes a new instance of the class using the + /// sent. + /// + public RateLimitedException(IRequest request) + : base("You are being rate limited.") + { + Request = request; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/Rest/IRestClient.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/Rest/IRestClient.cs new file mode 100644 index 0000000..71010f7 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/Rest/IRestClient.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.Rest +{ + /// + /// Represents a generic REST-based client. + /// + public interface IRestClient : IDisposable + { + /// + /// Sets the HTTP header of this client for all requests. + /// + /// The field name of the header. + /// The value of the header. + void SetHeader(string key, string value); + /// + /// Sets the cancellation token for this client. + /// + /// The cancellation token. + void SetCancelToken(CancellationToken cancelToken); + + /// + /// Sends a REST request. + /// + /// The method used to send this request (i.e. HTTP verb such as GET, POST). + /// The endpoint to send this request to. + /// The cancellation token used to cancel the task. + /// Indicates whether to send the header only. + /// The audit log reason. + /// + Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly = false, string reason = null); + Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly = false, string reason = null); + Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly = false, string reason = null); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/Rest/RestClientProvider.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/Rest/RestClientProvider.cs new file mode 100644 index 0000000..51a7eb6 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/Rest/RestClientProvider.cs @@ -0,0 +1,4 @@ +namespace Discord.Net.Rest +{ + public delegate IRestClient RestClientProvider(string baseUrl); +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/Rest/RestResponse.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/Rest/RestResponse.cs new file mode 100644 index 0000000..412ff4d --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/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/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/Udp/IUdpSocket.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/Udp/IUdpSocket.cs new file mode 100644 index 0000000..ed2881d --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/Udp/IUdpSocket.cs @@ -0,0 +1,21 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.Udp +{ + public interface IUdpSocket : IDisposable + { + event Func ReceivedDatagram; + + ushort Port { get; } + + void SetCancelToken(CancellationToken cancelToken); + void SetDestination(string ip, int port); + + Task StartAsync(); + Task StopAsync(); + + Task SendAsync(byte[] data, int index, int count); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/Udp/UdpSocketProvider.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/Udp/UdpSocketProvider.cs new file mode 100644 index 0000000..07fbd4f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/Udp/UdpSocketProvider.cs @@ -0,0 +1,4 @@ +namespace Discord.Net.Udp +{ + public delegate IUdpSocket UdpSocketProvider(); +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/WebSocketClosedException.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/WebSocketClosedException.cs new file mode 100644 index 0000000..6e2564f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/WebSocketClosedException.cs @@ -0,0 +1,34 @@ +using System; +namespace Discord.Net +{ + /// + /// The exception that is thrown when the WebSocket session is closed by Discord. + /// + public class WebSocketClosedException : Exception + { + /// + /// Gets the close code sent by Discord. + /// + /// + /// A + /// close code + /// from Discord. + /// + public int CloseCode { get; } + /// + /// Gets the reason of the interruption. + /// + public string Reason { get; } + + /// + /// Initializes a new instance of the using a Discord close code + /// and an optional reason. + /// + public WebSocketClosedException(int closeCode, string reason = null) + : base($"The server sent close {closeCode}{(reason != null ? $": \"{reason}\"" : "")}") + { + CloseCode = closeCode; + Reason = reason; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs new file mode 100644 index 0000000..14b41cc --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs @@ -0,0 +1,21 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.WebSockets +{ + public interface IWebSocketClient : IDisposable + { + event Func BinaryMessage; + event Func TextMessage; + event Func Closed; + + void SetHeader(string key, string value); + void SetCancelToken(CancellationToken cancelToken); + + Task ConnectAsync(string host); + Task DisconnectAsync(); + + Task SendAsync(byte[] data, int index, int count, bool isText); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/WebSockets/WebSocketProvider.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/WebSockets/WebSocketProvider.cs new file mode 100644 index 0000000..88f4672 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Net/WebSockets/WebSocketProvider.cs @@ -0,0 +1,4 @@ +namespace Discord.Net.WebSockets +{ + public delegate IWebSocketClient WebSocketProvider(); +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/RateLimitPrecision.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/RateLimitPrecision.cs new file mode 100644 index 0000000..fe3c1b9 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/RateLimitPrecision.cs @@ -0,0 +1,18 @@ +namespace Discord +{ + /// + /// Specifies the level of precision to request in the rate limit + /// response header. + /// + public enum RateLimitPrecision + { + /// + /// Specifies precision rounded up to the nearest whole second + /// + Second, + /// + /// Specifies precision rounded to the nearest millisecond. + /// + Millisecond + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/RequestOptions.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/RequestOptions.cs new file mode 100644 index 0000000..6aa0eea --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/RequestOptions.cs @@ -0,0 +1,84 @@ +using System.Threading; + +namespace Discord +{ + /// + /// Represents options that should be used when sending a request. + /// + public class RequestOptions + { + /// + /// Creates a new class with its default settings. + /// + public static RequestOptions Default => new RequestOptions(); + + /// + /// Gets or sets the maximum time to wait for for this request to complete. + /// + /// + /// Gets or set the max time, in milliseconds, to wait for 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. + /// + /// + /// A in milliseconds for when the request times out. + /// + public int? Timeout { get; set; } + /// + /// Gets or sets the cancellation token for this request. + /// + /// + /// A for this request. + /// + public CancellationToken CancelToken { get; set; } = CancellationToken.None; + /// + /// Gets or sets the retry behavior when the request fails. + /// + public RetryMode? RetryMode { get; set; } + public bool HeaderOnly { get; internal set; } + /// + /// Gets or sets the reason for this action in the guild's audit log. + /// + /// + /// Gets or sets the reason that will be written to the guild's audit log if applicable. This may not apply + /// to all actions. + /// + public string AuditLogReason { get; set; } + /// + /// Gets or sets whether or not this request should use the system + /// clock for rate-limiting. Defaults to true. + /// + /// + /// This property can also be set in . + /// + /// On a per-request basis, the system clock should only be disabled + /// when millisecond precision is especially important, and the + /// hosting system is known to have a desynced clock. + /// + public bool? UseSystemClock { get; set; } + + internal bool IgnoreState { get; set; } + internal string BucketId { get; set; } + internal bool IsClientBucket { get; set; } + internal bool IsReactionBucket { get; set; } + + internal static RequestOptions CreateOrClone(RequestOptions options) + { + if (options == null) + return new RequestOptions(); + else + return options.Clone(); + } + + /// + /// Initializes a new class with the default request timeout set in + /// . + /// + public RequestOptions() + { + Timeout = DiscordConfig.DefaultRequestTimeout; + } + + public RequestOptions Clone() => MemberwiseClone() as RequestOptions; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/RetryMode.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/RetryMode.cs new file mode 100644 index 0000000..1e09f4d --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/RetryMode.cs @@ -0,0 +1,22 @@ +using System; + +namespace Discord +{ + /// Specifies how a request should act in the case of an error. + [Flags] + public enum RetryMode + { + /// If a request fails, an exception is thrown immediately. + AlwaysFail = 0x0, + /// Retry if a request timed out. + RetryTimeouts = 0x1, + // /// Retry if a request failed due to a network error. + //RetryErrors = 0x2, + /// Retry if a request failed due to a rate-limit. + RetryRatelimit = 0x4, + /// Retry if a request failed due to an HTTP error 502. + Retry502 = 0x8, + /// Continuously retry a request until it times out, its cancel token is triggered, or the server responds with a non-502 error. + AlwaysRetry = RetryTimeouts | /*RetryErrors |*/ RetryRatelimit | Retry502, + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/TokenType.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/TokenType.cs new file mode 100644 index 0000000..8ca3f03 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/TokenType.cs @@ -0,0 +1,23 @@ +using System; + +namespace Discord +{ + /// Specifies the type of token to use with the client. + public enum TokenType + { + [Obsolete("User logins are deprecated and may result in a ToS strike against your account - please see https://github.com/RogueException/Discord.Net/issues/827", error: true)] + User, + /// + /// An OAuth2 token type. + /// + Bearer, + /// + /// A bot token type. + /// + Bot, + /// + /// A webhook token type. + /// + Webhook + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/AsyncEvent.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/AsyncEvent.cs new file mode 100644 index 0000000..731489d --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/AsyncEvent.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading.Tasks; + +namespace Discord +{ + internal class AsyncEvent + where T : class + { + private readonly object _subLock = new object(); + internal ImmutableArray _subscriptions; + + public bool HasSubscribers => _subscriptions.Length != 0; + public IReadOnlyList Subscriptions => _subscriptions; + + public AsyncEvent() + { + _subscriptions = ImmutableArray.Create(); + } + + 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); + } + } + + internal static class EventExtensions + { + public static async Task InvokeAsync(this AsyncEvent> eventHandler) + { + var subscribers = eventHandler.Subscriptions; + for (int i = 0; i < subscribers.Count; i++) + await subscribers[i].Invoke().ConfigureAwait(false); + } + public static async Task InvokeAsync(this AsyncEvent> eventHandler, T arg) + { + var subscribers = eventHandler.Subscriptions; + for (int i = 0; i < subscribers.Count; i++) + await subscribers[i].Invoke(arg).ConfigureAwait(false); + } + public static async Task InvokeAsync(this AsyncEvent> eventHandler, T1 arg1, T2 arg2) + { + var subscribers = eventHandler.Subscriptions; + for (int i = 0; i < subscribers.Count; i++) + await subscribers[i].Invoke(arg1, arg2).ConfigureAwait(false); + } + public static async Task InvokeAsync(this AsyncEvent> eventHandler, T1 arg1, T2 arg2, T3 arg3) + { + var subscribers = eventHandler.Subscriptions; + for (int i = 0; i < subscribers.Count; i++) + await subscribers[i].Invoke(arg1, arg2, arg3).ConfigureAwait(false); + } + public static async Task InvokeAsync(this AsyncEvent> eventHandler, T1 arg1, T2 arg2, T3 arg3, T4 arg4) + { + var subscribers = eventHandler.Subscriptions; + for (int i = 0; i < subscribers.Count; i++) + await subscribers[i].Invoke(arg1, arg2, arg3, arg4).ConfigureAwait(false); + } + public static async Task InvokeAsync(this AsyncEvent> eventHandler, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) + { + var subscribers = eventHandler.Subscriptions; + for (int i = 0; i < subscribers.Count; i++) + await subscribers[i].Invoke(arg1, arg2, arg3, arg4, arg5).ConfigureAwait(false); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/Cacheable.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/Cacheable.cs new file mode 100644 index 0000000..1857ae7 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/Cacheable.cs @@ -0,0 +1,66 @@ +using System; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a cached entity. + /// + /// The type of entity that is cached. + /// The type of this entity's ID. + public struct Cacheable + where TEntity : IEntity + where TId : IEquatable + { + /// + /// Gets whether this entity is cached. + /// + public bool HasValue { get; } + /// + /// Gets the ID of this entity. + /// + public TId Id { get; } + /// + /// Gets the entity if it could be pulled from cache. + /// + /// + /// This value is not guaranteed to be set; in cases where the entity cannot be pulled from cache, it is + /// null. + /// + public TEntity Value { get; } + private Func> DownloadFunc { get; } + + internal Cacheable(TEntity value, TId id, bool hasValue , Func> downloadFunc) + { + Value = value; + Id = id; + HasValue = hasValue; + DownloadFunc = downloadFunc; + } + + /// + /// Downloads this entity to cache. + /// + /// Thrown when used from a user account. + /// Thrown when the message is deleted. + /// + /// A task that represents the asynchronous download operation. The task result contains the downloaded + /// entity. + /// + public async Task DownloadAsync() + { + return await DownloadFunc().ConfigureAwait(false); + } + + /// + /// Returns the cached entity if it exists; otherwise downloads it. + /// + /// Thrown when used from a user account. + /// Thrown when the message is deleted and is not in cache. + /// + /// A task that represents the asynchronous operation that attempts to get the message via cache or to + /// download the message. The task result contains the downloaded entity. + /// + public async Task GetOrDownloadAsync() => HasValue ? Value : await DownloadAsync().ConfigureAwait(false); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/Comparers.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/Comparers.cs new file mode 100644 index 0000000..40500ff --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/Comparers.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; + +namespace Discord +{ + /// + /// Represents a collection of for various Discord objects. + /// + public static class DiscordComparers + { + // TODO: simplify with '??=' slated for C# 8.0 + /// + /// Gets an to be used to compare users. + /// + public static IEqualityComparer UserComparer => _userComparer ?? (_userComparer = new EntityEqualityComparer()); + /// + /// Gets an to be used to compare guilds. + /// + public static IEqualityComparer GuildComparer => _guildComparer ?? (_guildComparer = new EntityEqualityComparer()); + /// + /// Gets an to be used to compare channels. + /// + public static IEqualityComparer ChannelComparer => _channelComparer ?? (_channelComparer = new EntityEqualityComparer()); + /// + /// Gets an to be used to compare roles. + /// + public static IEqualityComparer RoleComparer => _roleComparer ?? (_roleComparer = new EntityEqualityComparer()); + /// + /// Gets an to be used to compare messages. + /// + public static IEqualityComparer MessageComparer => _messageComparer ?? (_messageComparer = new EntityEqualityComparer()); + + private static IEqualityComparer _userComparer; + private static IEqualityComparer _guildComparer; + private static IEqualityComparer _channelComparer; + private static IEqualityComparer _roleComparer; + private static IEqualityComparer _messageComparer; + + private sealed class EntityEqualityComparer : EqualityComparer + where TEntity : IEntity + where TId : IEquatable + { + public override bool Equals(TEntity x, TEntity y) + { + bool xNull = x == null; + bool yNull = y == null; + + if (xNull && yNull) + return true; + + if (xNull ^ yNull) + return false; + + return x.Id.Equals(y.Id); + } + + public override int GetHashCode(TEntity obj) + { + return obj?.Id.GetHashCode() ?? 0; + } + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/ConcurrentHashSet.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/ConcurrentHashSet.cs new file mode 100644 index 0000000..308f084 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/ConcurrentHashSet.cs @@ -0,0 +1,476 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +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}")] + internal class ConcurrentHashSet : IReadOnlyCollection + { + private sealed class Tables + { + internal readonly Node[] _buckets; + internal readonly object[] _locks; + internal volatile int[] _countPerLock; + + internal Tables(Node[] buckets, object[] locks, int[] countPerLock) + { + _buckets = buckets; + _locks = locks; + _countPerLock = countPerLock; + } + } + private sealed class Node + { + internal readonly T _value; + internal volatile Node _next; + internal readonly int _hashcode; + + internal Node(T key, int hashcode, Node next) + { + _value = key; + _next = next; + _hashcode = hashcode; + } + } + + private const int DefaultCapacity = 31; + private const int MaxLockNumber = 1024; + + private static int GetBucket(int hashcode, int bucketCount) + { + int bucketNo = (hashcode & 0x7fffffff) % bucketCount; + return bucketNo; + } + private static void GetBucketAndLockNo(int hashcode, out int bucketNo, out int lockNo, int bucketCount, int lockCount) + { + bucketNo = (hashcode & 0x7fffffff) % bucketCount; + lockNo = bucketNo % lockCount; + } + private static int DefaultConcurrencyLevel => ConcurrentHashSet.DefaultConcurrencyLevel; + + private volatile Tables _tables; + private readonly IEqualityComparer _comparer; + private readonly bool _growLockArray; + private int _budget; + + public int Count + { + get + { + int count = 0; + + int acquiredLocks = 0; + try + { + AcquireAllLocks(ref acquiredLocks); + + for (int i = 0; i < _tables._countPerLock.Length; i++) + count += _tables._countPerLock[i]; + } + finally { ReleaseLocks(0, acquiredLocks); } + + return count; + } + } + public bool IsEmpty + { + get + { + int acquiredLocks = 0; + try + { + // Acquire all locks + AcquireAllLocks(ref acquiredLocks); + + for (int i = 0; i < _tables._countPerLock.Length; i++) + { + if (_tables._countPerLock[i] != 0) + return false; + } + } + finally { ReleaseLocks(0, acquiredLocks); } + + return true; + } + } + public ReadOnlyCollection Values + { + get + { + int locksAcquired = 0; + try + { + AcquireAllLocks(ref locksAcquired); + List values = new List(); + + for (int i = 0; i < _tables._buckets.Length; i++) + { + Node current = _tables._buckets[i]; + while (current != null) + { + values.Add(current._value); + current = current._next; + } + } + + return new ReadOnlyCollection(values); + } + finally { ReleaseLocks(0, locksAcquired); } + } + } + + public ConcurrentHashSet() + : this(DefaultConcurrencyLevel, DefaultCapacity, true, EqualityComparer.Default) { } + public ConcurrentHashSet(int concurrencyLevel, int capacity) + : this(concurrencyLevel, capacity, false, EqualityComparer.Default) { } + public ConcurrentHashSet(IEnumerable collection) + : this(collection, EqualityComparer.Default) { } + public ConcurrentHashSet(IEqualityComparer comparer) + : this(DefaultConcurrencyLevel, DefaultCapacity, true, comparer) { } + /// is null + public ConcurrentHashSet(IEnumerable collection, IEqualityComparer comparer) + : this(comparer) + { + if (collection == null) throw new ArgumentNullException(paramName: nameof(collection)); + InitializeFromCollection(collection); + } + /// + /// or is null + /// + public ConcurrentHashSet(int concurrencyLevel, IEnumerable collection, IEqualityComparer comparer) + : this(concurrencyLevel, DefaultCapacity, false, comparer) + { + if (collection == null) throw new ArgumentNullException(paramName: nameof(collection)); + if (comparer == null) throw new ArgumentNullException(paramName: nameof(comparer)); + InitializeFromCollection(collection); + } + public ConcurrentHashSet(int concurrencyLevel, int capacity, IEqualityComparer comparer) + : this(concurrencyLevel, capacity, false, comparer) { } + internal ConcurrentHashSet(int concurrencyLevel, int capacity, bool growLockArray, IEqualityComparer comparer) + { + if (concurrencyLevel < 1) throw new ArgumentOutOfRangeException(paramName: nameof(concurrencyLevel)); + if (capacity < 0) throw new ArgumentOutOfRangeException(paramName: nameof(capacity)); + if (comparer == null) throw new ArgumentNullException(paramName: nameof(comparer)); + + if (capacity < concurrencyLevel) + capacity = concurrencyLevel; + + object[] locks = new object[concurrencyLevel]; + for (int i = 0; i < locks.Length; i++) + locks[i] = new object(); + + int[] countPerLock = new int[locks.Length]; + Node[] buckets = new Node[capacity]; + _tables = new Tables(buckets, locks, countPerLock); + + _comparer = comparer; + _growLockArray = growLockArray; + _budget = buckets.Length / locks.Length; + } + private void InitializeFromCollection(IEnumerable collection) + { + foreach (var value in collection) + { + if (value == null) throw new ArgumentNullException(paramName: "key"); + + if (!TryAddInternal(value, _comparer.GetHashCode(value), false)) + throw new ArgumentException(); + } + + if (_budget == 0) + _budget = _tables._buckets.Length / _tables._locks.Length; + } + /// is null + public bool ContainsKey(T value) + { + if (value == null) throw new ArgumentNullException(paramName: "key"); + return ContainsKeyInternal(value, _comparer.GetHashCode(value)); + } + private bool ContainsKeyInternal(T value, int hashcode) + { + Tables tables = _tables; + + int bucketNo = GetBucket(hashcode, tables._buckets.Length); + + Node n = Volatile.Read(ref tables._buckets[bucketNo]); + + while (n != null) + { + if (hashcode == n._hashcode && _comparer.Equals(n._value, value)) + return true; + n = n._next; + } + + return false; + } + + /// is null + public bool TryAdd(T value) + { + if (value == null) throw new ArgumentNullException(paramName: "key"); + return TryAddInternal(value, _comparer.GetHashCode(value), true); + } + private bool TryAddInternal(T value, int hashcode, bool acquireLock) + { + while (true) + { + Tables tables = _tables; + GetBucketAndLockNo(hashcode, out int bucketNo, out int lockNo, tables._buckets.Length, tables._locks.Length); + + bool resizeDesired = false; + bool lockTaken = false; + try + { + if (acquireLock) + Monitor.Enter(tables._locks[lockNo], ref lockTaken); + + if (tables != _tables) + continue; + + Node prev = null; + for (Node node = tables._buckets[bucketNo]; node != null; node = node._next) + { + if (hashcode == node._hashcode && _comparer.Equals(node._value, value)) + return false; + prev = node; + } + + Volatile.Write(ref tables._buckets[bucketNo], new Node(value, hashcode, tables._buckets[bucketNo])); + checked { tables._countPerLock[lockNo]++; } + + if (tables._countPerLock[lockNo] > _budget) + resizeDesired = true; + } + finally + { + if (lockTaken) + Monitor.Exit(tables._locks[lockNo]); + } + + if (resizeDesired) + GrowTable(tables); + + return true; + } + } + + /// is null + public bool TryRemove(T value) + { + if (value == null) throw new ArgumentNullException(paramName: "key"); + return TryRemoveInternal(value); + } + private bool TryRemoveInternal(T value) + { + int hashcode = _comparer.GetHashCode(value); + while (true) + { + Tables tables = _tables; + GetBucketAndLockNo(hashcode, out int bucketNo, out int lockNo, tables._buckets.Length, tables._locks.Length); + + lock (tables._locks[lockNo]) + { + if (tables != _tables) + continue; + + Node prev = null; + for (Node curr = tables._buckets[bucketNo]; curr != null; curr = curr._next) + { + if (hashcode == curr._hashcode && _comparer.Equals(curr._value, value)) + { + if (prev == null) + Volatile.Write(ref tables._buckets[bucketNo], curr._next); + else + prev._next = curr._next; + + value = curr._value; + tables._countPerLock[lockNo]--; + return true; + } + prev = curr; + } + } + + value = default(T); + return false; + } + } + + public void Clear() + { + int locksAcquired = 0; + try + { + AcquireAllLocks(ref locksAcquired); + + Tables newTables = new Tables(new Node[DefaultCapacity], _tables._locks, new int[_tables._countPerLock.Length]); + _tables = newTables; + _budget = Math.Max(1, newTables._buckets.Length / newTables._locks.Length); + } + finally + { + ReleaseLocks(0, locksAcquired); + } + } + + public IEnumerator GetEnumerator() + { + Node[] buckets = _tables._buckets; + + for (int i = 0; i < buckets.Length; i++) + { + Node current = Volatile.Read(ref buckets[i]); + + while (current != null) + { + yield return current._value; + current = current._next; + } + } + } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + private void GrowTable(Tables tables) + { + const int MaxArrayLength = 0X7FEFFFFF; + int locksAcquired = 0; + try + { + AcquireLocks(0, 1, ref locksAcquired); + if (tables != _tables) + return; + + long approxCount = 0; + for (int i = 0; i < tables._countPerLock.Length; i++) + approxCount += tables._countPerLock[i]; + + if (approxCount < tables._buckets.Length / 4) + { + _budget = 2 * _budget; + if (_budget < 0) + _budget = int.MaxValue; + return; + } + + int newLength = 0; + bool maximizeTableSize = false; + try + { + checked + { + newLength = tables._buckets.Length * 2 + 1; + while (newLength % 3 == 0 || newLength % 5 == 0 || newLength % 7 == 0) + newLength += 2; + + if (newLength > MaxArrayLength) + maximizeTableSize = true; + } + } + catch (OverflowException) + { + maximizeTableSize = true; + } + + if (maximizeTableSize) + { + newLength = MaxArrayLength; + _budget = int.MaxValue; + } + + AcquireLocks(1, tables._locks.Length, ref locksAcquired); + + object[] newLocks = tables._locks; + + if (_growLockArray && tables._locks.Length < MaxLockNumber) + { + newLocks = new object[tables._locks.Length * 2]; + Array.Copy(tables._locks, 0, newLocks, 0, tables._locks.Length); + for (int i = tables._locks.Length; i < newLocks.Length; i++) + newLocks[i] = new object(); + } + + Node[] newBuckets = new Node[newLength]; + int[] newCountPerLock = new int[newLocks.Length]; + + for (int i = 0; i < tables._buckets.Length; i++) + { + Node current = tables._buckets[i]; + while (current != null) + { + Node next = current._next; + GetBucketAndLockNo(current._hashcode, out int newBucketNo, out int newLockNo, newBuckets.Length, newLocks.Length); + + newBuckets[newBucketNo] = new Node(current._value, current._hashcode, newBuckets[newBucketNo]); + + checked { newCountPerLock[newLockNo]++; } + + current = next; + } + } + + _budget = Math.Max(1, newBuckets.Length / newLocks.Length); + _tables = new Tables(newBuckets, newLocks, newCountPerLock); + } + finally { ReleaseLocks(0, locksAcquired); } + } + + private void AcquireAllLocks(ref int locksAcquired) + { + AcquireLocks(0, 1, ref locksAcquired); + AcquireLocks(1, _tables._locks.Length, ref locksAcquired); + } + private void AcquireLocks(int fromInclusive, int toExclusive, ref int locksAcquired) + { + object[] locks = _tables._locks; + + for (int i = fromInclusive; i < toExclusive; i++) + { + bool lockTaken = false; + try + { + Monitor.Enter(locks[i], ref lockTaken); + } + finally + { + if (lockTaken) + locksAcquired++; + } + } + } + private void ReleaseLocks(int fromInclusive, int toExclusive) + { + for (int i = fromInclusive; i < toExclusive; i++) + Monitor.Exit(_tables._locks[i]); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/DateTimeUtils.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/DateTimeUtils.cs new file mode 100644 index 0000000..6084768 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/DateTimeUtils.cs @@ -0,0 +1,13 @@ +using System; + +namespace Discord +{ + /// + internal static class DateTimeUtils + { + 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/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/MentionUtils.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/MentionUtils.cs new file mode 100644 index 0000000..6ffb7ee --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/MentionUtils.cs @@ -0,0 +1,316 @@ +using System; +using System.Globalization; +using System.Text; + +namespace Discord +{ + /// + /// Provides a series of helper methods for parsing mentions. + /// + 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}>"; + /// + /// Returns a mention string based on the user ID. + /// + /// + /// A user mention string (e.g. <@80351110224678912>). + /// + public static string MentionUser(ulong id) => MentionUser(id.ToString(), true); + internal static string MentionChannel(string id) => $"<#{id}>"; + /// + /// Returns a mention string based on the channel ID. + /// + /// + /// A channel mention string (e.g. <#103735883630395392>). + /// + public static string MentionChannel(ulong id) => MentionChannel(id.ToString()); + internal static string MentionRole(string id) => $"<@&{id}>"; + /// + /// Returns a mention string based on the role ID. + /// + /// + /// A role mention string (e.g. <@&165511591545143296>). + /// + public static string MentionRole(ulong id) => MentionRole(id.ToString()); + + /// + /// Parses a provided user mention string. + /// + /// Invalid mention format. + public static ulong ParseUser(string text) + { + if (TryParseUser(text, out ulong id)) + return id; + throw new ArgumentException(message: "Invalid mention format.", paramName: 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. + /// + /// Invalid mention format. + public static ulong ParseChannel(string text) + { + if (TryParseChannel(text, out ulong id)) + return id; + throw new ArgumentException(message: "Invalid mention format.", paramName: 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. + /// + /// Invalid mention format. + public static ulong ParseRole(string text) + { + if (TryParseRole(text, out ulong id)) + return id; + throw new ArgumentException(message: "Invalid mention format.", paramName: 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, int startIndex, TagHandling userHandling, TagHandling channelHandling, TagHandling roleHandling, TagHandling everyoneHandling, TagHandling emojiHandling) + { + var text = new StringBuilder(msg.Content.Substring(startIndex)); + var tags = msg.Tags; + int indexOffset = -startIndex; + + foreach (var tag in tags) + { + if (tag.Index < startIndex) + continue; + + 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 ""; + case TagHandling.NameNoPrefix: + if (user != null) + return $"{guildUser?.Nickname ?? user?.Username}"; + else + return ""; + case TagHandling.FullName: + if (user != null) + return $"@{user.Username}#{user.Discriminator}"; + else + return ""; + case TagHandling.FullNameNoPrefix: + if (user != null) + return $"{user.Username}#{user.Discriminator}"; + else + return ""; + 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 ""; + case TagHandling.NameNoPrefix: + case TagHandling.FullNameNoPrefix: + if (channel != null) + return $"{channel.Name}"; + else + return ""; + 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 ""; + case TagHandling.NameNoPrefix: + case TagHandling.FullNameNoPrefix: + if (role != null) + return $"{role.Name}"; + else + return ""; + 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: + case TagHandling.NameNoPrefix: + case TagHandling.FullNameNoPrefix: + 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: + case TagHandling.NameNoPrefix: + case TagHandling.FullNameNoPrefix: + return "here"; + case TagHandling.Sanitize: + return $"@{SanitizeChar}here"; + } + } + return ""; + } + internal static string ResolveEmoji(ITag tag, TagHandling mode) + { + if (mode != TagHandling.Remove) + { + Emote emoji = (Emote)tag.Value; + + //Remove if its name contains any bad chars (prevents a few tag exploits) + for (int i = 0; i < emoji.Name.Length; i++) + { + char c = emoji.Name[i]; + if (!char.IsLetterOrDigit(c) && c != '_' && c != '-') + return ""; + } + + switch (mode) + { + case TagHandling.Name: + case TagHandling.FullName: + return $":{emoji.Name}:"; + case TagHandling.NameNoPrefix: + case TagHandling.FullNameNoPrefix: + return $"{emoji.Name}"; + case TagHandling.Sanitize: + return $"<{emoji.Id}{SanitizeChar}:{SanitizeChar}{emoji.Name}>"; + } + } + return ""; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/Optional.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/Optional.cs new file mode 100644 index 0000000..3481796 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/Optional.cs @@ -0,0 +1,60 @@ +using System; +using System.Diagnostics; + +namespace Discord +{ + //Based on https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/Nullable.cs + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public struct Optional + { + public static Optional Unspecified => default(Optional); + private readonly T _value; + + /// Gets the value for this parameter. + /// This property has no value set. + public T Value + { + get + { + if (!IsSpecified) + throw new InvalidOperationException("This property has no value set."); + return _value; + } + } + /// Returns true if this value has been specified. + public bool IsSpecified { get; } + + /// Creates a new Parameter with the provided value. + public Optional(T value) + { + _value = value; + IsSpecified = true; + } + + public T GetValueOrDefault() => _value; + public T GetValueOrDefault(T defaultValue) => IsSpecified ? _value : defaultValue; + + public override bool Equals(object other) + { + if (!IsSpecified) return other == null; + if (other == null) return false; + return _value.Equals(other); + } + public override int GetHashCode() => IsSpecified ? _value.GetHashCode() : 0; + + public override string ToString() => IsSpecified ? _value?.ToString() : null; + private string DebuggerDisplay => IsSpecified ? (_value?.ToString() ?? "") : ""; + + public static implicit operator Optional(T value) => new Optional(value); + public static explicit operator T(Optional value) => value.Value; + } + public static class Optional + { + public static Optional Create() => Optional.Unspecified; + public static Optional Create(T value) => new Optional(value); + + public static T? ToNullable(this Optional val) + where T : struct + => val.IsSpecified ? val.Value : (T?)null; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/Paging/Page.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/Paging/Page.cs new file mode 100644 index 0000000..996d0ac --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/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/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/Paging/PageInfo.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/Paging/PageInfo.cs new file mode 100644 index 0000000..3b49225 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/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/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/Paging/PagedEnumerator.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/Paging/PagedEnumerator.cs new file mode 100644 index 0000000..8420990 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/Paging/PagedEnumerator.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +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 Func, bool> _nextPage; + + public PagedAsyncEnumerable(int pageSize, Func>> getPage, Func, bool> nextPage = null, + ulong? start = null, int? count = null) + { + PageSize = pageSize; + _start = start; + _count = count; + + _getPage = getPage; + _nextPage = nextPage; + } + + public IAsyncEnumerator> GetAsyncEnumerator(CancellationToken cancellationToken = new CancellationToken()) => new Enumerator(this, cancellationToken); + internal class Enumerator : IAsyncEnumerator> + { + private readonly PagedAsyncEnumerable _source; + private readonly CancellationToken _token; + private readonly PageInfo _info; + + public IReadOnlyCollection Current { get; private set; } + + public Enumerator(PagedAsyncEnumerable source, CancellationToken token) + { + _source = source; + _token = token; + _info = new PageInfo(source._start, source._count, source.PageSize); + } + + public async ValueTask MoveNextAsync() + { + if (_info.Remaining == 0) + return false; + + var data = await _source._getPage(_info, _token).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 ? Math.Min(_info.Remaining.Value, _source.PageSize) : _source.PageSize; + + if (_info.Remaining != 0) + { + if (!_source._nextPage(_info, data)) + _info.Remaining = 0; + } + + return true; + } + + public ValueTask DisposeAsync() + { + Current = null; + return default; + } + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/Permissions.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/Permissions.cs new file mode 100644 index 0000000..fd0fe09 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/Permissions.cs @@ -0,0 +1,174 @@ +using System.Runtime.CompilerServices; + +namespace Discord +{ + internal static class Permissions + { + public const int MaxBits = 53; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PermValue GetValue(ulong allow, ulong deny, ChannelPermission flag) + => GetValue(allow, deny, (ulong)flag); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PermValue GetValue(ulong allow, ulong deny, GuildPermission flag) + => GetValue(allow, deny, (ulong)flag); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PermValue GetValue(ulong allow, ulong deny, ulong flag) + { + if (HasFlag(allow, flag)) + return PermValue.Allow; + else if (HasFlag(deny, flag)) + return PermValue.Deny; + else + return PermValue.Inherit; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool GetValue(ulong value, ChannelPermission flag) + => GetValue(value, (ulong)flag); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool GetValue(ulong value, GuildPermission flag) + => GetValue(value, (ulong)flag); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool GetValue(ulong value, ulong flag) => HasFlag(value, flag); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetValue(ref ulong rawValue, bool? value, ChannelPermission flag) + => SetValue(ref rawValue, value, (ulong)flag); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetValue(ref ulong rawValue, bool? value, GuildPermission flag) + => SetValue(ref rawValue, value, (ulong)flag); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetValue(ref ulong rawValue, bool? value, ulong flag) + { + if (value.HasValue) + { + if (value == true) + SetFlag(ref rawValue, flag); + else + UnsetFlag(ref rawValue, flag); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetValue(ref ulong allow, ref ulong deny, PermValue? value, ChannelPermission flag) + => SetValue(ref allow, ref deny, value, (ulong)flag); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetValue(ref ulong allow, ref ulong deny, PermValue? value, GuildPermission flag) + => SetValue(ref allow, ref deny, value, (ulong)flag); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetValue(ref ulong allow, ref ulong deny, PermValue? value, ulong flag) + { + if (value.HasValue) + { + switch (value) + { + case PermValue.Allow: + SetFlag(ref allow, flag); + UnsetFlag(ref deny, flag); + break; + case PermValue.Deny: + UnsetFlag(ref allow, flag); + SetFlag(ref deny, flag); + break; + default: + UnsetFlag(ref allow, flag); + UnsetFlag(ref deny, flag); + break; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool HasFlag(ulong value, ulong flag) => (value & flag) == flag; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetFlag(ref ulong value, ulong flag) => value |= flag; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void UnsetFlag(ref ulong value, ulong flag) => value &= ~flag; + + public static ChannelPermissions ToChannelPerms(IGuildChannel channel, ulong guildPermissions) + => new ChannelPermissions(guildPermissions & ChannelPermissions.All(channel).RawValue); + public static ulong ResolveGuild(IGuild guild, IGuildUser user) + { + ulong resolvedPermissions = 0; + + if (user.Id == guild.OwnerId) + resolvedPermissions = GuildPermissions.All.RawValue; //Owners always have all permissions + else if (user.IsWebhook) + resolvedPermissions = GuildPermissions.Webhook.RawValue; + else + { + 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 + } + return resolvedPermissions; + } + + /*public static ulong ResolveChannel(IGuildUser user, IGuildChannel channel) + { + return ResolveChannel(user, channel, ResolveGuild(user)); + }*/ + public static ulong ResolveChannel(IGuild guild, IGuildUser user, IGuildChannel channel, ulong guildPermissions) + { + ulong resolvedPermissions = 0; + + ulong mask = ChannelPermissions.All(channel).RawValue; + if (GetValue(guildPermissions, GuildPermission.Administrator)) //Includes owner + resolvedPermissions = mask; //Owners and administrators always have all permissions + else + { + //Start with this user's guild permissions + resolvedPermissions = guildPermissions; + + //Give/Take Everyone permissions + var perms = channel.GetPermissionOverwrite(guild.EveryoneRole); + if (perms != null) + resolvedPermissions = (resolvedPermissions & ~perms.Value.DenyValue) | perms.Value.AllowValue; + + //Give/Take Role permissions + ulong deniedPermissions = 0UL, allowedPermissions = 0UL; + foreach (var roleId in user.RoleIds) + { + IRole role; + if (roleId != guild.EveryoneRole.Id && (role = guild.GetRole(roleId)) != null) + { + perms = channel.GetPermissionOverwrite(role); + if (perms != null) + { + allowedPermissions |= perms.Value.AllowValue; + deniedPermissions |= perms.Value.DenyValue; + } + } + } + resolvedPermissions = (resolvedPermissions & ~deniedPermissions) | allowedPermissions; + + //Give/Take User permissions + perms = channel.GetPermissionOverwrite(user); + if (perms != null) + resolvedPermissions = (resolvedPermissions & ~perms.Value.DenyValue) | perms.Value.AllowValue; + + if (channel is ITextChannel) + { + if (!GetValue(resolvedPermissions, ChannelPermission.ViewChannel)) + { + //No read permission on a text channel removes all other permissions + resolvedPermissions = 0; + } + else if (!GetValue(resolvedPermissions, ChannelPermission.SendMessages)) + { + //No send permissions on a text channel removes all send-related permissions + resolvedPermissions &= ~(ulong)ChannelPermission.SendTTSMessages; + resolvedPermissions &= ~(ulong)ChannelPermission.MentionEveryone; + resolvedPermissions &= ~(ulong)ChannelPermission.EmbedLinks; + resolvedPermissions &= ~(ulong)ChannelPermission.AttachFiles; + } + } + resolvedPermissions &= mask; //Ensure we didn't get any permissions this channel doesn't support (from guildPerms, for example) + } + + return resolvedPermissions; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/Preconditions.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/Preconditions.cs new file mode 100644 index 0000000..6041585 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/Preconditions.cs @@ -0,0 +1,297 @@ +using System; + +namespace Discord +{ + internal static class Preconditions + { + //Objects + /// must not be . + public static void NotNull(T obj, string name, string msg = null) where T : class { if (obj == null) throw CreateNotNullException(name, msg); } + /// must not be . + public static void NotNull(Optional obj, string name, string msg = null) where T : class { if (obj.IsSpecified && obj.Value == null) throw CreateNotNullException(name, msg); } + + private static ArgumentNullException CreateNotNullException(string name, string msg) + { + if (msg == null) return new ArgumentNullException(paramName: name); + else return new ArgumentNullException(paramName: name, message: msg); + } + + //Strings + /// cannot be blank. + public static void NotEmpty(string obj, string name, string msg = null) { if (obj.Length == 0) throw CreateNotEmptyException(name, msg); } + /// cannot be blank. + public static void NotEmpty(Optional obj, string name, string msg = null) { if (obj.IsSpecified && obj.Value.Length == 0) throw CreateNotEmptyException(name, msg); } + /// cannot be blank. + /// must not be . + public static void NotNullOrEmpty(string obj, string name, string msg = null) + { + if (obj == null) throw CreateNotNullException(name, msg); + if (obj.Length == 0) throw CreateNotEmptyException(name, msg); + } + /// cannot be blank. + /// must not be . + public static void NotNullOrEmpty(Optional obj, string name, string msg = null) + { + if (obj.IsSpecified) + { + if (obj.Value == null) throw CreateNotNullException(name, msg); + if (obj.Value.Length == 0) throw CreateNotEmptyException(name, msg); + } + } + /// cannot be blank. + /// must not be . + public static void NotNullOrWhitespace(string obj, string name, string msg = null) + { + if (obj == null) throw CreateNotNullException(name, msg); + if (obj.Trim().Length == 0) throw CreateNotEmptyException(name, msg); + } + /// cannot be blank. + /// must not be . + public static void NotNullOrWhitespace(Optional obj, string name, string msg = null) + { + if (obj.IsSpecified) + { + if (obj.Value == null) throw CreateNotNullException(name, msg); + if (obj.Value.Trim().Length == 0) throw CreateNotEmptyException(name, msg); + } + } + + private static ArgumentException CreateNotEmptyException(string name, string msg) + => new ArgumentException(message: msg ?? "Argument cannot be blank.", paramName: name); + + //Numerics + /// Value may not be equal to . + public static void NotEqual(sbyte obj, sbyte value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . + public static void NotEqual(byte obj, byte value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . + public static void NotEqual(short obj, short value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . + public static void NotEqual(ushort obj, ushort value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . + public static void NotEqual(int obj, int value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . + public static void NotEqual(uint obj, uint value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . + public static void NotEqual(long obj, long value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . + public static void NotEqual(ulong obj, ulong value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . + public static void NotEqual(Optional obj, sbyte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . + public static void NotEqual(Optional obj, byte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . + public static void NotEqual(Optional obj, short value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . + public static void NotEqual(Optional obj, ushort value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . + public static void NotEqual(Optional obj, int value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . + public static void NotEqual(Optional obj, uint value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . + public static void NotEqual(Optional obj, long value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . + public static void NotEqual(Optional obj, ulong value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . + public static void NotEqual(sbyte? obj, sbyte value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . + public static void NotEqual(byte? obj, byte value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . + public static void NotEqual(short? obj, short value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . + public static void NotEqual(ushort? obj, ushort value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . + public static void NotEqual(int? obj, int value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . + public static void NotEqual(uint? obj, uint value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . + public static void NotEqual(long? obj, long value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . + public static void NotEqual(ulong? obj, ulong value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . + public static void NotEqual(Optional obj, sbyte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . + public static void NotEqual(Optional obj, byte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . + public static void NotEqual(Optional obj, short value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . + public static void NotEqual(Optional obj, ushort value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . + public static void NotEqual(Optional obj, int value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . + public static void NotEqual(Optional obj, uint value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . + public static void NotEqual(Optional obj, long value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + /// Value may not be equal to . + public static void NotEqual(Optional obj, ulong value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + + private static ArgumentException CreateNotEqualException(string name, string msg, T value) + => new ArgumentException(message: msg ?? $"Value may not be equal to {value}.", paramName: name); + + /// Value must be at least . + public static void AtLeast(sbyte obj, sbyte value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + /// Value must be at least . + public static void AtLeast(byte obj, byte value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + /// Value must be at least . + public static void AtLeast(short obj, short value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + /// Value must be at least . + public static void AtLeast(ushort obj, ushort value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + /// Value must be at least . + public static void AtLeast(int obj, int value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + /// Value must be at least . + public static void AtLeast(uint obj, uint value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + /// Value must be at least . + public static void AtLeast(long obj, long value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + /// Value must be at least . + public static void AtLeast(ulong obj, ulong value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + /// Value must be at least . + public static void AtLeast(Optional obj, sbyte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value < value) throw CreateAtLeastException(name, msg, value); } + /// Value must be at least . + public static void AtLeast(Optional obj, byte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value < value) throw CreateAtLeastException(name, msg, value); } + /// Value must be at least . + public static void AtLeast(Optional obj, short value, string name, string msg = null) { if (obj.IsSpecified && obj.Value < value) throw CreateAtLeastException(name, msg, value); } + /// Value must be at least . + public static void AtLeast(Optional obj, ushort value, string name, string msg = null) { if (obj.IsSpecified && obj.Value < value) throw CreateAtLeastException(name, msg, value); } + /// Value must be at least . + public static void AtLeast(Optional obj, int value, string name, string msg = null) { if (obj.IsSpecified && obj.Value < value) throw CreateAtLeastException(name, msg, value); } + /// Value must be at least . + public static void AtLeast(Optional obj, uint value, string name, string msg = null) { if (obj.IsSpecified && obj.Value < value) throw CreateAtLeastException(name, msg, value); } + /// Value must be at least . + public static void AtLeast(Optional obj, long value, string name, string msg = null) { if (obj.IsSpecified && obj.Value < value) throw CreateAtLeastException(name, msg, value); } + /// Value must be at least . + public static void AtLeast(Optional obj, ulong value, string name, string msg = null) { if (obj.IsSpecified && obj.Value < value) throw CreateAtLeastException(name, msg, value); } + + private static ArgumentException CreateAtLeastException(string name, string msg, T value) + => new ArgumentException(message: msg ?? $"Value must be at least {value}.", paramName: name); + + /// Value must be greater than . + public static void GreaterThan(sbyte obj, sbyte value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + /// Value must be greater than . + public static void GreaterThan(byte obj, byte value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + /// Value must be greater than . + public static void GreaterThan(short obj, short value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + /// Value must be greater than . + public static void GreaterThan(ushort obj, ushort value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + /// Value must be greater than . + public static void GreaterThan(int obj, int value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + /// Value must be greater than . + public static void GreaterThan(uint obj, uint value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + /// Value must be greater than . + public static void GreaterThan(long obj, long value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + /// Value must be greater than . + public static void GreaterThan(ulong obj, ulong value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + /// Value must be greater than . + public static void GreaterThan(Optional obj, sbyte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value <= value) throw CreateGreaterThanException(name, msg, value); } + /// Value must be greater than . + public static void GreaterThan(Optional obj, byte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value <= value) throw CreateGreaterThanException(name, msg, value); } + /// Value must be greater than . + public static void GreaterThan(Optional obj, short value, string name, string msg = null) { if (obj.IsSpecified && obj.Value <= value) throw CreateGreaterThanException(name, msg, value); } + /// Value must be greater than . + public static void GreaterThan(Optional obj, ushort value, string name, string msg = null) { if (obj.IsSpecified && obj.Value <= value) throw CreateGreaterThanException(name, msg, value); } + /// Value must be greater than . + public static void GreaterThan(Optional obj, int value, string name, string msg = null) { if (obj.IsSpecified && obj.Value <= value) throw CreateGreaterThanException(name, msg, value); } + /// Value must be greater than . + public static void GreaterThan(Optional obj, uint value, string name, string msg = null) { if (obj.IsSpecified && obj.Value <= value) throw CreateGreaterThanException(name, msg, value); } + /// Value must be greater than . + public static void GreaterThan(Optional obj, long value, string name, string msg = null) { if (obj.IsSpecified && obj.Value <= value) throw CreateGreaterThanException(name, msg, value); } + /// Value must be greater than . + public static void GreaterThan(Optional obj, ulong value, string name, string msg = null) { if (obj.IsSpecified && obj.Value <= value) throw CreateGreaterThanException(name, msg, value); } + + private static ArgumentException CreateGreaterThanException(string name, string msg, T value) + => new ArgumentException(message: msg ?? $"Value must be greater than {value}.", paramName: name); + + /// Value must be at most . + public static void AtMost(sbyte obj, sbyte value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + /// Value must be at most . + public static void AtMost(byte obj, byte value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + /// Value must be at most . + public static void AtMost(short obj, short value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + /// Value must be at most . + public static void AtMost(ushort obj, ushort value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + /// Value must be at most . + public static void AtMost(int obj, int value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + /// Value must be at most . + public static void AtMost(uint obj, uint value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + /// Value must be at most . + public static void AtMost(long obj, long value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + /// Value must be at most . + public static void AtMost(ulong obj, ulong value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + /// Value must be at most . + public static void AtMost(Optional obj, sbyte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value > value) throw CreateAtMostException(name, msg, value); } + /// Value must be at most . + public static void AtMost(Optional obj, byte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value > value) throw CreateAtMostException(name, msg, value); } + /// Value must be at most . + public static void AtMost(Optional obj, short value, string name, string msg = null) { if (obj.IsSpecified && obj.Value > value) throw CreateAtMostException(name, msg, value); } + /// Value must be at most . + public static void AtMost(Optional obj, ushort value, string name, string msg = null) { if (obj.IsSpecified && obj.Value > value) throw CreateAtMostException(name, msg, value); } + /// Value must be at most . + public static void AtMost(Optional obj, int value, string name, string msg = null) { if (obj.IsSpecified && obj.Value > value) throw CreateAtMostException(name, msg, value); } + /// Value must be at most . + public static void AtMost(Optional obj, uint value, string name, string msg = null) { if (obj.IsSpecified && obj.Value > value) throw CreateAtMostException(name, msg, value); } + /// Value must be at most . + public static void AtMost(Optional obj, long value, string name, string msg = null) { if (obj.IsSpecified && obj.Value > value) throw CreateAtMostException(name, msg, value); } + /// Value must be at most . + public static void AtMost(Optional obj, ulong value, string name, string msg = null) { if (obj.IsSpecified && obj.Value > value) throw CreateAtMostException(name, msg, value); } + + private static ArgumentException CreateAtMostException(string name, string msg, T value) + => new ArgumentException(message: msg ?? $"Value must be at most {value}.", paramName: name); + + /// Value must be less than . + public static void LessThan(sbyte obj, sbyte value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + /// Value must be less than . + public static void LessThan(byte obj, byte value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + /// Value must be less than . + public static void LessThan(short obj, short value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + /// Value must be less than . + public static void LessThan(ushort obj, ushort value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + /// Value must be less than . + public static void LessThan(int obj, int value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + /// Value must be less than . + public static void LessThan(uint obj, uint value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + /// Value must be less than . + public static void LessThan(long obj, long value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + /// Value must be less than . + public static void LessThan(ulong obj, ulong value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + /// Value must be less than . + public static void LessThan(Optional obj, sbyte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value >= value) throw CreateLessThanException(name, msg, value); } + /// Value must be less than . + public static void LessThan(Optional obj, byte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value >= value) throw CreateLessThanException(name, msg, value); } + /// Value must be less than . + public static void LessThan(Optional obj, short value, string name, string msg = null) { if (obj.IsSpecified && obj.Value >= value) throw CreateLessThanException(name, msg, value); } + /// Value must be less than . + public static void LessThan(Optional obj, ushort value, string name, string msg = null) { if (obj.IsSpecified && obj.Value >= value) throw CreateLessThanException(name, msg, value); } + /// Value must be less than . + public static void LessThan(Optional obj, int value, string name, string msg = null) { if (obj.IsSpecified && obj.Value >= value) throw CreateLessThanException(name, msg, value); } + /// Value must be less than . + public static void LessThan(Optional obj, uint value, string name, string msg = null) { if (obj.IsSpecified && obj.Value >= value) throw CreateLessThanException(name, msg, value); } + /// Value must be less than . + public static void LessThan(Optional obj, long value, string name, string msg = null) { if (obj.IsSpecified && obj.Value >= value) throw CreateLessThanException(name, msg, value); } + /// Value must be less than . + public static void LessThan(Optional obj, ulong value, string name, string msg = null) { if (obj.IsSpecified && obj.Value >= value) throw CreateLessThanException(name, msg, value); } + + private static ArgumentException CreateLessThanException(string name, string msg, T value) + => new ArgumentException(message: msg ?? $"Value must be less than {value}.", paramName: name); + + // Bulk Delete + /// Messages are younger than 2 weeks. + public static void YoungerThanTwoWeeks(ulong[] collection, string name) + { + var minimum = SnowflakeUtils.ToSnowflake(DateTimeOffset.UtcNow.Subtract(TimeSpan.FromDays(14))); + for (var i = 0; i < collection.Length; i++) + { + if (collection[i] == 0) continue; + if (collection[i] <= minimum) + throw new ArgumentOutOfRangeException(name, "Messages must be younger than two weeks old."); + } + } + /// The everyone role cannot be assigned to a user. + public static void NotEveryoneRole(ulong[] roles, ulong guildId, string name) + { + for (var i = 0; i < roles.Length; i++) + { + if (roles[i] == guildId) + throw new ArgumentException(message: "The everyone role cannot be assigned to a user.", paramName: name); + } + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/RoleUtils.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/RoleUtils.cs new file mode 100644 index 0000000..444afe6 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/RoleUtils.cs @@ -0,0 +1,18 @@ +namespace Discord +{ + internal static class RoleUtils + { + public static int Compare(IRole left, IRole right) + { + if (left == null) + return -1; + if (right == null) + return 1; + var result = left.Position.CompareTo(right.Position); + // As per Discord's documentation, a tie is broken by ID + if (result != 0) + return result; + return left.Id.CompareTo(right.Id); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/SnowflakeUtils.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/SnowflakeUtils.cs new file mode 100644 index 0000000..dd8f8ca --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/SnowflakeUtils.cs @@ -0,0 +1,29 @@ +using System; + +namespace Discord +{ + /// + /// Provides a series of helper methods for handling snowflake identifiers. + /// + public static class SnowflakeUtils + { + /// + /// Resolves the time of which the snowflake is generated. + /// + /// The snowflake identifier to resolve. + /// + /// A representing the time for when the object is geenrated. + /// + public static DateTimeOffset FromSnowflake(ulong value) + => DateTimeOffset.FromUnixTimeMilliseconds((long)((value >> 22) + 1420070400000UL)); + /// + /// Generates a pseudo-snowflake identifier with a . + /// + /// The time to be used in the new snowflake. + /// + /// A representing the newly generated snowflake identifier. + /// + public static ulong ToSnowflake(DateTimeOffset value) + => ((ulong)value.ToUnixTimeMilliseconds() - 1420070400000UL) << 22; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/TokenUtils.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/TokenUtils.cs new file mode 100644 index 0000000..c3dd392 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Core/Utils/TokenUtils.cs @@ -0,0 +1,184 @@ +using System; +using System.Globalization; +using System.Text; + +namespace Discord +{ + /// + /// Provides a series of helper methods for handling Discord login tokens. + /// + public static class TokenUtils + { + /// + /// The minimum length of a Bot token. + /// + /// + /// This value was determined by comparing against the examples in the Discord + /// documentation, and pre-existing tokens. + /// + internal const int MinBotTokenLength = 58; + + internal const char Base64Padding = '='; + + /// + /// Pads a base64-encoded string with 0, 1, or 2 '=' characters, + /// if the string is not a valid multiple of 4. + /// Does not ensure that the provided string contains only valid base64 characters. + /// Strings that already contain padding will not have any more padding applied. + /// + /// + /// A string that would require 3 padding characters is considered to be already corrupt. + /// Some older bot tokens may require padding, as the format provided by Discord + /// does not include this padding in the token. + /// + /// The base64 encoded string to pad with characters. + /// A string containing the base64 padding. + /// + /// Thrown if would require an invalid number of padding characters. + /// + /// + /// Thrown if is null, empty, or whitespace. + /// + internal static string PadBase64String(string encodedBase64) + { + if (string.IsNullOrWhiteSpace(encodedBase64)) + throw new ArgumentNullException(paramName: encodedBase64, + message: "The supplied base64-encoded string was null or whitespace."); + + // do not pad if already contains padding characters + if (encodedBase64.IndexOf(Base64Padding) != -1) + return encodedBase64; + + // based from https://stackoverflow.com/a/1228744 + var padding = (4 - (encodedBase64.Length % 4)) % 4; + if (padding == 3) + // can never have 3 characters of padding + throw new FormatException("The provided base64 string is corrupt, as it requires an invalid amount of padding."); + else if (padding == 0) + return encodedBase64; + return encodedBase64.PadRight(encodedBase64.Length + padding, Base64Padding); + } + + /// + /// Decodes a base 64 encoded string into a ulong value. + /// + /// A base 64 encoded string containing a User Id. + /// A ulong containing the decoded value of the string, or null if the value was invalid. + internal static ulong? DecodeBase64UserId(string encoded) + { + if (string.IsNullOrWhiteSpace(encoded)) + return null; + + try + { + // re-add base64 padding if missing + encoded = PadBase64String(encoded); + // decode the base64 string + var bytes = Convert.FromBase64String(encoded); + var idStr = Encoding.UTF8.GetString(bytes); + // try to parse a ulong from the resulting string + if (ulong.TryParse(idStr, NumberStyles.None, CultureInfo.InvariantCulture, out var id)) + return id; + } + catch (DecoderFallbackException) + { + // ignore exception, can be thrown by GetString + } + catch (FormatException) + { + // ignore exception, can be thrown if base64 string is invalid + } + catch (ArgumentException) + { + // ignore exception, can be thrown by BitConverter, or by PadBase64String + } + return null; + } + + /// + /// Checks the validity of a bot token by attempting to decode a ulong userid + /// from the bot token. + /// + /// + /// The bot token to validate. + /// + /// + /// True if the bot token was valid, false if it was not. + /// + internal static bool CheckBotTokenValidity(string message) + { + if (string.IsNullOrWhiteSpace(message)) + return false; + + // split each component of the JWT + var segments = message.Split('.'); + + // ensure that there are three parts + if (segments.Length != 3) + return false; + // return true if the user id could be determined + return DecodeBase64UserId(segments[0]).HasValue; + } + + /// + /// The set of all characters that are not allowed inside of a token. + /// + internal static char[] IllegalTokenCharacters = new char[] + { + ' ', '\t', '\r', '\n' + }; + + /// + /// Checks if the given token contains a whitespace or newline character + /// that would fail to log in. + /// + /// The token to validate. + /// + /// True if the token contains a whitespace or newline character. + /// + internal static bool CheckContainsIllegalCharacters(string token) + => token.IndexOfAny(IllegalTokenCharacters) != -1; + + /// + /// Checks the validity of the supplied token of a specific type. + /// + /// The type of token to validate. + /// The token value to validate. + /// Thrown when the supplied token string is null, empty, or contains only whitespace. + /// Thrown when the supplied or token value is invalid. + public static void ValidateToken(TokenType tokenType, string token) + { + // A Null or WhiteSpace token of any type is invalid. + if (string.IsNullOrWhiteSpace(token)) + throw new ArgumentNullException(paramName: nameof(token), message: "A token cannot be null, empty, or contain only whitespace."); + // ensure that there are no whitespace or newline characters + if (CheckContainsIllegalCharacters(token)) + throw new ArgumentException(message: "The token contains a whitespace or newline character. Ensure that the token has been properly trimmed.", paramName: nameof(token)); + + switch (tokenType) + { + case TokenType.Webhook: + // no validation is performed on Webhook tokens + break; + case TokenType.Bearer: + // no validation is performed on Bearer tokens + break; + case TokenType.Bot: + // bot tokens are assumed to be at least 58 characters in length + // this value was determined by referencing examples in the discord documentation, and by comparing with + // pre-existing tokens + if (token.Length < MinBotTokenLength) + throw new ArgumentException(message: $"A Bot token must be at least {MinBotTokenLength} characters in length. " + + "Ensure that the Bot Token provided is not an OAuth client secret.", paramName: nameof(token)); + // check the validity of the bot token by decoding the ulong userid from the jwt + if (!CheckBotTokenValidity(token)) + throw new ArgumentException(message: "The Bot token was invalid. " + + "Ensure that the Bot Token provided is not an OAuth client secret.", paramName: nameof(token)); + break; + default: + // All unrecognized TokenTypes (including User tokens) are considered to be invalid. + throw new ArgumentException(message: "Unrecognized TokenType.", paramName: nameof(token)); + } + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Application.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Application.cs new file mode 100644 index 0000000..ca4c443 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Application.cs @@ -0,0 +1,24 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class Application + { + [JsonProperty("description")] + public string Description { get; set; } + [JsonProperty("rpc_origins")] + public string[] RPCOrigins { get; set; } + [JsonProperty("name")] + public string Name { 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/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Attachment.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Attachment.cs new file mode 100644 index 0000000..4a651d9 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Attachment.cs @@ -0,0 +1,23 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class Attachment + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("filename")] + public string Filename { get; set; } + [JsonProperty("size")] + public int Size { get; set; } + [JsonProperty("url")] + public string Url { get; set; } + [JsonProperty("proxy_url")] + public string ProxyUrl { get; set; } + [JsonProperty("height")] + public Optional Height { get; set; } + [JsonProperty("width")] + public Optional Width { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/AuditLog.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/AuditLog.cs new file mode 100644 index 0000000..cd8ad14 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/AuditLog.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class AuditLog + { + [JsonProperty("webhooks")] + public Webhook[] Webhooks { get; set; } + + [JsonProperty("users")] + public User[] Users { get; set; } + + [JsonProperty("audit_log_entries")] + public AuditLogEntry[] Entries { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/AuditLogChange.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/AuditLogChange.cs new file mode 100644 index 0000000..44e5850 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/AuditLogChange.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Discord.API +{ + internal class AuditLogChange + { + [JsonProperty("key")] + public string ChangedProperty { get; set; } + + [JsonProperty("new_value")] + public JToken NewValue { get; set; } + + [JsonProperty("old_value")] + public JToken OldValue { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/AuditLogEntry.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/AuditLogEntry.cs new file mode 100644 index 0000000..80d9a9e --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/AuditLogEntry.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class AuditLogEntry + { + [JsonProperty("target_id")] + public ulong? TargetId { get; set; } + [JsonProperty("user_id")] + public ulong UserId { get; set; } + + [JsonProperty("changes")] + public AuditLogChange[] Changes { get; set; } + [JsonProperty("options")] + public AuditLogOptions Options { get; set; } + + [JsonProperty("id")] + public ulong Id { get; set; } + + [JsonProperty("action_type")] + public ActionType Action { get; set; } + + [JsonProperty("reason")] + public string Reason { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/AuditLogOptions.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/AuditLogOptions.cs new file mode 100644 index 0000000..24141d9 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/AuditLogOptions.cs @@ -0,0 +1,27 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class AuditLogOptions + { + //Message delete + [JsonProperty("count")] + public int? MessageDeleteCount { get; set; } + [JsonProperty("channel_id")] + public ulong? MessageDeleteChannelId { get; set; } + + //Prune + [JsonProperty("delete_member_days")] + public int? PruneDeleteMemberDays { get; set; } + [JsonProperty("members_removed")] + public int? PruneMembersRemoved { get; set; } + + //Overwrite Update + [JsonProperty("role_name")] + public string OverwriteRoleName { get; set; } + [JsonProperty("type")] + public PermissionTarget OverwriteType { get; set; } + [JsonProperty("id")] + public ulong? OverwriteTargetId { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Ban.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Ban.cs new file mode 100644 index 0000000..202004f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Ban.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class Ban + { + [JsonProperty("user")] + public User User { get; set; } + [JsonProperty("reason")] + public string Reason { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Channel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Channel.cs new file mode 100644 index 0000000..57a5ce9 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Channel.cs @@ -0,0 +1,53 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; +using System; + +namespace Discord.API +{ + internal class Channel + { + //Shared + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("type")] + public ChannelType Type { get; set; } + [JsonProperty("last_message_id")] + public ulong? LastMessageId { 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; } + [JsonProperty("permission_overwrites")] + public Optional PermissionOverwrites { get; set; } + [JsonProperty("parent_id")] + public ulong? CategoryId { get; set; } + + //TextChannel + [JsonProperty("topic")] + public Optional Topic { get; set; } + [JsonProperty("last_pin_timestamp")] + public Optional LastPinTimestamp { get; set; } + [JsonProperty("nsfw")] + public Optional Nsfw { get; set; } + [JsonProperty("rate_limit_per_user")] + public Optional SlowMode { get; set; } + + //VoiceChannel + [JsonProperty("bitrate")] + public Optional Bitrate { get; set; } + [JsonProperty("user_limit")] + public Optional UserLimit { get; set; } + + //PrivateChannel + [JsonProperty("recipients")] + public Optional Recipients { get; set; } + + //GroupChannel + [JsonProperty("icon")] + public Optional Icon { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Connection.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Connection.cs new file mode 100644 index 0000000..ad0a76a --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Connection.cs @@ -0,0 +1,21 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Discord.API +{ + internal class Connection + { + [JsonProperty("id")] + public string Id { get; set; } + [JsonProperty("type")] + public string Type { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("revoked")] + public bool Revoked { get; set; } + + [JsonProperty("integrations")] + public IReadOnlyCollection Integrations { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Embed.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Embed.cs new file mode 100644 index 0000000..fbf20d9 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Embed.cs @@ -0,0 +1,37 @@ +#pragma warning disable CS1591 +using System; +using Newtonsoft.Json; +using Discord.Net.Converters; + +namespace Discord.API +{ + internal class Embed + { + [JsonProperty("title")] + public string Title { get; set; } + [JsonProperty("description")] + public string Description { get; set; } + [JsonProperty("url")] + public string Url { get; set; } + [JsonProperty("color")] + public uint? Color { get; set; } + [JsonProperty("type"), JsonConverter(typeof(EmbedTypeConverter))] + public EmbedType Type { get; set; } + [JsonProperty("timestamp")] + public DateTimeOffset? Timestamp { get; set; } + [JsonProperty("author")] + public Optional Author { get; set; } + [JsonProperty("footer")] + public Optional Footer { get; set; } + [JsonProperty("video")] + public Optional Video { get; set; } + [JsonProperty("thumbnail")] + public Optional Thumbnail { get; set; } + [JsonProperty("image")] + public Optional Image { get; set; } + [JsonProperty("provider")] + public Optional Provider { get; set; } + [JsonProperty("fields")] + public Optional Fields { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/EmbedAuthor.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/EmbedAuthor.cs new file mode 100644 index 0000000..d7f3ae6 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/EmbedAuthor.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class EmbedAuthor + { + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("url")] + public string Url { get; set; } + [JsonProperty("icon_url")] + public string IconUrl { get; set; } + [JsonProperty("proxy_icon_url")] + public string ProxyIconUrl { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/EmbedField.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/EmbedField.cs new file mode 100644 index 0000000..6ce810f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/EmbedField.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class EmbedField + { + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("value")] + public string Value { get; set; } + [JsonProperty("inline")] + public bool Inline { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/EmbedFooter.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/EmbedFooter.cs new file mode 100644 index 0000000..cd08e7e --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/EmbedFooter.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class EmbedFooter + { + [JsonProperty("text")] + public string Text { get; set; } + [JsonProperty("icon_url")] + public string IconUrl { get; set; } + [JsonProperty("proxy_icon_url")] + public string ProxyIconUrl { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/EmbedImage.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/EmbedImage.cs new file mode 100644 index 0000000..e650d99 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/EmbedImage.cs @@ -0,0 +1,17 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class EmbedImage + { + [JsonProperty("url")] + public string Url { get; set; } + [JsonProperty("proxy_url")] + public string ProxyUrl { get; set; } + [JsonProperty("height")] + public Optional Height { get; set; } + [JsonProperty("width")] + public Optional Width { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/EmbedProvider.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/EmbedProvider.cs new file mode 100644 index 0000000..e012614 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/EmbedProvider.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class EmbedProvider + { + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("url")] + public string Url { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/EmbedThumbnail.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/EmbedThumbnail.cs new file mode 100644 index 0000000..9c87ca4 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/EmbedThumbnail.cs @@ -0,0 +1,17 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class EmbedThumbnail + { + [JsonProperty("url")] + public string Url { get; set; } + [JsonProperty("proxy_url")] + public string ProxyUrl { get; set; } + [JsonProperty("height")] + public Optional Height { get; set; } + [JsonProperty("width")] + public Optional Width { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/EmbedVideo.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/EmbedVideo.cs new file mode 100644 index 0000000..3a034d2 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/EmbedVideo.cs @@ -0,0 +1,15 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class EmbedVideo + { + [JsonProperty("url")] + public string Url { get; set; } + [JsonProperty("height")] + public Optional Height { get; set; } + [JsonProperty("width")] + public Optional Width { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Emoji.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Emoji.cs new file mode 100644 index 0000000..945cc6d --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Emoji.cs @@ -0,0 +1,23 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class Emoji + { + [JsonProperty("id")] + public ulong? Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("animated")] + public bool? Animated { get; set; } + [JsonProperty("roles")] + public ulong[] Roles { get; set; } + [JsonProperty("require_colons")] + public bool RequireColons { get; set; } + [JsonProperty("managed")] + public bool Managed { get; set; } + [JsonProperty("user")] + public Optional User { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Game.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Game.cs new file mode 100644 index 0000000..d3a6186 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Game.cs @@ -0,0 +1,51 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using System.Runtime.Serialization; + +namespace Discord.API +{ + internal class Game + { + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("url")] + public Optional StreamUrl { get; set; } + [JsonProperty("type")] + public Optional Type { get; set; } + [JsonProperty("details")] + public Optional Details { get; set; } + [JsonProperty("state")] + public Optional State { get; set; } + [JsonProperty("application_id")] + public Optional ApplicationId { get; set; } + [JsonProperty("assets")] + public Optional Assets { get; set; } + [JsonProperty("party")] + public Optional Party { get; set; } + [JsonProperty("secrets")] + public Optional Secrets { get; set; } + [JsonProperty("timestamps")] + public Optional Timestamps { get; set; } + [JsonProperty("instance")] + public Optional Instance { get; set; } + [JsonProperty("sync_id")] + public Optional SyncId { get; set; } + [JsonProperty("session_id")] + public Optional SessionId { get; set; } + [JsonProperty("Flags")] + public Optional Flags { get; set; } + [JsonProperty("id")] + public Optional Id { get; set; } + [JsonProperty("emoji")] + public Optional Emoji { get; set; } + [JsonProperty("created_at")] + public Optional CreatedAt { get; set; } + + [OnError] + internal void OnError(StreamingContext context, ErrorContext errorContext) + { + errorContext.Handled = true; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/GameAssets.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/GameAssets.cs new file mode 100644 index 0000000..94a5407 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/GameAssets.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class GameAssets + { + [JsonProperty("small_text")] + public Optional SmallText { get; set; } + [JsonProperty("small_image")] + public Optional SmallImage { get; set; } + [JsonProperty("large_text")] + public Optional LargeText { get; set; } + [JsonProperty("large_image")] + public Optional LargeImage { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/GameParty.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/GameParty.cs new file mode 100644 index 0000000..4f8ce26 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/GameParty.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class GameParty + { + [JsonProperty("id")] + public string Id { get; set; } + [JsonProperty("size")] + public long[] Size { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/GameSecrets.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/GameSecrets.cs new file mode 100644 index 0000000..e70b48f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/GameSecrets.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class GameSecrets + { + [JsonProperty("match")] + public string Match { get; set; } + [JsonProperty("join")] + public string Join { get; set; } + [JsonProperty("spectate")] + public string Spectate { get; set; } + } +} \ No newline at end of file diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/GameTimestamps.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/GameTimestamps.cs new file mode 100644 index 0000000..5c6f10b --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/GameTimestamps.cs @@ -0,0 +1,15 @@ +using System; +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class GameTimestamps + { + [JsonProperty("start")] + [UnixTimestamp] + public Optional Start { get; set; } + [JsonProperty("end")] + [UnixTimestamp] + public Optional End { get; set; } + } +} \ No newline at end of file diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Guild.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Guild.cs new file mode 100644 index 0000000..56bd841 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Guild.cs @@ -0,0 +1,64 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class Guild + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("icon")] + public string Icon { get; set; } + [JsonProperty("splash")] + public string Splash { get; set; } + [JsonProperty("owner_id")] + public ulong OwnerId { get; set; } + [JsonProperty("region")] + public string Region { get; set; } + [JsonProperty("afk_channel_id")] + public ulong? AFKChannelId { get; set; } + [JsonProperty("afk_timeout")] + public int AFKTimeout { get; set; } + [JsonProperty("embed_enabled")] + public bool EmbedEnabled { get; set; } + [JsonProperty("embed_channel_id")] + public ulong? EmbedChannelId { get; set; } + [JsonProperty("verification_level")] + public VerificationLevel VerificationLevel { get; set; } + [JsonProperty("default_message_notifications")] + public DefaultMessageNotifications DefaultMessageNotifications { get; set; } + [JsonProperty("explicit_content_filter")] + public ExplicitContentFilterLevel ExplicitContentFilter { get; set; } + [JsonProperty("voice_states")] + public VoiceState[] VoiceStates { get; set; } + [JsonProperty("roles")] + public Role[] Roles { get; set; } + [JsonProperty("emojis")] + public Emoji[] Emojis { get; set; } + [JsonProperty("features")] + public string[] Features { get; set; } + [JsonProperty("mfa_level")] + public MfaLevel MfaLevel { get; set; } + [JsonProperty("application_id")] + public ulong? ApplicationId { get; set; } + [JsonProperty("system_channel_id")] + public ulong? SystemChannelId { get; set; } + [JsonProperty("premium_tier")] + public PremiumTier PremiumTier { get; set; } + [JsonProperty("vanity_url_code")] + public string VanityURLCode { get; set; } + [JsonProperty("banner")] + public string Banner { get; set; } + [JsonProperty("description")] + public string Description { get; set; } + // this value is inverted, flags set will turn OFF features + [JsonProperty("system_channel_flags")] + public SystemChannelMessageDeny SystemChannelFlags { get; set; } + [JsonProperty("premium_subscription_count")] + public int? PremiumSubscriptionCount { get; set; } + [JsonProperty("preferred_locale")] + public string PreferredLocale { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/GuildEmbed.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/GuildEmbed.cs new file mode 100644 index 0000000..ff8b8e1 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/GuildEmbed.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class GuildEmbed + { + [JsonProperty("enabled")] + public bool Enabled { get; set; } + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/GuildMember.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/GuildMember.cs new file mode 100644 index 0000000..940eb92 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/GuildMember.cs @@ -0,0 +1,24 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; +using System; + +namespace Discord.API +{ + internal class GuildMember + { + [JsonProperty("user")] + public User User { get; set; } + [JsonProperty("nick")] + public Optional Nick { get; set; } + [JsonProperty("roles")] + public Optional Roles { get; set; } + [JsonProperty("joined_at")] + public Optional JoinedAt { get; set; } + [JsonProperty("deaf")] + public Optional Deaf { get; set; } + [JsonProperty("mute")] + public Optional Mute { get; set; } + [JsonProperty("premium_since")] + public Optional PremiumSince { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Integration.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Integration.cs new file mode 100644 index 0000000..8213599 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Integration.cs @@ -0,0 +1,32 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; +using System; + +namespace Discord.API +{ + internal class Integration + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("type")] + public string Type { get; set; } + [JsonProperty("enabled")] + public bool Enabled { get; set; } + [JsonProperty("syncing")] + public bool Syncing { get; set; } + [JsonProperty("role_id")] + public ulong RoleId { get; set; } + [JsonProperty("expire_behavior")] + public ulong ExpireBehavior { get; set; } + [JsonProperty("expire_grace_period")] + public ulong ExpireGracePeriod { get; set; } + [JsonProperty("user")] + public User User { get; set; } + [JsonProperty("account")] + public IntegrationAccount Account { get; set; } + [JsonProperty("synced_at")] + public DateTimeOffset SyncedAt { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/IntegrationAccount.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/IntegrationAccount.cs new file mode 100644 index 0000000..22831e7 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/IntegrationAccount.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class IntegrationAccount + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Invite.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Invite.cs new file mode 100644 index 0000000..649bc37 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Invite.cs @@ -0,0 +1,19 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class Invite + { + [JsonProperty("code")] + public string Code { get; set; } + [JsonProperty("guild")] + public Optional Guild { get; set; } + [JsonProperty("channel")] + public InviteChannel Channel { get; set; } + [JsonProperty("approximate_presence_count")] + public Optional PresenceCount { get; set; } + [JsonProperty("approximate_member_count")] + public Optional MemberCount { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/InviteChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/InviteChannel.cs new file mode 100644 index 0000000..f8f2a34 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/InviteChannel.cs @@ -0,0 +1,15 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class InviteChannel + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("type")] + public int Type { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/InviteGuild.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/InviteGuild.cs new file mode 100644 index 0000000..3d6d7cd --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/InviteGuild.cs @@ -0,0 +1,15 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class InviteGuild + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("splash_hash")] + public string SplashHash { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/InviteMetadata.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/InviteMetadata.cs new file mode 100644 index 0000000..ca019b7 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/InviteMetadata.cs @@ -0,0 +1,24 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; +using System; + +namespace Discord.API +{ + internal class InviteMetadata : Invite + { + [JsonProperty("inviter")] + public User Inviter { get; set; } + [JsonProperty("uses")] + public Optional Uses { get; set; } + [JsonProperty("max_uses")] + public Optional MaxUses { get; set; } + [JsonProperty("max_age")] + public Optional MaxAge { get; set; } + [JsonProperty("temporary")] + public bool Temporary { get; set; } + [JsonProperty("created_at")] + public Optional CreatedAt { get; set; } + [JsonProperty("revoked")] + public bool Revoked { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/InviteVanity.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/InviteVanity.cs new file mode 100644 index 0000000..d397926 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/InviteVanity.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class InviteVanity + { + [JsonProperty("code")] + public string Code { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Message.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Message.cs new file mode 100644 index 0000000..f200356 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Message.cs @@ -0,0 +1,58 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; +using System; + +namespace Discord.API +{ + internal class Message + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("type")] + public MessageType Type { get; set; } + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + // ALWAYS sent on WebSocket messages + [JsonProperty("guild_id")] + public Optional GuildId { get; set; } + [JsonProperty("webhook_id")] + public Optional WebhookId { get; set; } + [JsonProperty("author")] + public Optional Author { get; set; } + // ALWAYS sent on WebSocket messages + [JsonProperty("member")] + public Optional Member { get; set; } + [JsonProperty("content")] + public Optional Content { get; set; } + [JsonProperty("timestamp")] + public Optional Timestamp { get; set; } + [JsonProperty("edited_timestamp")] + public Optional EditedTimestamp { get; set; } + [JsonProperty("tts")] + public Optional IsTextToSpeech { get; set; } + [JsonProperty("mention_everyone")] + public Optional MentionEveryone { get; set; } + [JsonProperty("mentions")] + public Optional[]> UserMentions { get; set; } + [JsonProperty("mention_roles")] + public Optional RoleMentions { get; set; } + [JsonProperty("attachments")] + public Optional Attachments { get; set; } + [JsonProperty("embeds")] + public Optional Embeds { get; set; } + [JsonProperty("pinned")] + public Optional Pinned { get; set; } + [JsonProperty("reactions")] + public Optional Reactions { get; set; } + // sent with Rich Presence-related chat embeds + [JsonProperty("activity")] + public Optional Activity { get; set; } + // sent with Rich Presence-related chat embeds + [JsonProperty("application")] + public Optional Application { get; set; } + [JsonProperty("message_reference")] + public Optional Reference { get; set; } + [JsonProperty("flags")] + public Optional Flags { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/MessageActivity.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/MessageActivity.cs new file mode 100644 index 0000000..701f6fc --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/MessageActivity.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + public class MessageActivity + { + [JsonProperty("type")] + public Optional Type { get; set; } + [JsonProperty("party_id")] + public Optional PartyId { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/MessageApplication.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/MessageApplication.cs new file mode 100644 index 0000000..7302185 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/MessageApplication.cs @@ -0,0 +1,38 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + public class MessageApplication + { + /// + /// Gets the snowflake ID of the application. + /// + [JsonProperty("id")] + public ulong Id { get; set; } + /// + /// Gets the ID of the embed's image asset. + /// + [JsonProperty("cover_image")] + public string CoverImage { get; set; } + /// + /// Gets the application's description. + /// + [JsonProperty("description")] + public string Description { get; set; } + /// + /// Gets the ID of the application's icon. + /// + [JsonProperty("icon")] + public string Icon { get; set; } + /// + /// Gets the name of the application. + /// + [JsonProperty("name")] + public string Name { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/MessageFlags.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/MessageFlags.cs new file mode 100644 index 0000000..ebe4e80 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/MessageFlags.cs @@ -0,0 +1,10 @@ +using System; + +namespace Discord.API +{ + [Flags] + internal enum MessageFlags : byte // probably safe to constrain this to 8 values, if not, it's internal so who cares + { + Suppressed = 0x04, + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/MessageReference.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/MessageReference.cs new file mode 100644 index 0000000..8c0f8fe --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/MessageReference.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class MessageReference + { + [JsonProperty("message_id")] + public Optional MessageId { get; set; } + + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + + [JsonProperty("guild_id")] + public Optional GuildId { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Overwrite.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Overwrite.cs new file mode 100644 index 0000000..1f3548a --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Overwrite.cs @@ -0,0 +1,17 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class Overwrite + { + [JsonProperty("id")] + public ulong TargetId { get; set; } + [JsonProperty("type")] + public PermissionTarget TargetType { get; set; } + [JsonProperty("deny"), Int53] + public ulong Deny { get; set; } + [JsonProperty("allow"), Int53] + public ulong Allow { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Presence.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Presence.cs new file mode 100644 index 0000000..22526e8 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Presence.cs @@ -0,0 +1,30 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Discord.API +{ + internal class Presence + { + [JsonProperty("user")] + public User User { get; set; } + [JsonProperty("guild_id")] + public Optional GuildId { get; set; } + [JsonProperty("status")] + public UserStatus Status { get; set; } + [JsonProperty("game")] + public Game Game { get; set; } + + [JsonProperty("roles")] + public Optional Roles { get; set; } + [JsonProperty("nick")] + public Optional Nick { get; set; } + // This property is a Dictionary where each key is the ClientType + // and the values are the current client status. + // The client status values are all the same. + // Example: + // "client_status": { "desktop": "dnd", "mobile": "dnd" } + [JsonProperty("client_status")] + public Optional> ClientStatus { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Reaction.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Reaction.cs new file mode 100644 index 0000000..4d368ab --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Reaction.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class Reaction + { + [JsonProperty("count")] + public int Count { get; set; } + [JsonProperty("me")] + public bool Me { get; set; } + [JsonProperty("emoji")] + public Emoji Emoji { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/ReadState.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/ReadState.cs new file mode 100644 index 0000000..6ea6e4b --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/ReadState.cs @@ -0,0 +1,15 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class ReadState + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("mention_count")] + public int MentionCount { get; set; } + [JsonProperty("last_message_id")] + public Optional LastMessageId { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Relationship.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Relationship.cs new file mode 100644 index 0000000..ecbb96f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Relationship.cs @@ -0,0 +1,15 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class Relationship + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("user")] + public User User { get; set; } + [JsonProperty("type")] + public RelationshipType Type { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/RelationshipType.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/RelationshipType.cs new file mode 100644 index 0000000..0ed99f3 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/RelationshipType.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 +namespace Discord.API +{ + internal enum RelationshipType + { + Friend = 1, + Blocked = 2, + IncomingPending = 3, + OutgoingPending = 4 + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Role.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Role.cs new file mode 100644 index 0000000..856a869 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Role.cs @@ -0,0 +1,25 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class Role + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("color")] + 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] + public ulong Permissions { get; set; } + [JsonProperty("managed")] + public bool Managed { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/User.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/User.cs new file mode 100644 index 0000000..2eff375 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/User.cs @@ -0,0 +1,33 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class User + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("username")] + public Optional Username { get; set; } + [JsonProperty("discriminator")] + public Optional Discriminator { get; set; } + [JsonProperty("bot")] + public Optional Bot { get; set; } + [JsonProperty("avatar")] + public Optional Avatar { get; set; } + + //CurrentUser + [JsonProperty("verified")] + public Optional Verified { get; set; } + [JsonProperty("email")] + public Optional Email { get; set; } + [JsonProperty("mfa_enabled")] + public Optional MfaEnabled { get; set; } + [JsonProperty("flags")] + public Optional Flags { get; set; } + [JsonProperty("premium_type")] + public Optional PremiumType { get; set; } + [JsonProperty("locale")] + public Optional Locale { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/UserGuild.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/UserGuild.cs new file mode 100644 index 0000000..f4f7638 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/UserGuild.cs @@ -0,0 +1,19 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class UserGuild + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("icon")] + public string Icon { get; set; } + [JsonProperty("owner")] + public bool Owner { get; set; } + [JsonProperty("permissions"), Int53] + public ulong Permissions { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/VoiceRegion.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/VoiceRegion.cs new file mode 100644 index 0000000..606af07 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/VoiceRegion.cs @@ -0,0 +1,21 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class VoiceRegion + { + [JsonProperty("id")] + public string Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("vip")] + public bool IsVip { get; set; } + [JsonProperty("optimal")] + public bool IsOptimal { get; set; } + [JsonProperty("deprecated")] + public bool IsDeprecated { get; set; } + [JsonProperty("custom")] + public bool IsCustom { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/VoiceState.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/VoiceState.cs new file mode 100644 index 0000000..c7a571e --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/VoiceState.cs @@ -0,0 +1,32 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class VoiceState + { + [JsonProperty("guild_id")] + public ulong? GuildId { get; set; } + [JsonProperty("channel_id")] + public ulong? ChannelId { get; set; } + [JsonProperty("user_id")] + public ulong UserId { get; set; } + // ALWAYS sent over WebSocket, never on REST + [JsonProperty("member")] + public Optional Member { get; set; } + [JsonProperty("session_id")] + public string SessionId { get; set; } + [JsonProperty("deaf")] + public bool Deaf { get; set; } + [JsonProperty("mute")] + public bool Mute { get; set; } + [JsonProperty("self_deaf")] + public bool SelfDeaf { get; set; } + [JsonProperty("self_mute")] + public bool SelfMute { get; set; } + [JsonProperty("suppress")] + public bool Suppress { get; set; } + [JsonProperty("self_stream")] + public bool SelfStream { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Webhook.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Webhook.cs new file mode 100644 index 0000000..cbd5fda --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Common/Webhook.cs @@ -0,0 +1,25 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class Webhook + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + [JsonProperty("token")] + public string Token { get; set; } + + [JsonProperty("name")] + public Optional Name { get; set; } + [JsonProperty("avatar")] + public Optional Avatar { get; set; } + [JsonProperty("guild_id")] + public Optional GuildId { get; set; } + + [JsonProperty("user")] + public Optional Creator { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/EntityOrId.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/EntityOrId.cs new file mode 100644 index 0000000..9bcda26 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/EntityOrId.cs @@ -0,0 +1,19 @@ +namespace Discord.API +{ + internal struct EntityOrId + { + public ulong Id { get; } + public T Object { get; } + + public EntityOrId(ulong id) + { + Id = id; + Object = default(T); + } + public EntityOrId(T obj) + { + Id = 0; + Object = obj; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Image.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Image.cs new file mode 100644 index 0000000..b2357a0 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Image.cs @@ -0,0 +1,21 @@ +using System.IO; + +namespace Discord.API +{ + internal struct Image + { + public Stream Stream { get; } + public string Hash { get; } + + public Image(Stream stream) + { + Stream = stream; + Hash = null; + } + public Image(string hash) + { + Stream = null; + Hash = hash; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Int53Attribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Int53Attribute.cs new file mode 100644 index 0000000..70ef2f1 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Int53Attribute.cs @@ -0,0 +1,8 @@ +#pragma warning disable CS1591 +using System; + +namespace Discord.API +{ + [AttributeUsage(AttributeTargets.Property)] + internal class Int53Attribute : Attribute { } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Net/MultipartFile.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Net/MultipartFile.cs new file mode 100644 index 0000000..604852e --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Net/MultipartFile.cs @@ -0,0 +1,16 @@ +using System.IO; + +namespace Discord.Net.Rest +{ + internal struct MultipartFile + { + public Stream Stream { get; } + public string Filename { get; } + + public MultipartFile(Stream stream, string filename) + { + Stream = stream; + Filename = filename; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/AddGuildMemberParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/AddGuildMemberParams.cs new file mode 100644 index 0000000..ef6229e --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/AddGuildMemberParams.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class AddGuildMemberParams + { + [JsonProperty("access_token")] + public string AccessToken { get; set; } + [JsonProperty("nick")] + public Optional Nickname { get; set; } + [JsonProperty("roles")] + public Optional RoleIds { get; set; } + [JsonProperty("mute")] + public Optional IsMuted { get; set; } + [JsonProperty("deaf")] + public Optional IsDeafened { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/CreateChannelInviteParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/CreateChannelInviteParams.cs new file mode 100644 index 0000000..db79bc3 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/CreateChannelInviteParams.cs @@ -0,0 +1,18 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal 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; } + [JsonProperty("unique")] + public Optional IsUnique { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/CreateDMChannelParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/CreateDMChannelParams.cs new file mode 100644 index 0000000..f32796e --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/CreateDMChannelParams.cs @@ -0,0 +1,17 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class CreateDMChannelParams + { + [JsonProperty("recipient_id")] + public ulong RecipientId { get; } + + public CreateDMChannelParams(ulong recipientId) + { + RecipientId = recipientId; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/CreateGuildBanParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/CreateGuildBanParams.cs new file mode 100644 index 0000000..f0432e5 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/CreateGuildBanParams.cs @@ -0,0 +1,9 @@ +#pragma warning disable CS1591 +namespace Discord.API.Rest +{ + internal class CreateGuildBanParams + { + public Optional DeleteMessageDays { get; set; } + public string Reason { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs new file mode 100644 index 0000000..a102bd3 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs @@ -0,0 +1,36 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class CreateGuildChannelParams + { + [JsonProperty("name")] + public string Name { get; } + [JsonProperty("type")] + public ChannelType Type { get; } + [JsonProperty("parent_id")] + public Optional CategoryId { get; set; } + [JsonProperty("position")] + public Optional Position { get; set; } + + //Text channels + [JsonProperty("topic")] + public Optional Topic { get; set; } + [JsonProperty("nsfw")] + public Optional IsNsfw { get; set; } + + //Voice channels + [JsonProperty("bitrate")] + public Optional Bitrate { get; set; } + [JsonProperty("user_limit")] + public Optional UserLimit { get; set; } + + public CreateGuildChannelParams(string name, ChannelType type) + { + Name = name; + Type = type; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/CreateGuildEmoteParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/CreateGuildEmoteParams.cs new file mode 100644 index 0000000..3081998 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/CreateGuildEmoteParams.cs @@ -0,0 +1,16 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class CreateGuildEmoteParams + { + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("image")] + public Image Image { get; set; } + [JsonProperty("roles")] + public Optional RoleIds { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/CreateGuildIntegrationParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/CreateGuildIntegrationParams.cs new file mode 100644 index 0000000..1053a0e --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/CreateGuildIntegrationParams.cs @@ -0,0 +1,20 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class CreateGuildIntegrationParams + { + [JsonProperty("id")] + public ulong Id { get; } + [JsonProperty("type")] + public string Type { get; } + + public CreateGuildIntegrationParams(ulong id, string type) + { + Id = id; + Type = type; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/CreateGuildParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/CreateGuildParams.cs new file mode 100644 index 0000000..cda6cae --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/CreateGuildParams.cs @@ -0,0 +1,23 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class CreateGuildParams + { + [JsonProperty("name")] + public string Name { get; } + [JsonProperty("region")] + public string RegionId { get; } + + [JsonProperty("icon")] + public Optional Icon { get; set; } + + public CreateGuildParams(string name, string regionId) + { + Name = name; + RegionId = regionId; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/CreateMessageParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/CreateMessageParams.cs new file mode 100644 index 0000000..d77bff8 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/CreateMessageParams.cs @@ -0,0 +1,24 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class CreateMessageParams + { + [JsonProperty("content")] + public string Content { get; } + + [JsonProperty("nonce")] + public Optional Nonce { get; set; } + [JsonProperty("tts")] + public Optional IsTTS { get; set; } + [JsonProperty("embed")] + public Optional Embed { get; set; } + + public CreateMessageParams(string content) + { + Content = content; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs new file mode 100644 index 0000000..970a302 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs @@ -0,0 +1,28 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class CreateWebhookMessageParams + { + [JsonProperty("content")] + public string Content { get; } + + [JsonProperty("nonce")] + public Optional Nonce { get; set; } + [JsonProperty("tts")] + public Optional IsTTS { get; set; } + [JsonProperty("embeds")] + public Optional Embeds { get; set; } + [JsonProperty("username")] + public Optional Username { get; set; } + [JsonProperty("avatar_url")] + public Optional AvatarUrl { get; set; } + + public CreateWebhookMessageParams(string content) + { + Content = content; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/CreateWebhookParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/CreateWebhookParams.cs new file mode 100644 index 0000000..0d1059f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/CreateWebhookParams.cs @@ -0,0 +1,14 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class CreateWebhookParams + { + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("avatar")] + public Optional Avatar { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/DeleteMessagesParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/DeleteMessagesParams.cs new file mode 100644 index 0000000..ca9d8c2 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/DeleteMessagesParams.cs @@ -0,0 +1,17 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class DeleteMessagesParams + { + [JsonProperty("messages")] + public ulong[] MessageIds { get; } + + public DeleteMessagesParams(ulong[] messageIds) + { + MessageIds = messageIds; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/GetAuditLogsParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/GetAuditLogsParams.cs new file mode 100644 index 0000000..f136fa7 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/GetAuditLogsParams.cs @@ -0,0 +1,10 @@ +namespace Discord.API.Rest +{ + class GetAuditLogsParams + { + public Optional Limit { get; set; } + public Optional BeforeEntryId { get; set; } + public Optional UserId { get; set; } + public Optional ActionType { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/GetBotGatewayResponse.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/GetBotGatewayResponse.cs new file mode 100644 index 0000000..111fcf3 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/GetBotGatewayResponse.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + internal class GetBotGatewayResponse + { + [JsonProperty("url")] + public string Url { get; set; } + [JsonProperty("shards")] + public int Shards { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/GetChannelMessagesParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/GetChannelMessagesParams.cs new file mode 100644 index 0000000..ea53276 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/GetChannelMessagesParams.cs @@ -0,0 +1,10 @@ +#pragma warning disable CS1591 +namespace Discord.API.Rest +{ + internal class GetChannelMessagesParams + { + public Optional Limit { get; set; } + public Optional RelativeDirection { get; set; } + public Optional RelativeMessageId { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/GetGatewayResponse.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/GetGatewayResponse.cs new file mode 100644 index 0000000..ce36301 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/GetGatewayResponse.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + internal class GetGatewayResponse + { + [JsonProperty("url")] + public string Url { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/GetGuildMembersParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/GetGuildMembersParams.cs new file mode 100644 index 0000000..66023cb --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/GetGuildMembersParams.cs @@ -0,0 +1,9 @@ +#pragma warning disable CS1591 +namespace Discord.API.Rest +{ + internal class GetGuildMembersParams + { + public Optional Limit { get; set; } + public Optional AfterUserId { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/GetGuildPruneCountResponse.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/GetGuildPruneCountResponse.cs new file mode 100644 index 0000000..4af85ac --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/GetGuildPruneCountResponse.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + internal class GetGuildPruneCountResponse + { + [JsonProperty("pruned")] + public int Pruned { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/GetGuildSummariesParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/GetGuildSummariesParams.cs new file mode 100644 index 0000000..f770ef3 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/GetGuildSummariesParams.cs @@ -0,0 +1,9 @@ +#pragma warning disable CS1591 +namespace Discord.API.Rest +{ + internal class GetGuildSummariesParams + { + public Optional Limit { get; set; } + public Optional AfterGuildId { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/GetReactionUsersParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/GetReactionUsersParams.cs new file mode 100644 index 0000000..a0967bb --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/GetReactionUsersParams.cs @@ -0,0 +1,8 @@ +namespace Discord.API.Rest +{ + internal class GetReactionUsersParams + { + public Optional Limit { get; set; } + public Optional AfterUserId { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/GuildPruneParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/GuildPruneParams.cs new file mode 100644 index 0000000..6a98d37 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/GuildPruneParams.cs @@ -0,0 +1,17 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class GuildPruneParams + { + [JsonProperty("days")] + public int Days { get; } + + public GuildPruneParams(int days) + { + Days = days; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyChannelPermissionsParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyChannelPermissionsParams.cs new file mode 100644 index 0000000..0fe5f7e --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyChannelPermissionsParams.cs @@ -0,0 +1,23 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class ModifyChannelPermissionsParams + { + [JsonProperty("type")] + public string Type { get; } + [JsonProperty("allow")] + public ulong Allow { get; } + [JsonProperty("deny")] + public ulong Deny { get; } + + public ModifyChannelPermissionsParams(string type, ulong allow, ulong deny) + { + Type = type; + Allow = allow; + Deny = deny; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyCurrentUserNickParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyCurrentUserNickParams.cs new file mode 100644 index 0000000..ba44e34 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyCurrentUserNickParams.cs @@ -0,0 +1,17 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class ModifyCurrentUserNickParams + { + [JsonProperty("nick")] + public string Nickname { get; } + + public ModifyCurrentUserNickParams(string nickname) + { + Nickname = nickname; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyCurrentUserParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyCurrentUserParams.cs new file mode 100644 index 0000000..7ba27c3 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyCurrentUserParams.cs @@ -0,0 +1,14 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class ModifyCurrentUserParams + { + [JsonProperty("username")] + public Optional Username { get; set; } + [JsonProperty("avatar")] + public Optional Avatar { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs new file mode 100644 index 0000000..e5e8a46 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs @@ -0,0 +1,18 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class ModifyGuildChannelParams + { + [JsonProperty("name")] + public Optional Name { get; set; } + [JsonProperty("position")] + public Optional Position { get; set; } + [JsonProperty("parent_id")] + public Optional CategoryId { get; set; } + [JsonProperty("permission_overwrites")] + public Optional Overwrites { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyGuildChannelsParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyGuildChannelsParams.cs new file mode 100644 index 0000000..f97fbda --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyGuildChannelsParams.cs @@ -0,0 +1,20 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class ModifyGuildChannelsParams + { + [JsonProperty("id")] + public ulong Id { get; } + [JsonProperty("position")] + public int Position { get; } + + public ModifyGuildChannelsParams(ulong id, int position) + { + Id = id; + Position = position; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyGuildEmbedParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyGuildEmbedParams.cs new file mode 100644 index 0000000..487744c --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyGuildEmbedParams.cs @@ -0,0 +1,14 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class ModifyGuildEmbedParams + { + [JsonProperty("enabled")] + public Optional Enabled { get; set; } + [JsonProperty("channel")] + public Optional ChannelId { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyGuildEmoteParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyGuildEmoteParams.cs new file mode 100644 index 0000000..a2295dd --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyGuildEmoteParams.cs @@ -0,0 +1,14 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class ModifyGuildEmoteParams + { + [JsonProperty("name")] + public Optional Name { get; set; } + [JsonProperty("roles")] + public Optional RoleIds { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyGuildIntegrationParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyGuildIntegrationParams.cs new file mode 100644 index 0000000..0a1b4f9 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyGuildIntegrationParams.cs @@ -0,0 +1,16 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal 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/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyGuildMemberParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyGuildMemberParams.cs new file mode 100644 index 0000000..a381d6f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyGuildMemberParams.cs @@ -0,0 +1,20 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal 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/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs new file mode 100644 index 0000000..cfb107b --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs @@ -0,0 +1,40 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal 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("system_channel_id")] + public Optional SystemChannelId { get; set; } + [JsonProperty("icon")] + public Optional Icon { get; set; } + [JsonProperty("banner")] + public Optional Banner { 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; } + [JsonProperty("explicit_content_filter")] + public Optional ExplicitContentFilter { get; set; } + [JsonProperty("system_channel_flags")] + public Optional SystemChannelFlags { get; set; } + [JsonProperty("preferred_locale")] + public string PreferredLocale { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyGuildRoleParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyGuildRoleParams.cs new file mode 100644 index 0000000..287e1ca --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyGuildRoleParams.cs @@ -0,0 +1,20 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class ModifyGuildRoleParams + { + [JsonProperty("name")] + public Optional Name { get; set; } + [JsonProperty("permissions")] + public Optional Permissions { get; set; } + [JsonProperty("color")] + public Optional Color { get; set; } + [JsonProperty("hoist")] + public Optional Hoist { get; set; } + [JsonProperty("mentionable")] + public Optional Mentionable { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyGuildRolesParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyGuildRolesParams.cs new file mode 100644 index 0000000..0e816a2 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyGuildRolesParams.cs @@ -0,0 +1,20 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class ModifyGuildRolesParams : ModifyGuildRoleParams + { + [JsonProperty("id")] + public ulong Id { get; } + [JsonProperty("position")] + public int Position { get; } + + public ModifyGuildRolesParams(ulong id, int position) + { + Id = id; + Position = position; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyMessageParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyMessageParams.cs new file mode 100644 index 0000000..fdff4de --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyMessageParams.cs @@ -0,0 +1,14 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class ModifyMessageParams + { + [JsonProperty("content")] + public Optional Content { get; set; } + [JsonProperty("embed")] + public Optional Embed { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyTextChannelParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyTextChannelParams.cs new file mode 100644 index 0000000..94f149f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyTextChannelParams.cs @@ -0,0 +1,16 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class ModifyTextChannelParams : ModifyGuildChannelParams + { + [JsonProperty("topic")] + public Optional Topic { get; set; } + [JsonProperty("nsfw")] + public Optional IsNsfw { get; set; } + [JsonProperty("rate_limit_per_user")] + public Optional SlowModeInterval { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyVoiceChannelParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyVoiceChannelParams.cs new file mode 100644 index 0000000..ce36eb1 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyVoiceChannelParams.cs @@ -0,0 +1,14 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class ModifyVoiceChannelParams : ModifyGuildChannelParams + { + [JsonProperty("bitrate")] + public Optional Bitrate { get; set; } + [JsonProperty("user_limit")] + public Optional UserLimit { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyWebhookParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyWebhookParams.cs new file mode 100644 index 0000000..0f2d6e3 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/ModifyWebhookParams.cs @@ -0,0 +1,16 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class ModifyWebhookParams + { + [JsonProperty("name")] + public Optional Name { get; set; } + [JsonProperty("avatar")] + public Optional Avatar { get; set; } + [JsonProperty("channel_id")] + public Optional ChannelId { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/SuppressEmbedParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/SuppressEmbedParams.cs new file mode 100644 index 0000000..9139627 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/SuppressEmbedParams.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class SuppressEmbedParams + { + [JsonProperty("suppress")] + public bool Suppressed { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/UploadFileParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/UploadFileParams.cs new file mode 100644 index 0000000..7ba21d0 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/UploadFileParams.cs @@ -0,0 +1,59 @@ +#pragma warning disable CS1591 +using Discord.Net.Converters; +using Discord.Net.Rest; +using Newtonsoft.Json; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Discord.API.Rest +{ + internal class UploadFileParams + { + private static JsonSerializer _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; + + 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 Optional Embed { get; set; } + public bool IsSpoiler { get; set; } = false; + + public UploadFileParams(Stream file) + { + File = file; + } + + public IReadOnlyDictionary ToDictionary() + { + var d = new Dictionary(); + var filename = Filename.GetValueOrDefault("unknown.dat"); + if (IsSpoiler && !filename.StartsWith(AttachmentExtensions.SpoilerPrefix)) + filename = filename.Insert(0, AttachmentExtensions.SpoilerPrefix); + d["file"] = new MultipartFile(File, filename); + + var payload = new Dictionary(); + if (Content.IsSpecified) + payload["content"] = Content.Value; + if (IsTTS.IsSpecified) + payload["tts"] = IsTTS.Value.ToString(); + if (Nonce.IsSpecified) + payload["nonce"] = Nonce.Value; + if (Embed.IsSpecified) + payload["embed"] = Embed.Value; + if (IsSpoiler) + payload["hasSpoiler"] = IsSpoiler.ToString(); + + var json = new StringBuilder(); + using (var text = new StringWriter(json)) + using (var writer = new JsonTextWriter(text)) + _serializer.Serialize(writer, payload); + + d["payload_json"] = json.ToString(); + + return d; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs new file mode 100644 index 0000000..26153c2 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs @@ -0,0 +1,65 @@ +#pragma warning disable CS1591 +using System.Collections.Generic; +using System.IO; +using System.Text; +using Discord.Net.Converters; +using Discord.Net.Rest; +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + internal class UploadWebhookFileParams + { + private static JsonSerializer _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; + + 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 Optional Username { get; set; } + public Optional AvatarUrl { get; set; } + public Optional Embeds { get; set; } + + public bool IsSpoiler { get; set; } = false; + + public UploadWebhookFileParams(Stream file) + { + File = file; + } + + public IReadOnlyDictionary ToDictionary() + { + var d = new Dictionary(); + var filename = Filename.GetValueOrDefault("unknown.dat"); + if (IsSpoiler && !filename.StartsWith(AttachmentExtensions.SpoilerPrefix)) + filename = filename.Insert(0, AttachmentExtensions.SpoilerPrefix); + + d["file"] = new MultipartFile(File, filename); + + var payload = new Dictionary(); + if (Content.IsSpecified) + payload["content"] = Content.Value; + if (IsTTS.IsSpecified) + payload["tts"] = IsTTS.Value.ToString(); + if (Nonce.IsSpecified) + payload["nonce"] = Nonce.Value; + if (Username.IsSpecified) + payload["username"] = Username.Value; + if (AvatarUrl.IsSpecified) + payload["avatar_url"] = AvatarUrl.Value; + if (Embeds.IsSpecified) + payload["embeds"] = Embeds.Value; + + var json = new StringBuilder(); + using (var text = new StringWriter(json)) + using (var writer = new JsonTextWriter(text)) + _serializer.Serialize(writer, payload); + + d["payload_json"] = json.ToString(); + + return d; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/UnixTimestampAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/UnixTimestampAttribute.cs new file mode 100644 index 0000000..3890ffc --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/API/UnixTimestampAttribute.cs @@ -0,0 +1,7 @@ +using System; + +namespace Discord.API +{ + [AttributeUsage(AttributeTargets.Property)] + internal class UnixTimestampAttribute : Attribute { } +} \ No newline at end of file diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/BaseDiscordClient.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/BaseDiscordClient.cs new file mode 100644 index 0000000..1837e38 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/BaseDiscordClient.cs @@ -0,0 +1,222 @@ +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); } } + internal 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; + private readonly SemaphoreSlim _stateLock; + private bool _isFirstLogin, _isDisposed; + + internal API.DiscordRestApiClient ApiClient { get; } + internal LogManager LogManager { get; } + /// + /// Gets the login state of the client. + /// + public LoginState LoginState { get; private set; } + /// + /// Gets the logged-in user. + /// + public ISelfUser CurrentUser { get; protected set; } + /// + public TokenType TokenType => ApiClient.AuthTokenType; + + /// 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); + + _stateLock = new SemaphoreSlim(1, 1); + _restLogger = LogManager.CreateLogger("Rest"); + _isFirstLogin = config.DisplayInitialLog; + + ApiClient.RequestQueue.RateLimitTriggered += async (id, info) => + { + if (info == null) + await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); + else + await _restLogger.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 _stateLock.WaitAsync().ConfigureAwait(false); + try + { + await LoginInternalAsync(tokenType, token, validateToken).ConfigureAwait(false); + } + finally { _stateLock.Release(); } + } + internal virtual async Task LoginInternalAsync(TokenType tokenType, string token, bool validateToken) + { + if (_isFirstLogin) + { + _isFirstLogin = false; + await LogManager.WriteInitialLog().ConfigureAwait(false); + } + + if (LoginState != LoginState.LoggedOut) + await LogoutInternalAsync().ConfigureAwait(false); + LoginState = LoginState.LoggingIn; + + try + { + // If token validation is enabled, validate the token and let it throw any ArgumentExceptions + // that result from invalid parameters + if (validateToken) + { + try + { + TokenUtils.ValidateToken(tokenType, token); + } + catch (ArgumentException ex) + { + // log these ArgumentExceptions and allow for the client to attempt to log in anyways + await LogManager.WarningAsync("Discord", "A supplied token was invalid.", ex).ConfigureAwait(false); + } + } + + await ApiClient.LoginAsync(tokenType, token).ConfigureAwait(false); + await OnLoginAsync(tokenType, token).ConfigureAwait(false); + LoginState = LoginState.LoggedIn; + } + catch + { + await LogoutInternalAsync().ConfigureAwait(false); + throw; + } + + await _loggedInEvent.InvokeAsync().ConfigureAwait(false); + } + internal virtual Task OnLoginAsync(TokenType tokenType, string token) + => Task.Delay(0); + + public async Task LogoutAsync() + { + await _stateLock.WaitAsync().ConfigureAwait(false); + try + { + await LogoutInternalAsync().ConfigureAwait(false); + } + finally { _stateLock.Release(); } + } + internal virtual 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); + } + internal virtual Task OnLogoutAsync() + => Task.Delay(0); + + internal virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { +#pragma warning disable IDISP007 + ApiClient.Dispose(); +#pragma warning restore IDISP007 + _stateLock?.Dispose(); + _isDisposed = true; + } + } + /// + public void Dispose() => Dispose(true); + + /// + public Task GetRecommendedShardCountAsync(RequestOptions options = null) + => ClientHelper.GetRecommendShardCountAsync(this, options); + + //IDiscordClient + /// + ConnectionState IDiscordClient.ConnectionState => ConnectionState.Disconnected; + /// + ISelfUser IDiscordClient.CurrentUser => CurrentUser; + + /// + Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) + => throw new NotSupportedException(); + + /// + Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(null); + /// + Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode, RequestOptions options) + => Task.FromResult>(ImmutableArray.Create()); + /// + Task> IDiscordClient.GetDMChannelsAsync(CacheMode mode, RequestOptions options) + => Task.FromResult>(ImmutableArray.Create()); + /// + Task> IDiscordClient.GetGroupChannelsAsync(CacheMode mode, RequestOptions options) + => Task.FromResult>(ImmutableArray.Create()); + + /// + Task> IDiscordClient.GetConnectionsAsync(RequestOptions options) + => Task.FromResult>(ImmutableArray.Create()); + + /// + Task IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options) + => Task.FromResult(null); + + /// + Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(null); + /// + Task> IDiscordClient.GetGuildsAsync(CacheMode mode, RequestOptions options) + => Task.FromResult>(ImmutableArray.Create()); + /// + /// Creating a guild is not supported with the base client. + Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) + => throw new NotSupportedException(); + + /// + Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(null); + /// + Task IDiscordClient.GetUserAsync(string username, string discriminator, RequestOptions options) + => Task.FromResult(null); + + /// + Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) + => Task.FromResult>(ImmutableArray.Create()); + /// + Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) + => Task.FromResult(null); + + /// + Task IDiscordClient.GetWebhookAsync(ulong id, RequestOptions options) + => Task.FromResult(null); + + /// + Task IDiscordClient.StartAsync() + => Task.Delay(0); + /// + Task IDiscordClient.StopAsync() + => Task.Delay(0); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/ClientHelper.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/ClientHelper.cs new file mode 100644 index 0000000..a8f6b58 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/ClientHelper.cs @@ -0,0 +1,180 @@ +using System; +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, RequestOptions options) + { + var model = await client.ApiClient.GetMyApplicationAsync(options).ConfigureAwait(false); + return RestApplication.Create(client, model); + } + + public static async Task GetChannelAsync(BaseDiscordClient client, + ulong id, RequestOptions options) + { + var model = await client.ApiClient.GetChannelAsync(id, options).ConfigureAwait(false); + if (model != null) + return RestChannel.Create(client, model); + return null; + } + /// Unexpected channel type. + public static async Task> GetPrivateChannelsAsync(BaseDiscordClient client, RequestOptions options) + { + var models = await client.ApiClient.GetMyPrivateChannelsAsync(options).ConfigureAwait(false); + return models.Select(x => RestChannel.CreatePrivate(client, x)).ToImmutableArray(); + } + public static async Task> GetDMChannelsAsync(BaseDiscordClient client, RequestOptions options) + { + var models = await client.ApiClient.GetMyPrivateChannelsAsync(options).ConfigureAwait(false); + return models + .Where(x => x.Type == ChannelType.DM) + .Select(x => RestDMChannel.Create(client, x)).ToImmutableArray(); + } + public static async Task> GetGroupChannelsAsync(BaseDiscordClient client, RequestOptions options) + { + var models = await client.ApiClient.GetMyPrivateChannelsAsync(options).ConfigureAwait(false); + return models + .Where(x => x.Type == ChannelType.Group) + .Select(x => RestGroupChannel.Create(client, x)).ToImmutableArray(); + } + + public static async Task> GetConnectionsAsync(BaseDiscordClient client, RequestOptions options) + { + var models = await client.ApiClient.GetMyConnectionsAsync(options).ConfigureAwait(false); + return models.Select(RestConnection.Create).ToImmutableArray(); + } + + public static async Task GetInviteAsync(BaseDiscordClient client, + string inviteId, RequestOptions options) + { + var model = await client.ApiClient.GetInviteAsync(inviteId, options).ConfigureAwait(false); + if (model != null) + return RestInviteMetadata.Create(client, null, null, model); + return null; + } + + public static async Task GetGuildAsync(BaseDiscordClient client, + ulong id, RequestOptions options) + { + var model = await client.ApiClient.GetGuildAsync(id, options).ConfigureAwait(false); + if (model != null) + return RestGuild.Create(client, model); + return null; + } + public static async Task GetGuildEmbedAsync(BaseDiscordClient client, + ulong id, RequestOptions options) + { + var model = await client.ApiClient.GetGuildEmbedAsync(id, options).ConfigureAwait(false); + if (model != null) + return RestGuildEmbed.Create(model); + return null; + } + public static IAsyncEnumerable> GetGuildSummariesAsync(BaseDiscordClient client, + ulong? fromGuildId, int? limit, RequestOptions options) + { + return new PagedAsyncEnumerable( + DiscordConfig.MaxGuildsPerBatch, + async (info, ct) => + { + var args = new GetGuildSummariesParams + { + Limit = info.PageSize + }; + if (info.Position != null) + args.AfterGuildId = info.Position.Value; + var models = await client.ApiClient.GetMyGuildsAsync(args, options).ConfigureAwait(false); + return models + .Select(x => RestUserGuild.Create(client, x)) + .ToImmutableArray(); + }, + nextPage: (info, lastPage) => + { + if (lastPage.Count != DiscordConfig.MaxMessagesPerBatch) + return false; + info.Position = lastPage.Max(x => x.Id); + return true; + }, + start: fromGuildId, + count: limit + ); + } + public static async Task> GetGuildsAsync(BaseDiscordClient client, RequestOptions options) + { + var summaryModels = await GetGuildSummariesAsync(client, null, null, options).FlattenAsync().ConfigureAwait(false); + var guilds = ImmutableArray.CreateBuilder(); + 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, RequestOptions options) + { + var args = new CreateGuildParams(name, region.Id); + if (jpegIcon != null) + args.Icon = new API.Image(jpegIcon); + + var model = await client.ApiClient.CreateGuildAsync(args, options).ConfigureAwait(false); + return RestGuild.Create(client, model); + } + + public static async Task GetUserAsync(BaseDiscordClient client, + ulong id, RequestOptions options) + { + var model = await client.ApiClient.GetUserAsync(id, options).ConfigureAwait(false); + if (model != null) + return RestUser.Create(client, model); + return null; + } + public static async Task GetGuildUserAsync(BaseDiscordClient client, + ulong guildId, ulong id, RequestOptions options) + { + var guild = await GetGuildAsync(client, guildId, options).ConfigureAwait(false); + if (guild == null) + return null; + + var model = await client.ApiClient.GetGuildMemberAsync(guildId, id, options).ConfigureAwait(false); + if (model != null) + return RestGuildUser.Create(client, guild, model); + + return null; + } + + public static async Task GetWebhookAsync(BaseDiscordClient client, ulong id, RequestOptions options) + { + var model = await client.ApiClient.GetWebhookAsync(id).ConfigureAwait(false); + if (model != null) + return RestWebhook.Create(client, (IGuild)null, model); + return null; + } + + public static async Task> GetVoiceRegionsAsync(BaseDiscordClient client, RequestOptions options) + { + var models = await client.ApiClient.GetVoiceRegionsAsync(options).ConfigureAwait(false); + return models.Select(x => RestVoiceRegion.Create(client, x)).ToImmutableArray(); + } + public static async Task GetVoiceRegionAsync(BaseDiscordClient client, + string id, RequestOptions options) + { + var models = await client.ApiClient.GetVoiceRegionsAsync(options).ConfigureAwait(false); + return models.Select(x => RestVoiceRegion.Create(client, x)).FirstOrDefault(x => x.Id == id); + } + + public static async Task GetRecommendShardCountAsync(BaseDiscordClient client, RequestOptions options) + { + var response = await client.ApiClient.GetBotGatewayAsync(options).ConfigureAwait(false); + return response.Shards; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/DiscordRestApiClient.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/DiscordRestApiClient.cs new file mode 100644 index 0000000..ff6d172 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/DiscordRestApiClient.cs @@ -0,0 +1,1546 @@ + +#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.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 +{ + internal 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; + + protected bool _isDisposed; + private CancellationTokenSource _loginCancelToken; + + public RetryMode DefaultRetryMode { get; } + public string UserAgent { get; } + internal RequestQueue RequestQueue { get; } + + public LoginState LoginState { get; private set; } + public TokenType AuthTokenType { get; private set; } + internal string AuthToken { get; private set; } + internal IRestClient RestClient { get; private set; } + internal ulong? CurrentUserId { get; set; } + public RateLimitPrecision RateLimitPrecision { get; private set; } + internal bool UseSystemClock { get; set; } + + internal JsonSerializer Serializer => _serializer; + + /// Unknown OAuth token type. + public DiscordRestApiClient(RestClientProvider restClientProvider, string userAgent, RetryMode defaultRetryMode = RetryMode.AlwaysRetry, + JsonSerializer serializer = null, RateLimitPrecision rateLimitPrecision = RateLimitPrecision.Second, bool useSystemClock = true) + { + _restClientProvider = restClientProvider; + UserAgent = userAgent; + DefaultRetryMode = defaultRetryMode; + _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; + RateLimitPrecision = rateLimitPrecision; + UseSystemClock = useSystemClock; + + RequestQueue = new RequestQueue(); + _stateLock = new SemaphoreSlim(1, 1); + + SetBaseUrl(DiscordConfig.APIUrl); + } + + /// Unknown OAuth token type. + internal void SetBaseUrl(string baseUrl) + { + RestClient?.Dispose(); + RestClient = _restClientProvider(baseUrl); + RestClient.SetHeader("accept", "*/*"); + RestClient.SetHeader("user-agent", UserAgent); + RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken)); + RestClient.SetHeader("X-RateLimit-Precision", RateLimitPrecision.ToString().ToLower()); + } + /// Unknown OAuth token type. + internal static string GetPrefixedToken(TokenType tokenType, string token) + { + switch (tokenType) + { + case default(TokenType): + return token; + case TokenType.Bot: + return $"Bot {token}"; + case TokenType.Bearer: + return $"Bearer {token}"; + default: + throw new ArgumentException(message: "Unknown OAuth token type.", paramName: nameof(tokenType)); + } + } + internal virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + _loginCancelToken?.Dispose(); + RestClient?.Dispose(); + RequestQueue?.Dispose(); + _stateLock?.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?.Dispose(); + _loginCancelToken = new CancellationTokenSource(); + + AuthToken = null; + await RequestQueue.SetCancelTokenAsync(_loginCancelToken.Token).ConfigureAwait(false); + RestClient.SetCancelToken(_loginCancelToken.Token); + + AuthTokenType = tokenType; + AuthToken = token; + if (tokenType != TokenType.Webhook) + RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken)); + + LoginState = LoginState.LoggedIn; + } + catch + { + 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); + + CurrentUserId = null; + LoginState = LoginState.LoggedOut; + } + + internal virtual Task ConnectInternalAsync() => Task.Delay(0); + internal virtual Task DisconnectInternalAsync() => Task.Delay(0); + + //Core + internal Task SendAsync(string method, Expression> endpointExpr, BucketIds ids, + ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) + => SendAsync(method, GetEndpoint(endpointExpr), GetBucketId(ids, endpointExpr, funcName), clientBucket, options); + public async Task SendAsync(string method, string endpoint, + string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) + { + options = options ?? new RequestOptions(); + options.HeaderOnly = true; + options.BucketId = bucketId; + + var request = new RestRequest(RestClient, method, endpoint, options); + await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); + } + + internal Task SendJsonAsync(string method, Expression> endpointExpr, object payload, BucketIds ids, + ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) + => SendJsonAsync(method, GetEndpoint(endpointExpr), payload, GetBucketId(ids, endpointExpr, funcName), clientBucket, options); + public async Task SendJsonAsync(string method, string endpoint, object payload, + string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) + { + options = options ?? new RequestOptions(); + options.HeaderOnly = true; + options.BucketId = bucketId; + + string json = payload != null ? SerializeJson(payload) : null; + var request = new JsonRestRequest(RestClient, method, endpoint, json, options); + await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); + } + + internal Task SendMultipartAsync(string method, Expression> endpointExpr, IReadOnlyDictionary multipartArgs, BucketIds ids, + ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) + => SendMultipartAsync(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(ids, endpointExpr, funcName), clientBucket, options); + public async Task SendMultipartAsync(string method, string endpoint, IReadOnlyDictionary multipartArgs, + string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) + { + options = options ?? new RequestOptions(); + options.HeaderOnly = true; + options.BucketId = bucketId; + + var request = new MultipartRestRequest(RestClient, method, endpoint, multipartArgs, options); + await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); + } + + internal Task SendAsync(string method, Expression> endpointExpr, BucketIds ids, + ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) where TResponse : class + => SendAsync(method, GetEndpoint(endpointExpr), GetBucketId(ids, endpointExpr, funcName), clientBucket, options); + public async Task SendAsync(string method, string endpoint, + string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class + { + options = options ?? new RequestOptions(); + options.BucketId = bucketId; + + var request = new RestRequest(RestClient, method, endpoint, options); + return DeserializeJson(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false)); + } + + internal Task SendJsonAsync(string method, Expression> endpointExpr, object payload, BucketIds ids, + ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) where TResponse : class + => SendJsonAsync(method, GetEndpoint(endpointExpr), payload, GetBucketId(ids, endpointExpr, funcName), clientBucket, options); + public async Task SendJsonAsync(string method, string endpoint, object payload, + string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class + { + options = options ?? new RequestOptions(); + options.BucketId = bucketId; + + string json = payload != null ? SerializeJson(payload) : null; + var request = new JsonRestRequest(RestClient, method, endpoint, json, options); + return DeserializeJson(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false)); + } + + internal Task SendMultipartAsync(string method, Expression> endpointExpr, IReadOnlyDictionary multipartArgs, BucketIds ids, + ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) + => SendMultipartAsync(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(ids, endpointExpr, funcName), clientBucket, options); + public async Task SendMultipartAsync(string method, string endpoint, IReadOnlyDictionary multipartArgs, + string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) + { + options = options ?? new RequestOptions(); + options.BucketId = bucketId; + + var request = new MultipartRestRequest(RestClient, method, endpoint, multipartArgs, options); + return DeserializeJson(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false)); + } + + private async Task SendInternalAsync(string method, string endpoint, RestRequest request) + { + if (!request.Options.IgnoreState) + CheckState(); + if (request.Options.RetryMode == null) + request.Options.RetryMode = DefaultRetryMode; + if (request.Options.UseSystemClock == null) + request.Options.UseSystemClock = UseSystemClock; + + 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); + } + + //Gateway + public async Task GetGatewayAsync(RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + return await SendAsync("GET", () => "gateway", new BucketIds(), options: options).ConfigureAwait(false); + } + public async Task GetBotGatewayAsync(RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + return await SendAsync("GET", () => "gateway/bot", 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.HttpCode == 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.HttpCode == 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); + } + /// + /// must not be equal to zero. + /// -and- + /// must be greater than zero. + /// + /// + /// must not be . + /// -and- + /// must not be or empty. + /// + public async Task ModifyGuildChannelAsync(ulong channelId, Rest.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, Rest.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)); + Preconditions.AtLeast(args.SlowModeInterval, 0, nameof(args.SlowModeInterval)); + Preconditions.AtMost(args.SlowModeInterval, 21600, nameof(args.SlowModeInterval)); + 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, Rest.ModifyVoiceChannelParams args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.AtLeast(args.Bitrate, 8000, nameof(args.Bitrate)); + Preconditions.AtLeast(args.UserLimit, 0, nameof(args.UserLimit)); + 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 Rest.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; + } + } + public async Task AddRoleAsync(ulong guildId, ulong userId, ulong roleId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(userId, 0, nameof(userId)); + Preconditions.NotEqual(roleId, 0, nameof(roleId)); + Preconditions.NotEqual(roleId, guildId, nameof(roleId), "The Everyone role cannot be added to a user."); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + await SendAsync("PUT", () => $"guilds/{guildId}/members/{userId}/roles/{roleId}", ids, options: options).ConfigureAwait(false); + } + public async Task RemoveRoleAsync(ulong guildId, ulong userId, ulong roleId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(userId, 0, nameof(userId)); + Preconditions.NotEqual(roleId, 0, nameof(roleId)); + Preconditions.NotEqual(roleId, guildId, nameof(roleId), "The Everyone role cannot be removed from a user."); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + await SendAsync("DELETE", () => $"guilds/{guildId}/members/{userId}/roles/{roleId}", ids, options: options).ConfigureAwait(false); + } + + //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.HttpCode == 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); + } + /// Message content is too long, length must be less or equal to . + public async Task CreateMessageAsync(ulong channelId, CreateMessageParams args, RequestOptions options = null) + { + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + if (!args.Embed.IsSpecified || args.Embed.Value == null) + Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); + + if (args.Content?.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentException(message: $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", paramName: nameof(args.Content)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + return await SendJsonAsync("POST", () => $"channels/{channelId}/messages", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + } + /// Message content is too long, length must be less or equal to . + /// This operation may only be called with a token. + public async Task CreateWebhookMessageAsync(ulong webhookId, CreateWebhookMessageParams args, RequestOptions options = null) + { + if (AuthTokenType != TokenType.Webhook) + throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); + + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEqual(webhookId, 0, nameof(webhookId)); + if (!args.Embeds.IsSpecified || args.Embeds.Value == null || args.Embeds.Value.Length == 0) + Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); + + if (args.Content?.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentException(message: $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", paramName: nameof(args.Content)); + options = RequestOptions.CreateOrClone(options); + + return await SendJsonAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args, new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + } + /// Message content is too long, length must be less or equal to . + 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 && 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, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + } + + /// Message content is too long, length must be less or equal to . + /// This operation may only be called with a token. + public async Task UploadWebhookFileAsync(ulong webhookId, UploadWebhookFileParams args, RequestOptions options = null) + { + if (AuthTokenType != TokenType.Webhook) + throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); + + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEqual(webhookId, 0, nameof(webhookId)); + 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)); + } + + return await SendMultipartAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args.ToDictionary(), new BucketIds(), clientBucket: ClientBucketType.SendEdit, 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)); + Preconditions.YoungerThanTwoWeeks(args.MessageIds, nameof(args.MessageIds)); + 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; + } + } + /// Message content is too long, length must be less or equal to . + public async Task ModifyMessageAsync(ulong channelId, ulong messageId, Rest.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) + { + if (!args.Embed.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, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + } + + public async Task SuppressEmbedAsync(ulong channelId, ulong messageId, Rest.SuppressEmbedParams args, 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 SendJsonAsync("POST", () => $"channels/{channelId}/messages/{messageId}/suppress-embeds", args, ids, options: options).ConfigureAwait(false); + } + + public async Task AddReactionAsync(ulong channelId, ulong messageId, string emoji, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + Preconditions.NotNullOrWhitespace(emoji, nameof(emoji)); + + options = RequestOptions.CreateOrClone(options); + options.IsReactionBucket = true; + + var ids = new BucketIds(channelId: channelId); + + // @me is non-const to fool the ratelimiter, otherwise it will put add/remove in separate buckets + var me = "@me"; + await SendAsync("PUT", () => $"channels/{channelId}/messages/{messageId}/reactions/{emoji}/{me}", ids, options: options).ConfigureAwait(false); + } + public async Task RemoveReactionAsync(ulong channelId, ulong messageId, ulong userId, string emoji, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + Preconditions.NotNullOrWhitespace(emoji, nameof(emoji)); + + options = RequestOptions.CreateOrClone(options); + options.IsReactionBucket = true; + + var ids = new BucketIds(channelId: channelId); + + var user = CurrentUserId.HasValue ? (userId == CurrentUserId.Value ? "@me" : userId.ToString()) : userId.ToString(); + await SendAsync("DELETE", () => $"channels/{channelId}/messages/{messageId}/reactions/{emoji}/{user}", ids, options: options).ConfigureAwait(false); + } + public async Task RemoveAllReactionsAsync(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}/reactions", ids, options: options).ConfigureAwait(false); + } + public async Task> GetReactionUsersAsync(ulong channelId, ulong messageId, string emoji, GetReactionUsersParams args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + Preconditions.NotNullOrWhitespace(emoji, nameof(emoji)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.GreaterThan(args.Limit, 0, nameof(args.Limit)); + Preconditions.AtMost(args.Limit, DiscordConfig.MaxUserReactionsPerBatch, nameof(args.Limit)); + Preconditions.GreaterThan(args.AfterUserId, 0, nameof(args.AfterUserId)); + options = RequestOptions.CreateOrClone(options); + + int limit = args.Limit.GetValueOrDefault(DiscordConfig.MaxUserReactionsPerBatch); + ulong afterUserId = args.AfterUserId.GetValueOrDefault(0); + + var ids = new BucketIds(channelId: channelId); + Expression> endpoint = () => $"channels/{channelId}/messages/{messageId}/reactions/{emoji}?limit={limit}&after={afterUserId}"; + return await SendAsync>("GET", endpoint, 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.HttpCode == 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, Rest.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, 1, 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, 1, nameof(args.Days)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendAsync("GET", () => $"guilds/{guildId}/prune?days={args.Days}", 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 GetGuildBanAsync(ulong guildId, ulong userId, RequestOptions options) + { + Preconditions.NotEqual(userId, 0, nameof(userId)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendAsync("GET", () => $"guilds/{guildId}/bans/{userId}", ids, options: options).ConfigureAwait(false); + } + /// + /// and must not be equal to zero. + /// -and- + /// must be between 0 to 7. + /// + /// must not be . + 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), "Prune length must be within [0, 7]"); + Preconditions.AtMost(args.DeleteMessageDays, 7, nameof(args.DeleteMessageDays), "Prune length must be within [0, 7]"); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + string reason = string.IsNullOrWhiteSpace(args.Reason) ? "" : $"&reason={Uri.EscapeDataString(args.Reason)}"; + await SendAsync("PUT", () => $"guilds/{guildId}/bans/{userId}?delete-message-days={args.DeleteMessageDays}{reason}", ids, options: options).ConfigureAwait(false); + } + /// and must not be equal to zero. + 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 + /// must not be equal to zero. + 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.HttpCode == HttpStatusCode.NotFound) { return null; } + } + /// must not be equal to zero. + /// must not be . + public async Task ModifyGuildEmbedAsync(ulong guildId, Rest.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 + /// must not be equal to zero. + 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); + } + /// and must not be equal to zero. + /// must not be . + 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, Rest.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 + /// cannot be blank. + /// must not be . + 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}?with_counts=true", new BucketIds(), options: options).ConfigureAwait(false); + } + catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; } + } + /// may not be equal to zero. + public async Task GetVanityInviteAsync(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}/vanity-url", ids, options: options).ConfigureAwait(false); + } + /// may not be equal to zero. + 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); + } + /// may not be equal to zero. + 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); + } + /// + /// may not be equal to zero. + /// -and- + /// and must be greater than zero. + /// -and- + /// must be lesser than 86400. + /// + /// must not be . + 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)); + Preconditions.AtMost(args.MaxAge, 86400, nameof(args.MaxAge), + "The maximum age of an invite must be less than or equal to a day (86400 seconds)."); + 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); + } + + //Guild Members + public async Task AddGuildMemberAsync(ulong guildId, ulong userId, AddGuildMemberParams args, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(userId, 0, nameof(userId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotNullOrWhitespace(args.AccessToken, nameof(args.AccessToken)); + + if (args.RoleIds.IsSpecified) + { + foreach (var roleId in args.RoleIds.Value) + Preconditions.NotEqual(roleId, 0, nameof(roleId)); + } + + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + + return await SendJsonAsync("PUT", () => $"guilds/{guildId}/members/{userId}", args, ids, options: options); + } + 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.HttpCode == 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, string reason, 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); + reason = string.IsNullOrWhiteSpace(reason) ? "" : $"?reason={Uri.EscapeDataString(reason)}"; + await SendAsync("DELETE", () => $"guilds/{guildId}/members/{userId}{reason}", ids, options: options).ConfigureAwait(false); + } + public async Task ModifyGuildMemberAsync(ulong guildId, ulong userId, Rest.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 == CurrentUserId; + + if (args.RoleIds.IsSpecified) + Preconditions.NotEveryoneRole(args.RoleIds.Value, guildId, nameof(args.RoleIds)); + if (isCurrentUser && args.Nickname.IsSpecified) + { + var nickArgs = new Rest.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, Rest.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)); + 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 ids = new BucketIds(guildId: guildId); + return await SendJsonAsync>("PATCH", () => $"guilds/{guildId}/roles", args, ids, options: options).ConfigureAwait(false); + } + + //Guild emoji + public async Task GetGuildEmoteAsync(ulong guildId, ulong emoteId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(emoteId, 0, nameof(emoteId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendAsync("GET", () => $"guilds/{guildId}/emojis/{emoteId}", ids, options: options).ConfigureAwait(false); + } + + public async Task CreateGuildEmoteAsync(ulong guildId, Rest.CreateGuildEmoteParams args, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotNullOrWhitespace(args.Name, nameof(args.Name)); + Preconditions.NotNull(args.Image.Stream, nameof(args.Image)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendJsonAsync("POST", () => $"guilds/{guildId}/emojis", args, ids, options: options).ConfigureAwait(false); + } + + public async Task ModifyGuildEmoteAsync(ulong guildId, ulong emoteId, ModifyGuildEmoteParams args, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(emoteId, 0, nameof(emoteId)); + Preconditions.NotNull(args, nameof(args)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendJsonAsync("PATCH", () => $"guilds/{guildId}/emojis/{emoteId}", args, ids, options: options).ConfigureAwait(false); + } + + public async Task DeleteGuildEmoteAsync(ulong guildId, ulong emoteId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(emoteId, 0, nameof(emoteId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + await SendAsync("DELETE", () => $"guilds/{guildId}/emojis/{emoteId}", 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.HttpCode == 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(GetGuildSummariesParams args, RequestOptions options = null) + { + Preconditions.NotNull(args, nameof(args)); + Preconditions.GreaterThan(args.Limit, 0, nameof(args.Limit)); + Preconditions.AtMost(args.Limit, DiscordConfig.MaxGuildsPerBatch, nameof(args.Limit)); + Preconditions.GreaterThan(args.AfterGuildId, 0, nameof(args.AfterGuildId)); + options = RequestOptions.CreateOrClone(options); + + int limit = args.Limit.GetValueOrDefault(int.MaxValue); + ulong afterGuildId = args.AfterGuildId.GetValueOrDefault(0); + + return await SendAsync>("GET", () => $"users/@me/guilds?limit={limit}&after={afterGuildId}", 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(Rest.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, Rest.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); + } + + //Audit logs + public async Task GetAuditLogsAsync(ulong guildId, GetAuditLogsParams args, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotNull(args, nameof(args)); + options = RequestOptions.CreateOrClone(options); + + int limit = args.Limit.GetValueOrDefault(int.MaxValue); + + var ids = new BucketIds(guildId: guildId); + Expression> endpoint; + + var queryArgs = new StringBuilder(); + if (args.BeforeEntryId.IsSpecified) + { + queryArgs.Append("&before=") + .Append(args.BeforeEntryId); + } + if (args.UserId.IsSpecified) + { + queryArgs.Append("&user_id=") + .Append(args.UserId.Value); + } + if (args.ActionType.IsSpecified) + { + queryArgs.Append("&action_type=") + .Append(args.ActionType.Value); + } + + // still use string interp for the query w/o params, as this is necessary for CreateBucketId + endpoint = () => $"guilds/{guildId}/audit-logs?limit={limit}{queryArgs.ToString()}"; + return await SendAsync("GET", endpoint, ids, options: options).ConfigureAwait(false); + } + + //Webhooks + public async Task CreateWebhookAsync(ulong channelId, CreateWebhookParams args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotNull(args.Name, nameof(args.Name)); + options = RequestOptions.CreateOrClone(options); + var ids = new BucketIds(channelId: channelId); + + return await SendJsonAsync("POST", () => $"channels/{channelId}/webhooks", args, ids, options: options).ConfigureAwait(false); + } + public async Task GetWebhookAsync(ulong webhookId, RequestOptions options = null) + { + Preconditions.NotEqual(webhookId, 0, nameof(webhookId)); + options = RequestOptions.CreateOrClone(options); + + try + { + if (AuthTokenType == TokenType.Webhook) + return await SendAsync("GET", () => $"webhooks/{webhookId}/{AuthToken}", new BucketIds(), options: options).ConfigureAwait(false); + else + return await SendAsync("GET", () => $"webhooks/{webhookId}", new BucketIds(), options: options).ConfigureAwait(false); + } + catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; } + } + public async Task ModifyWebhookAsync(ulong webhookId, ModifyWebhookParams args, RequestOptions options = null) + { + Preconditions.NotEqual(webhookId, 0, nameof(webhookId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name)); + options = RequestOptions.CreateOrClone(options); + + if (AuthTokenType == TokenType.Webhook) + return await SendJsonAsync("PATCH", () => $"webhooks/{webhookId}/{AuthToken}", args, new BucketIds(), options: options).ConfigureAwait(false); + else + return await SendJsonAsync("PATCH", () => $"webhooks/{webhookId}", args, new BucketIds(), options: options).ConfigureAwait(false); + } + public async Task DeleteWebhookAsync(ulong webhookId, RequestOptions options = null) + { + Preconditions.NotEqual(webhookId, 0, nameof(webhookId)); + options = RequestOptions.CreateOrClone(options); + + if (AuthTokenType == TokenType.Webhook) + await SendAsync("DELETE", () => $"webhooks/{webhookId}/{AuthToken}", new BucketIds(), options: options).ConfigureAwait(false); + else + await SendAsync("DELETE", () => $"webhooks/{webhookId}", new BucketIds(), options: options).ConfigureAwait(false); + } + public async Task> GetGuildWebhooksAsync(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}/webhooks", ids, options: options).ConfigureAwait(false); + } + public async Task> GetChannelWebhooksAsync(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}/webhooks", ids, options: options).ConfigureAwait(false); + } + + //Helpers + /// Client is not logged in. + 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 class BucketIds + { + public ulong GuildId { get; internal set; } + public ulong ChannelId { get; internal set; } + + 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; + + //Unpack the array, if one exists (happens with 4+ parameters) + if (methodArgs.Length > 1 && methodArgs[1].NodeType == ExpressionType.NewArrayInit) + { + var arrayExpr = methodArgs[1] as NewArrayExpression; + var elements = arrayExpr.Expressions.ToArray(); + Array.Resize(ref methodArgs, elements.Length + 1); + Array.Copy(elements, 0, methodArgs, 1, elements.Length); + } + + 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 - lastIndex); + int rightIndex = format.IndexOf("}", leftIndex); + + int argId = int.Parse(format.Substring(leftIndex + 1, rightIndex - leftIndex - 1), NumberStyles.None, CultureInfo.InvariantCulture); + string fieldName = GetFieldName(methodArgs[argId + 1]); + + var mappedId = BucketIds.GetIndex(fieldName); + + if(!mappedId.HasValue && rightIndex != endIndex && format.Length > rightIndex + 1 && format[rightIndex + 1] == '/') //Ignore the next slash + rightIndex++; + + if (mappedId.HasValue) + builder.Append($"{{{mappedId.Value}}}"); + + lastIndex = rightIndex + 1; + } + if (builder[builder.Length - 1] == '/') + builder.Remove(builder.Length - 1, 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/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/DiscordRestClient.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/DiscordRestClient.cs new file mode 100644 index 0000000..4c29d16 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/DiscordRestClient.cs @@ -0,0 +1,188 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Threading.Tasks; + +namespace Discord.Rest +{ + /// + /// Provides a client to send REST-based requests to Discord. + /// + public class DiscordRestClient : BaseDiscordClient, IDiscordClient + { + private RestApplication _applicationInfo; + + /// + /// Gets the logged-in user. + /// + public new RestSelfUser CurrentUser => base.CurrentUser as RestSelfUser; + + /// + public DiscordRestClient() : this(new DiscordRestConfig()) { } + /// + /// Initializes a new with the provided configuration. + /// + /// The configuration to be used with the client. + public DiscordRestClient(DiscordRestConfig config) : base(config, CreateApiClient(config)) { } + // used for socket client rest access + internal DiscordRestClient(DiscordRestConfig config, API.DiscordRestApiClient api) : base(config, api) { } + + private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config) + => new API.DiscordRestApiClient(config.RestClientProvider, + DiscordRestConfig.UserAgent, + rateLimitPrecision: config.RateLimitPrecision, + useSystemClock: config.UseSystemClock); + + internal override void Dispose(bool disposing) + { + if (disposing) + ApiClient.Dispose(); + + base.Dispose(disposing); + } + + /// + internal override async Task OnLoginAsync(TokenType tokenType, string token) + { + var user = await ApiClient.GetMyUserAsync(new RequestOptions { RetryMode = RetryMode.AlwaysRetry }).ConfigureAwait(false); + ApiClient.CurrentUserId = user.Id; + base.CurrentUser = RestSelfUser.Create(this, user); + } + /// + internal override Task OnLogoutAsync() + { + _applicationInfo = null; + return Task.Delay(0); + } + + public async Task GetApplicationInfoAsync(RequestOptions options = null) + { + return _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this, options).ConfigureAwait(false)); + } + + public Task GetChannelAsync(ulong id, RequestOptions options = null) + => ClientHelper.GetChannelAsync(this, id, options); + public Task> GetPrivateChannelsAsync(RequestOptions options = null) + => ClientHelper.GetPrivateChannelsAsync(this, options); + public Task> GetDMChannelsAsync(RequestOptions options = null) + => ClientHelper.GetDMChannelsAsync(this, options); + public Task> GetGroupChannelsAsync(RequestOptions options = null) + => ClientHelper.GetGroupChannelsAsync(this, options); + + public Task> GetConnectionsAsync(RequestOptions options = null) + => ClientHelper.GetConnectionsAsync(this, options); + + public Task GetInviteAsync(string inviteId, RequestOptions options = null) + => ClientHelper.GetInviteAsync(this, inviteId, options); + + public Task GetGuildAsync(ulong id, RequestOptions options = null) + => ClientHelper.GetGuildAsync(this, id, options); + public Task GetGuildEmbedAsync(ulong id, RequestOptions options = null) + => ClientHelper.GetGuildEmbedAsync(this, id, options); + public IAsyncEnumerable> GetGuildSummariesAsync(RequestOptions options = null) + => ClientHelper.GetGuildSummariesAsync(this, null, null, options); + public IAsyncEnumerable> GetGuildSummariesAsync(ulong fromGuildId, int limit, RequestOptions options = null) + => ClientHelper.GetGuildSummariesAsync(this, fromGuildId, limit, options); + public Task> GetGuildsAsync(RequestOptions options = null) + => ClientHelper.GetGuildsAsync(this, options); + public Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null, RequestOptions options = null) + => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon, options); + + public Task GetUserAsync(ulong id, RequestOptions options = null) + => ClientHelper.GetUserAsync(this, id, options); + public Task GetGuildUserAsync(ulong guildId, ulong id, RequestOptions options = null) + => ClientHelper.GetGuildUserAsync(this, guildId, id, options); + + public Task> GetVoiceRegionsAsync(RequestOptions options = null) + => ClientHelper.GetVoiceRegionsAsync(this, options); + public Task GetVoiceRegionAsync(string id, RequestOptions options = null) + => ClientHelper.GetVoiceRegionAsync(this, id, options); + public Task GetWebhookAsync(ulong id, RequestOptions options = null) + => ClientHelper.GetWebhookAsync(this, id, options); + + //IDiscordClient + /// + async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) + => await GetApplicationInfoAsync(options).ConfigureAwait(false); + + /// + async Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetChannelAsync(id, options).ConfigureAwait(false); + else + return null; + } + /// + async Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetPrivateChannelsAsync(options).ConfigureAwait(false); + else + return ImmutableArray.Create(); + } + /// + async Task> IDiscordClient.GetDMChannelsAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetDMChannelsAsync(options).ConfigureAwait(false); + else + return ImmutableArray.Create(); + } + /// + async Task> IDiscordClient.GetGroupChannelsAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetGroupChannelsAsync(options).ConfigureAwait(false); + else + return ImmutableArray.Create(); + } + + /// + async Task> IDiscordClient.GetConnectionsAsync(RequestOptions options) + => await GetConnectionsAsync(options).ConfigureAwait(false); + + async Task IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options) + => await GetInviteAsync(inviteId, options).ConfigureAwait(false); + + /// + async Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetGuildAsync(id, options).ConfigureAwait(false); + else + return null; + } + /// + async Task> IDiscordClient.GetGuildsAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetGuildsAsync(options).ConfigureAwait(false); + else + return ImmutableArray.Create(); + } + /// + async Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) + => await CreateGuildAsync(name, region, jpegIcon, options).ConfigureAwait(false); + + /// + async Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetUserAsync(id, options).ConfigureAwait(false); + else + return null; + } + + /// + async Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) + => await GetVoiceRegionsAsync(options).ConfigureAwait(false); + /// + async Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) + => await GetVoiceRegionAsync(id, options).ConfigureAwait(false); + + /// + async Task IDiscordClient.GetWebhookAsync(ulong id, RequestOptions options) + => await GetWebhookAsync(id, options).ConfigureAwait(false); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/DiscordRestConfig.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/DiscordRestConfig.cs new file mode 100644 index 0000000..7bf7440 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/DiscordRestConfig.cs @@ -0,0 +1,13 @@ +using Discord.Net.Rest; + +namespace Discord.Rest +{ + /// + /// Represents a configuration class for . + /// + public class DiscordRestConfig : DiscordConfig + { + /// Gets or sets the provider used to generate new REST connections. + public RestClientProvider RestClientProvider { get; set; } = DefaultRestClientProvider.Instance; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs new file mode 100644 index 0000000..7936343 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + internal static class AuditLogHelper + { + private static readonly Dictionary> CreateMapping + = new Dictionary>() + { + [ActionType.GuildUpdated] = GuildUpdateAuditLogData.Create, + + [ActionType.ChannelCreated] = ChannelCreateAuditLogData.Create, + [ActionType.ChannelUpdated] = ChannelUpdateAuditLogData.Create, + [ActionType.ChannelDeleted] = ChannelDeleteAuditLogData.Create, + + [ActionType.OverwriteCreated] = OverwriteCreateAuditLogData.Create, + [ActionType.OverwriteUpdated] = OverwriteUpdateAuditLogData.Create, + [ActionType.OverwriteDeleted] = OverwriteDeleteAuditLogData.Create, + + [ActionType.Kick] = KickAuditLogData.Create, + [ActionType.Prune] = PruneAuditLogData.Create, + [ActionType.Ban] = BanAuditLogData.Create, + [ActionType.Unban] = UnbanAuditLogData.Create, + [ActionType.MemberUpdated] = MemberUpdateAuditLogData.Create, + [ActionType.MemberRoleUpdated] = MemberRoleAuditLogData.Create, + + [ActionType.RoleCreated] = RoleCreateAuditLogData.Create, + [ActionType.RoleUpdated] = RoleUpdateAuditLogData.Create, + [ActionType.RoleDeleted] = RoleDeleteAuditLogData.Create, + + [ActionType.InviteCreated] = InviteCreateAuditLogData.Create, + [ActionType.InviteUpdated] = InviteUpdateAuditLogData.Create, + [ActionType.InviteDeleted] = InviteDeleteAuditLogData.Create, + + [ActionType.WebhookCreated] = WebhookCreateAuditLogData.Create, + [ActionType.WebhookUpdated] = WebhookUpdateAuditLogData.Create, + [ActionType.WebhookDeleted] = WebhookDeleteAuditLogData.Create, + + [ActionType.EmojiCreated] = EmoteCreateAuditLogData.Create, + [ActionType.EmojiUpdated] = EmoteUpdateAuditLogData.Create, + [ActionType.EmojiDeleted] = EmoteDeleteAuditLogData.Create, + + [ActionType.MessageDeleted] = MessageDeleteAuditLogData.Create, + }; + + public static IAuditLogData CreateData(BaseDiscordClient discord, Model log, EntryModel entry) + { + if (CreateMapping.TryGetValue(entry.Action, out var func)) + return func(discord, log, entry); + + return null; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs new file mode 100644 index 0000000..fc807ca --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs @@ -0,0 +1,32 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to a ban. + /// + public class BanAuditLogData : IAuditLogData + { + private BanAuditLogData(IUser user) + { + Target = user; + } + + internal static BanAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); + return new BanAuditLogData(RestUser.Create(discord, userInfo)); + } + + /// + /// Gets the user that was banned. + /// + /// + /// A user object representing the banned user. + /// + public IUser Target { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs new file mode 100644 index 0000000..f432b4c --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs @@ -0,0 +1,112 @@ +using System.Collections.Generic; +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to a channel creation. + /// + public class ChannelCreateAuditLogData : IAuditLogData + { + private ChannelCreateAuditLogData(ulong id, string name, ChannelType type, int? rateLimit, bool? nsfw, int? bitrate, IReadOnlyCollection overwrites) + { + ChannelId = id; + ChannelName = name; + ChannelType = type; + SlowModeInterval = rateLimit; + IsNsfw = nsfw; + Bitrate = bitrate; + Overwrites = overwrites; + } + + internal static ChannelCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + var overwrites = new List(); + + var overwritesModel = changes.FirstOrDefault(x => x.ChangedProperty == "permission_overwrites"); + var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type"); + var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name"); + var rateLimitPerUserModel = changes.FirstOrDefault(x => x.ChangedProperty == "rate_limit_per_user"); + var nsfwModel = changes.FirstOrDefault(x => x.ChangedProperty == "nsfw"); + var bitrateModel = changes.FirstOrDefault(x => x.ChangedProperty == "bitrate"); + + var type = typeModel.NewValue.ToObject(discord.ApiClient.Serializer); + var name = nameModel.NewValue.ToObject(discord.ApiClient.Serializer); + int? rateLimitPerUser = rateLimitPerUserModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + bool? nsfw = nsfwModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + int? bitrate = bitrateModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + + foreach (var overwrite in overwritesModel.NewValue) + { + var deny = overwrite["deny"].ToObject(discord.ApiClient.Serializer); + var permType = overwrite["type"].ToObject(discord.ApiClient.Serializer); + var id = overwrite["id"].ToObject(discord.ApiClient.Serializer); + var allow = overwrite["allow"].ToObject(discord.ApiClient.Serializer); + + overwrites.Add(new Overwrite(id, permType, new OverwritePermissions(allow, deny))); + } + + return new ChannelCreateAuditLogData(entry.TargetId.Value, name, type, rateLimitPerUser, nsfw, bitrate, overwrites.ToReadOnlyCollection()); + } + + /// + /// Gets the snowflake ID of the created channel. + /// + /// + /// A representing the snowflake identifier for the created channel. + /// + public ulong ChannelId { get; } + /// + /// Gets the name of the created channel. + /// + /// + /// A string containing the name of the created channel. + /// + public string ChannelName { get; } + /// + /// Gets the type of the created channel. + /// + /// + /// The type of channel that was created. + /// + public ChannelType ChannelType { get; } + /// + /// Gets the current slow-mode delay of the created channel. + /// + /// + /// An representing the time in seconds required before the user can send another + /// message; 0 if disabled. + /// null if this is not mentioned in this entry. + /// + public int? SlowModeInterval { get; } + /// + /// Gets the value that indicates whether the created channel is NSFW. + /// + /// + /// true if the created channel has the NSFW flag enabled; otherwise false. + /// null if this is not mentioned in this entry. + /// + public bool? IsNsfw { get; } + /// + /// Gets the bit-rate that the clients in the created voice channel are requested to use. + /// + /// + /// An representing the bit-rate (bps) that the created voice channel defines and requests the + /// client(s) to use. + /// null if this is not mentioned in this entry. + /// + public int? Bitrate { get; } + /// + /// Gets a collection of permission overwrites that was assigned to the created channel. + /// + /// + /// A collection of permission , containing the permission overwrites that were + /// assigned to the created channel. + /// + public IReadOnlyCollection Overwrites { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs new file mode 100644 index 0000000..3907499 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to a channel deletion. + /// + public class ChannelDeleteAuditLogData : IAuditLogData + { + private ChannelDeleteAuditLogData(ulong id, string name, ChannelType type, int? rateLimit, bool? nsfw, int? bitrate, IReadOnlyCollection overwrites) + { + ChannelId = id; + ChannelName = name; + ChannelType = type; + SlowModeInterval = rateLimit; + IsNsfw = nsfw; + Bitrate = bitrate; + Overwrites = overwrites; + } + + internal static ChannelDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var overwritesModel = changes.FirstOrDefault(x => x.ChangedProperty == "permission_overwrites"); + var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type"); + var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name"); + var rateLimitPerUserModel = changes.FirstOrDefault(x => x.ChangedProperty == "rate_limit_per_user"); + var nsfwModel = changes.FirstOrDefault(x => x.ChangedProperty == "nsfw"); + var bitrateModel = changes.FirstOrDefault(x => x.ChangedProperty == "bitrate"); + + var overwrites = overwritesModel.OldValue.ToObject(discord.ApiClient.Serializer) + .Select(x => new Overwrite(x.TargetId, x.TargetType, new OverwritePermissions(x.Allow, x.Deny))) + .ToList(); + var type = typeModel.OldValue.ToObject(discord.ApiClient.Serializer); + var name = nameModel.OldValue.ToObject(discord.ApiClient.Serializer); + int? rateLimitPerUser = rateLimitPerUserModel?.OldValue?.ToObject(discord.ApiClient.Serializer); + bool? nsfw = nsfwModel?.OldValue?.ToObject(discord.ApiClient.Serializer); + int? bitrate = bitrateModel?.OldValue?.ToObject(discord.ApiClient.Serializer); + var id = entry.TargetId.Value; + + return new ChannelDeleteAuditLogData(id, name, type, rateLimitPerUser, nsfw, bitrate, overwrites.ToReadOnlyCollection()); + } + + /// + /// Gets the snowflake ID of the deleted channel. + /// + /// + /// A representing the snowflake identifier for the deleted channel. + /// + public ulong ChannelId { get; } + /// + /// Gets the name of the deleted channel. + /// + /// + /// A string containing the name of the deleted channel. + /// + public string ChannelName { get; } + /// + /// Gets the type of the deleted channel. + /// + /// + /// The type of channel that was deleted. + /// + public ChannelType ChannelType { get; } + /// + /// Gets the slow-mode delay of the deleted channel. + /// + /// + /// An representing the time in seconds required before the user can send another + /// message; 0 if disabled. + /// null if this is not mentioned in this entry. + /// + public int? SlowModeInterval { get; } + /// + /// Gets the value that indicates whether the deleted channel was NSFW. + /// + /// + /// true if this channel had the NSFW flag enabled; otherwise false. + /// null if this is not mentioned in this entry. + /// + public bool? IsNsfw { get; } + /// + /// Gets the bit-rate of this channel if applicable. + /// + /// + /// An representing the bit-rate set of the voice channel. + /// null if this is not mentioned in this entry. + /// + public int? Bitrate { get; } + /// + /// Gets a collection of permission overwrites that was assigned to the deleted channel. + /// + /// + /// A collection of permission . + /// + public IReadOnlyCollection Overwrites { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs new file mode 100644 index 0000000..d6d2fb4 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs @@ -0,0 +1,57 @@ +namespace Discord.Rest +{ + /// + /// Represents information for a channel. + /// + public struct ChannelInfo + { + internal ChannelInfo(string name, string topic, int? rateLimit, bool? nsfw, int? bitrate) + { + Name = name; + Topic = topic; + SlowModeInterval = rateLimit; + IsNsfw = nsfw; + Bitrate = bitrate; + } + + /// + /// Gets the name of this channel. + /// + /// + /// A string containing the name of this channel. + /// + public string Name { get; } + /// + /// Gets the topic of this channel. + /// + /// + /// A string containing the topic of this channel, if any. + /// + public string Topic { get; } + /// + /// Gets the current slow-mode delay of this channel. + /// + /// + /// An representing the time in seconds required before the user can send another + /// message; 0 if disabled. + /// null if this is not mentioned in this entry. + /// + public int? SlowModeInterval { get; } + /// + /// Gets the value that indicates whether this channel is NSFW. + /// + /// + /// true if this channel has the NSFW flag enabled; otherwise false. + /// null if this is not mentioned in this entry. + /// + public bool? IsNsfw { get; } + /// + /// Gets the bit-rate of this channel if applicable. + /// + /// + /// An representing the bit-rate set for the voice channel; + /// null if this is not mentioned in this entry. + /// + public int? Bitrate { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelUpdateAuditLogData.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelUpdateAuditLogData.cs new file mode 100644 index 0000000..fa52331 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelUpdateAuditLogData.cs @@ -0,0 +1,69 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to a channel update. + /// + public class ChannelUpdateAuditLogData : IAuditLogData + { + private ChannelUpdateAuditLogData(ulong id, ChannelInfo before, ChannelInfo after) + { + ChannelId = id; + Before = before; + After = after; + } + + internal static ChannelUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name"); + var topicModel = changes.FirstOrDefault(x => x.ChangedProperty == "topic"); + var rateLimitPerUserModel = changes.FirstOrDefault(x => x.ChangedProperty == "rate_limit_per_user"); + var nsfwModel = changes.FirstOrDefault(x => x.ChangedProperty == "nsfw"); + var bitrateModel = changes.FirstOrDefault(x => x.ChangedProperty == "bitrate"); + + string oldName = nameModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newName = nameModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + string oldTopic = topicModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newTopic = topicModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + int? oldRateLimitPerUser = rateLimitPerUserModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newRateLimitPerUser = rateLimitPerUserModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + bool? oldNsfw = nsfwModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newNsfw = nsfwModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + int? oldBitrate = bitrateModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newBitrate = bitrateModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + + var before = new ChannelInfo(oldName, oldTopic, oldRateLimitPerUser, oldNsfw, oldBitrate); + var after = new ChannelInfo(newName, newTopic, newRateLimitPerUser, newNsfw, newBitrate); + + return new ChannelUpdateAuditLogData(entry.TargetId.Value, before, after); + } + + /// + /// Gets the snowflake ID of the updated channel. + /// + /// + /// A representing the snowflake identifier for the updated channel. + /// + public ulong ChannelId { get; } + /// + /// Gets the channel information before the changes. + /// + /// + /// An information object containing the original channel information before the changes were made. + /// + public ChannelInfo Before { get; } + /// + /// Gets the channel information after the changes. + /// + /// + /// An information object containing the channel information after the changes were made. + /// + public ChannelInfo After { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteCreateAuditLogData.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteCreateAuditLogData.cs new file mode 100644 index 0000000..92e9257 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteCreateAuditLogData.cs @@ -0,0 +1,42 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to an emoji creation. + /// + public class EmoteCreateAuditLogData : IAuditLogData + { + private EmoteCreateAuditLogData(ulong id, string name) + { + EmoteId = id; + Name = name; + } + + internal static EmoteCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var change = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "name"); + + var emoteName = change.NewValue?.ToObject(discord.ApiClient.Serializer); + return new EmoteCreateAuditLogData(entry.TargetId.Value, emoteName); + } + + /// + /// Gets the snowflake ID of the created emoji. + /// + /// + /// A representing the snowflake identifier for the created emoji. + /// + public ulong EmoteId { get; } + /// + /// Gets the name of the created emoji. + /// + /// + /// A string containing the name of the created emoji. + /// + public string Name { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteDeleteAuditLogData.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteDeleteAuditLogData.cs new file mode 100644 index 0000000..fd307d5 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteDeleteAuditLogData.cs @@ -0,0 +1,43 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to an emoji deletion. + /// + public class EmoteDeleteAuditLogData : IAuditLogData + { + private EmoteDeleteAuditLogData(ulong id, string name) + { + EmoteId = id; + Name = name; + } + + internal static EmoteDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var change = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "name"); + + var emoteName = change.OldValue?.ToObject(discord.ApiClient.Serializer); + + return new EmoteDeleteAuditLogData(entry.TargetId.Value, emoteName); + } + + /// + /// Gets the snowflake ID of the deleted emoji. + /// + /// + /// A representing the snowflake identifier for the deleted emoji. + /// + public ulong EmoteId { get; } + /// + /// Gets the name of the deleted emoji. + /// + /// + /// A string containing the name of the deleted emoji. + /// + public string Name { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteUpdateAuditLogData.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteUpdateAuditLogData.cs new file mode 100644 index 0000000..96e791d --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteUpdateAuditLogData.cs @@ -0,0 +1,52 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to an emoji update. + /// + public class EmoteUpdateAuditLogData : IAuditLogData + { + private EmoteUpdateAuditLogData(ulong id, string oldName, string newName) + { + EmoteId = id; + OldName = oldName; + NewName = newName; + } + + internal static EmoteUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var change = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "name"); + + var newName = change.NewValue?.ToObject(discord.ApiClient.Serializer); + var oldName = change.OldValue?.ToObject(discord.ApiClient.Serializer); + + return new EmoteUpdateAuditLogData(entry.TargetId.Value, oldName, newName); + } + + /// + /// Gets the snowflake ID of the updated emoji. + /// + /// + /// A representing the snowflake identifier of the updated emoji. + /// + public ulong EmoteId { get; } + /// + /// Gets the new name of the updated emoji. + /// + /// + /// A string containing the new name of the updated emoji. + /// + public string NewName { get; } + /// + /// Gets the old name of the updated emoji. + /// + /// + /// A string containing the old name of the updated emoji. + /// + public string OldName { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildInfo.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildInfo.cs new file mode 100644 index 0000000..85c7ac4 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildInfo.cs @@ -0,0 +1,128 @@ +namespace Discord.Rest +{ + /// + /// Represents information for a guild. + /// + public struct GuildInfo + { + internal GuildInfo(int? afkTimeout, DefaultMessageNotifications? defaultNotifs, + ulong? afkChannel, string name, string region, string icon, + VerificationLevel? verification, IUser owner, MfaLevel? mfa, ExplicitContentFilterLevel? filter, + ulong? systemChannel, ulong? widgetChannel, bool? widget) + { + AfkTimeout = afkTimeout; + DefaultMessageNotifications = defaultNotifs; + AfkChannelId = afkChannel; + Name = name; + RegionId = region; + IconHash = icon; + VerificationLevel = verification; + Owner = owner; + MfaLevel = mfa; + ExplicitContentFilter = filter; + SystemChannelId = systemChannel; + EmbedChannelId = widgetChannel; + IsEmbeddable = widget; + } + + /// + /// Gets the amount of time (in seconds) a user must be inactive in a voice channel for until they are + /// automatically moved to the AFK voice channel. + /// + /// + /// An representing the amount of time in seconds for a user to be marked as inactive + /// and moved into the AFK voice channel. + /// null if this is not mentioned in this entry. + /// + public int? AfkTimeout { get; } + /// + /// Gets the default message notifications for users who haven't explicitly set their notification settings. + /// + /// + /// The default message notifications setting of this guild. + /// null if this is not mentioned in this entry. + /// + public DefaultMessageNotifications? DefaultMessageNotifications { get; } + /// + /// Gets the ID of the AFK voice channel for this guild. + /// + /// + /// A representing the snowflake identifier of the AFK voice channel; null if + /// none is set. + /// + public ulong? AfkChannelId { get; } + /// + /// Gets the name of this guild. + /// + /// + /// A string containing the name of this guild. + /// + public string Name { get; } + /// + /// Gets the ID of the region hosting this guild's voice channels. + /// + public string RegionId { get; } + /// + /// Gets the ID of this guild's icon. + /// + /// + /// A string containing the identifier for the splash image; null if none is set. + /// + public string IconHash { get; } + /// + /// Gets the level of requirements a user must fulfill before being allowed to post messages in this guild. + /// + /// + /// The level of requirements. + /// null if this is not mentioned in this entry. + /// + public VerificationLevel? VerificationLevel { get; } + /// + /// Gets the owner of this guild. + /// + /// + /// A user object representing the owner of this guild. + /// + public IUser Owner { get; } + /// + /// Gets the level of Multi-Factor Authentication requirements a user must fulfill before being allowed to + /// perform administrative actions in this guild. + /// + /// + /// The level of MFA requirement. + /// null if this is not mentioned in this entry. + /// + public MfaLevel? MfaLevel { get; } + /// + /// Gets the level of content filtering applied to user's content in a Guild. + /// + /// + /// The level of explicit content filtering. + /// + public ExplicitContentFilterLevel? ExplicitContentFilter { get; } + /// + /// Gets the ID of the channel where system messages are sent. + /// + /// + /// A representing the snowflake identifier of the channel where system + /// messages are sent; null if none is set. + /// + public ulong? SystemChannelId { get; } + /// + /// Gets the ID of the widget embed channel of this guild. + /// + /// + /// A representing the snowflake identifier of the embedded channel found within the + /// widget settings of this guild; null if none is set. + /// + public ulong? EmbedChannelId { get; } + /// + /// Gets a value that indicates whether this guild is embeddable (i.e. can use widget). + /// + /// + /// true if this guild can be embedded via widgets; otherwise false. + /// null if this is not mentioned in this entry. + /// + public bool? IsEmbeddable { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildUpdateAuditLogData.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildUpdateAuditLogData.cs new file mode 100644 index 0000000..80b719a --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildUpdateAuditLogData.cs @@ -0,0 +1,103 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to a guild update. + /// + public class GuildUpdateAuditLogData : IAuditLogData + { + private GuildUpdateAuditLogData(GuildInfo before, GuildInfo after) + { + Before = before; + After = after; + } + + internal static GuildUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var afkTimeoutModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_timeout"); + var defaultMessageNotificationsModel = changes.FirstOrDefault(x => x.ChangedProperty == "default_message_notifications"); + var afkChannelModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_channel_id"); + var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name"); + var regionIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "region"); + var iconHashModel = changes.FirstOrDefault(x => x.ChangedProperty == "icon_hash"); + var verificationLevelModel = changes.FirstOrDefault(x => x.ChangedProperty == "verification_level"); + var ownerIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "owner_id"); + var mfaLevelModel = changes.FirstOrDefault(x => x.ChangedProperty == "mfa_level"); + var contentFilterModel = changes.FirstOrDefault(x => x.ChangedProperty == "explicit_content_filter"); + var systemChannelIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "system_channel_id"); + var widgetChannelIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "widget_channel_id"); + var widgetEnabledModel = changes.FirstOrDefault(x => x.ChangedProperty == "widget_enabled"); + + int? oldAfkTimeout = afkTimeoutModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newAfkTimeout = afkTimeoutModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + DefaultMessageNotifications? oldDefaultMessageNotifications = defaultMessageNotificationsModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newDefaultMessageNotifications = defaultMessageNotificationsModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + ulong? oldAfkChannelId = afkChannelModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newAfkChannelId = afkChannelModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + string oldName = nameModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newName = nameModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + string oldRegionId = regionIdModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newRegionId = regionIdModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + string oldIconHash = iconHashModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newIconHash = iconHashModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + VerificationLevel? oldVerificationLevel = verificationLevelModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newVerificationLevel = verificationLevelModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + ulong? oldOwnerId = ownerIdModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newOwnerId = ownerIdModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + MfaLevel? oldMfaLevel = mfaLevelModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newMfaLevel = mfaLevelModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + ExplicitContentFilterLevel? oldContentFilter = contentFilterModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newContentFilter = contentFilterModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + ulong? oldSystemChannelId = systemChannelIdModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newSystemChannelId = systemChannelIdModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + ulong? oldWidgetChannelId = widgetChannelIdModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newWidgetChannelId = widgetChannelIdModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + bool? oldWidgetEnabled = widgetEnabledModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newWidgetEnabled = widgetEnabledModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + + IUser oldOwner = null; + if (oldOwnerId != null) + { + var oldOwnerInfo = log.Users.FirstOrDefault(x => x.Id == oldOwnerId.Value); + oldOwner = RestUser.Create(discord, oldOwnerInfo); + } + + IUser newOwner = null; + if (newOwnerId != null) + { + var newOwnerInfo = log.Users.FirstOrDefault(x => x.Id == newOwnerId.Value); + newOwner = RestUser.Create(discord, newOwnerInfo); + } + + var before = new GuildInfo(oldAfkTimeout, oldDefaultMessageNotifications, + oldAfkChannelId, oldName, oldRegionId, oldIconHash, oldVerificationLevel, oldOwner, + oldMfaLevel, oldContentFilter, oldSystemChannelId, oldWidgetChannelId, oldWidgetEnabled); + var after = new GuildInfo(newAfkTimeout, newDefaultMessageNotifications, + newAfkChannelId, newName, newRegionId, newIconHash, newVerificationLevel, newOwner, + newMfaLevel, newContentFilter, newSystemChannelId, newWidgetChannelId, newWidgetEnabled); + + return new GuildUpdateAuditLogData(before, after); + } + + /// + /// Gets the guild information before the changes. + /// + /// + /// An information object containing the original guild information before the changes were made. + /// + public GuildInfo Before { get; } + /// + /// Gets the guild information after the changes. + /// + /// + /// An information object containing the guild information after the changes were made. + /// + public GuildInfo After { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs new file mode 100644 index 0000000..215a3c1 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs @@ -0,0 +1,102 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to an invite creation. + /// + public class InviteCreateAuditLogData : IAuditLogData + { + private InviteCreateAuditLogData(int maxAge, string code, bool temporary, IUser inviter, ulong channelId, int uses, int maxUses) + { + MaxAge = maxAge; + Code = code; + Temporary = temporary; + Creator = inviter; + ChannelId = channelId; + Uses = uses; + MaxUses = maxUses; + } + + internal static InviteCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var maxAgeModel = changes.FirstOrDefault(x => x.ChangedProperty == "max_age"); + var codeModel = changes.FirstOrDefault(x => x.ChangedProperty == "code"); + var temporaryModel = changes.FirstOrDefault(x => x.ChangedProperty == "temporary"); + var inviterIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "inviter_id"); + var channelIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "channel_id"); + var usesModel = changes.FirstOrDefault(x => x.ChangedProperty == "uses"); + var maxUsesModel = changes.FirstOrDefault(x => x.ChangedProperty == "max_uses"); + + var maxAge = maxAgeModel.NewValue.ToObject(discord.ApiClient.Serializer); + var code = codeModel.NewValue.ToObject(discord.ApiClient.Serializer); + var temporary = temporaryModel.NewValue.ToObject(discord.ApiClient.Serializer); + var inviterId = inviterIdModel.NewValue.ToObject(discord.ApiClient.Serializer); + var channelId = channelIdModel.NewValue.ToObject(discord.ApiClient.Serializer); + var uses = usesModel.NewValue.ToObject(discord.ApiClient.Serializer); + var maxUses = maxUsesModel.NewValue.ToObject(discord.ApiClient.Serializer); + + var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId); + var inviter = RestUser.Create(discord, inviterInfo); + + return new InviteCreateAuditLogData(maxAge, code, temporary, inviter, channelId, uses, maxUses); + } + + /// + /// Gets the time (in seconds) until the invite expires. + /// + /// + /// An representing the time in seconds until this invite expires. + /// + public int MaxAge { get; } + /// + /// Gets the unique identifier for this invite. + /// + /// + /// A string containing the invite code (e.g. FTqNnyS). + /// + public string Code { get; } + /// + /// Gets a value that determines whether the invite is a temporary one. + /// + /// + /// true if users accepting this invite will be removed from the guild when they log off; otherwise + /// false. + /// + public bool Temporary { get; } + /// + /// Gets the user that created this invite. + /// + /// + /// A user that created this invite. + /// + public IUser Creator { get; } + /// + /// Gets the ID of the channel this invite is linked to. + /// + /// + /// A representing the channel snowflake identifier that the invite points to. + /// + public ulong ChannelId { get; } + /// + /// Gets the number of times this invite has been used. + /// + /// + /// An representing the number of times this invite was used. + /// + public int Uses { get; } + /// + /// Gets the max number of uses this invite may have. + /// + /// + /// An representing the number of uses this invite may be accepted until it is removed + /// from the guild; null if none is set. + /// + public int MaxUses { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs new file mode 100644 index 0000000..5e49bb6 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs @@ -0,0 +1,102 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to an invite removal. + /// + public class InviteDeleteAuditLogData : IAuditLogData + { + private InviteDeleteAuditLogData(int maxAge, string code, bool temporary, IUser inviter, ulong channelId, int uses, int maxUses) + { + MaxAge = maxAge; + Code = code; + Temporary = temporary; + Creator = inviter; + ChannelId = channelId; + Uses = uses; + MaxUses = maxUses; + } + + internal static InviteDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var maxAgeModel = changes.FirstOrDefault(x => x.ChangedProperty == "max_age"); + var codeModel = changes.FirstOrDefault(x => x.ChangedProperty == "code"); + var temporaryModel = changes.FirstOrDefault(x => x.ChangedProperty == "temporary"); + var inviterIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "inviter_id"); + var channelIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "channel_id"); + var usesModel = changes.FirstOrDefault(x => x.ChangedProperty == "uses"); + var maxUsesModel = changes.FirstOrDefault(x => x.ChangedProperty == "max_uses"); + + var maxAge = maxAgeModel.OldValue.ToObject(discord.ApiClient.Serializer); + var code = codeModel.OldValue.ToObject(discord.ApiClient.Serializer); + var temporary = temporaryModel.OldValue.ToObject(discord.ApiClient.Serializer); + var inviterId = inviterIdModel.OldValue.ToObject(discord.ApiClient.Serializer); + var channelId = channelIdModel.OldValue.ToObject(discord.ApiClient.Serializer); + var uses = usesModel.OldValue.ToObject(discord.ApiClient.Serializer); + var maxUses = maxUsesModel.OldValue.ToObject(discord.ApiClient.Serializer); + + var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId); + var inviter = RestUser.Create(discord, inviterInfo); + + return new InviteDeleteAuditLogData(maxAge, code, temporary, inviter, channelId, uses, maxUses); + } + + /// + /// Gets the time (in seconds) until the invite expires. + /// + /// + /// An representing the time in seconds until this invite expires. + /// + public int MaxAge { get; } + /// + /// Gets the unique identifier for this invite. + /// + /// + /// A string containing the invite code (e.g. FTqNnyS). + /// + public string Code { get; } + /// + /// Gets a value that indicates whether the invite is a temporary one. + /// + /// + /// true if users accepting this invite will be removed from the guild when they log off; otherwise + /// false. + /// + public bool Temporary { get; } + /// + /// Gets the user that created this invite. + /// + /// + /// A user that created this invite. + /// + public IUser Creator { get; } + /// + /// Gets the ID of the channel this invite is linked to. + /// + /// + /// A representing the channel snowflake identifier that the invite points to. + /// + public ulong ChannelId { get; } + /// + /// Gets the number of times this invite has been used. + /// + /// + /// An representing the number of times this invite has been used. + /// + public int Uses { get; } + /// + /// Gets the max number of uses this invite may have. + /// + /// + /// An representing the number of uses this invite may be accepted until it is removed + /// from the guild; null if none is set. + /// + public int MaxUses { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteInfo.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteInfo.cs new file mode 100644 index 0000000..aaad362 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteInfo.cs @@ -0,0 +1,57 @@ +namespace Discord.Rest +{ + /// + /// Represents information for an invite. + /// + public struct InviteInfo + { + internal InviteInfo(int? maxAge, string code, bool? temporary, ulong? channelId, int? maxUses) + { + MaxAge = maxAge; + Code = code; + Temporary = temporary; + ChannelId = channelId; + MaxUses = maxUses; + } + + /// + /// Gets the time (in seconds) until the invite expires. + /// + /// + /// An representing the time in seconds until this invite expires; null if this + /// invite never expires or not specified. + /// + public int? MaxAge { get; } + /// + /// Gets the unique identifier for this invite. + /// + /// + /// A string containing the invite code (e.g. FTqNnyS). + /// + public string Code { get; } + /// + /// Gets a value that indicates whether the invite is a temporary one. + /// + /// + /// true if users accepting this invite will be removed from the guild when they log off, + /// false if not; null if not specified. + /// + public bool? Temporary { get; } + /// + /// Gets the ID of the channel this invite is linked to. + /// + /// + /// A representing the channel snowflake identifier that the invite points to; + /// null if not specified. + /// + public ulong? ChannelId { get; } + /// + /// Gets the max number of uses this invite may have. + /// + /// + /// An representing the number of uses this invite may be accepted until it is removed + /// from the guild; null if none is specified. + /// + public int? MaxUses { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteUpdateAuditLogData.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteUpdateAuditLogData.cs new file mode 100644 index 0000000..95bfb84 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteUpdateAuditLogData.cs @@ -0,0 +1,61 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data relating to an invite update. + /// + public class InviteUpdateAuditLogData : IAuditLogData + { + private InviteUpdateAuditLogData(InviteInfo before, InviteInfo after) + { + Before = before; + After = after; + } + + internal static InviteUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var maxAgeModel = changes.FirstOrDefault(x => x.ChangedProperty == "max_age"); + var codeModel = changes.FirstOrDefault(x => x.ChangedProperty == "code"); + var temporaryModel = changes.FirstOrDefault(x => x.ChangedProperty == "temporary"); + var channelIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "channel_id"); + var maxUsesModel = changes.FirstOrDefault(x => x.ChangedProperty == "max_uses"); + + int? oldMaxAge = maxAgeModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newMaxAge = maxAgeModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + string oldCode = codeModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newCode = codeModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + bool? oldTemporary = temporaryModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newTemporary = temporaryModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + ulong? oldChannelId = channelIdModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newChannelId = channelIdModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + int? oldMaxUses = maxUsesModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newMaxUses = maxUsesModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + + var before = new InviteInfo(oldMaxAge, oldCode, oldTemporary, oldChannelId, oldMaxUses); + var after = new InviteInfo(newMaxAge, newCode, newTemporary, newChannelId, newMaxUses); + + return new InviteUpdateAuditLogData(before, after); + } + + /// + /// Gets the invite information before the changes. + /// + /// + /// An information object containing the original invite information before the changes were made. + /// + public InviteInfo Before { get; } + /// + /// Gets the invite information after the changes. + /// + /// + /// An information object containing the invite information after the changes were made. + /// + public InviteInfo After { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/KickAuditLogData.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/KickAuditLogData.cs new file mode 100644 index 0000000..dceb73d --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/KickAuditLogData.cs @@ -0,0 +1,32 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to a kick. + /// + public class KickAuditLogData : IAuditLogData + { + private KickAuditLogData(RestUser user) + { + Target = user; + } + + internal static KickAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); + return new KickAuditLogData(RestUser.Create(discord, userInfo)); + } + + /// + /// Gets the user that was kicked. + /// + /// + /// A user object representing the kicked user. + /// + public IUser Target { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberInfo.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberInfo.cs new file mode 100644 index 0000000..ffa316f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberInfo.cs @@ -0,0 +1,41 @@ +namespace Discord.Rest +{ + /// + /// Represents information for a member. + /// + public struct MemberInfo + { + internal MemberInfo(string nick, bool? deaf, bool? mute) + { + Nickname = nick; + Deaf = deaf; + Mute = mute; + } + + /// + /// Gets the nickname of the updated member. + /// + /// + /// A string representing the nickname of the updated member; null if none is set. + /// + public string Nickname { get; } + /// + /// Gets a value that indicates whether the updated member is deafened by the guild. + /// + /// + /// true if the updated member is deafened (i.e. not permitted to listen to or speak to others) by the guild; + /// otherwise false. + /// null if this is not mentioned in this entry. + /// + public bool? Deaf { get; } + /// + /// Gets a value that indicates whether the updated member is muted (i.e. not permitted to speak via voice) by the + /// guild. + /// + /// + /// true if the updated member is muted by the guild; otherwise false. + /// null if this is not mentioned in this entry. + /// + public bool? Mute { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs new file mode 100644 index 0000000..763c90c --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to a change in a guild member's roles. + /// + public class MemberRoleAuditLogData : IAuditLogData + { + private MemberRoleAuditLogData(IReadOnlyCollection roles, IUser target) + { + Roles = roles; + Target = target; + } + + internal static MemberRoleAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var roleInfos = changes.SelectMany(x => x.NewValue.ToObject(discord.ApiClient.Serializer), + (model, role) => new { model.ChangedProperty, Role = role }) + .Select(x => new MemberRoleEditInfo(x.Role.Name, x.Role.Id, x.ChangedProperty == "$add")) + .ToList(); + + var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); + var user = RestUser.Create(discord, userInfo); + + return new MemberRoleAuditLogData(roleInfos.ToReadOnlyCollection(), user); + } + + /// + /// Gets a collection of role changes that were performed on the member. + /// + /// + /// A read-only collection of , containing the roles that were changed on + /// the member. + /// + public IReadOnlyCollection Roles { get; } + /// + /// Gets the user that the roles changes were performed on. + /// + /// + /// A user object representing the user that the role changes were performed on. + /// + public IUser Target { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleEditInfo.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleEditInfo.cs new file mode 100644 index 0000000..b0abf2d --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleEditInfo.cs @@ -0,0 +1,37 @@ +namespace Discord.Rest +{ + /// + /// An information object representing a change in one of a guild member's roles. + /// + public struct MemberRoleEditInfo + { + internal MemberRoleEditInfo(string name, ulong roleId, bool added) + { + Name = name; + RoleId = roleId; + Added = added; + } + + /// + /// Gets the name of the role that was changed. + /// + /// + /// A string containing the name of the role that was changed. + /// + public string Name { get; } + /// + /// Gets the ID of the role that was changed. + /// + /// + /// A representing the snowflake identifier of the role that was changed. + /// + public ulong RoleId { get; } + /// + /// Gets a value that indicates whether the role was added to the user. + /// + /// + /// true if the role was added to the user; otherwise false. + /// + public bool Added { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs new file mode 100644 index 0000000..f22b83e --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs @@ -0,0 +1,66 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to a change in a guild member. + /// + public class MemberUpdateAuditLogData : IAuditLogData + { + private MemberUpdateAuditLogData(IUser target, MemberInfo before, MemberInfo after) + { + Target = target; + Before = before; + After = after; + } + + internal static MemberUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var nickModel = changes.FirstOrDefault(x => x.ChangedProperty == "nick"); + var deafModel = changes.FirstOrDefault(x => x.ChangedProperty == "deaf"); + var muteModel = changes.FirstOrDefault(x => x.ChangedProperty == "mute"); + + string oldNick = nickModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newNick = nickModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + bool? oldDeaf = deafModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newDeaf = deafModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + bool? oldMute = muteModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newMute = muteModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + + var targetInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); + var user = RestUser.Create(discord, targetInfo); + + var before = new MemberInfo(oldNick, oldDeaf, oldMute); + var after = new MemberInfo(newNick, newDeaf, newMute); + + return new MemberUpdateAuditLogData(user, before, after); + } + + /// + /// Gets the user that the changes were performed on. + /// + /// + /// A user object representing the user who the changes were performed on. + /// + public IUser Target { get; } + /// + /// Gets the member information before the changes. + /// + /// + /// An information object containing the original member information before the changes were made. + /// + public MemberInfo Before { get; } + /// + /// Gets the member information after the changes. + /// + /// + /// An information object containing the member information after the changes were made. + /// + public MemberInfo After { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs new file mode 100644 index 0000000..c6b2e10 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs @@ -0,0 +1,46 @@ +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to message deletion(s). + /// + public class MessageDeleteAuditLogData : IAuditLogData + { + private MessageDeleteAuditLogData(ulong channelId, int count, ulong authorId) + { + ChannelId = channelId; + MessageCount = count; + AuthorId = authorId; + } + + internal static MessageDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + return new MessageDeleteAuditLogData(entry.Options.MessageDeleteChannelId.Value, entry.Options.MessageDeleteCount.Value, entry.TargetId.Value); + } + + /// + /// Gets the number of messages that were deleted. + /// + /// + /// An representing the number of messages that were deleted from the channel. + /// + public int MessageCount { get; } + /// + /// Gets the ID of the channel that the messages were deleted from. + /// + /// + /// A representing the snowflake identifier for the channel that the messages were + /// deleted from. + /// + public ulong ChannelId { get; } + /// + /// Gets the author of the messages that were deleted. + /// + /// + /// A representing the snowflake identifier for the user that created the deleted messages. + /// + public ulong AuthorId { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteCreateAuditLogData.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteCreateAuditLogData.cs new file mode 100644 index 0000000..3f39118 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteCreateAuditLogData.cs @@ -0,0 +1,53 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data for a permissions overwrite creation. + /// + public class OverwriteCreateAuditLogData : IAuditLogData + { + private OverwriteCreateAuditLogData(ulong channelId, Overwrite overwrite) + { + ChannelId = channelId; + Overwrite = overwrite; + } + + internal static OverwriteCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var denyModel = changes.FirstOrDefault(x => x.ChangedProperty == "deny"); + var allowModel = changes.FirstOrDefault(x => x.ChangedProperty == "allow"); + + var deny = denyModel.NewValue.ToObject(discord.ApiClient.Serializer); + var allow = allowModel.NewValue.ToObject(discord.ApiClient.Serializer); + + var permissions = new OverwritePermissions(allow, deny); + + var id = entry.Options.OverwriteTargetId.Value; + var type = entry.Options.OverwriteType; + + return new OverwriteCreateAuditLogData(entry.TargetId.Value, new Overwrite(id, type, permissions)); + } + + /// + /// Gets the ID of the channel that the overwrite was created from. + /// + /// + /// A representing the snowflake identifier for the channel that the overwrite was + /// created from. + /// + public ulong ChannelId { get; } + /// + /// Gets the permission overwrite object that was created. + /// + /// + /// An object representing the overwrite that was created. + /// + public Overwrite Overwrite { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs new file mode 100644 index 0000000..a193e76 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs @@ -0,0 +1,52 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to the deletion of a permission overwrite. + /// + public class OverwriteDeleteAuditLogData : IAuditLogData + { + private OverwriteDeleteAuditLogData(ulong channelId, Overwrite deletedOverwrite) + { + ChannelId = channelId; + Overwrite = deletedOverwrite; + } + + internal static OverwriteDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var denyModel = changes.FirstOrDefault(x => x.ChangedProperty == "deny"); + var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type"); + var idModel = changes.FirstOrDefault(x => x.ChangedProperty == "id"); + var allowModel = changes.FirstOrDefault(x => x.ChangedProperty == "allow"); + + var deny = denyModel.OldValue.ToObject(discord.ApiClient.Serializer); + var type = typeModel.OldValue.ToObject(discord.ApiClient.Serializer); + var id = idModel.OldValue.ToObject(discord.ApiClient.Serializer); + var allow = allowModel.OldValue.ToObject(discord.ApiClient.Serializer); + + return new OverwriteDeleteAuditLogData(entry.TargetId.Value, new Overwrite(id, type, new OverwritePermissions(allow, deny))); + } + + /// + /// Gets the ID of the channel that the overwrite was deleted from. + /// + /// + /// A representing the snowflake identifier for the channel that the overwrite was + /// deleted from. + /// + public ulong ChannelId { get; } + /// + /// Gets the permission overwrite object that was deleted. + /// + /// + /// An object representing the overwrite that was deleted. + /// + public Overwrite Overwrite { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteUpdateAuditLogData.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteUpdateAuditLogData.cs new file mode 100644 index 0000000..c2b8d42 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteUpdateAuditLogData.cs @@ -0,0 +1,81 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to the update of a permission overwrite. + /// + public class OverwriteUpdateAuditLogData : IAuditLogData + { + private OverwriteUpdateAuditLogData(ulong channelId, OverwritePermissions before, OverwritePermissions after, ulong targetId, PermissionTarget targetType) + { + ChannelId = channelId; + OldPermissions = before; + NewPermissions = after; + OverwriteTargetId = targetId; + OverwriteType = targetType; + } + + internal static OverwriteUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var denyModel = changes.FirstOrDefault(x => x.ChangedProperty == "deny"); + var allowModel = changes.FirstOrDefault(x => x.ChangedProperty == "allow"); + + var beforeAllow = allowModel?.OldValue?.ToObject(discord.ApiClient.Serializer); + var afterAllow = allowModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + var beforeDeny = denyModel?.OldValue?.ToObject(discord.ApiClient.Serializer); + var afterDeny = denyModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + + var beforePermissions = new OverwritePermissions(beforeAllow ?? 0, beforeDeny ?? 0); + var afterPermissions = new OverwritePermissions(afterAllow ?? 0, afterDeny ?? 0); + + var type = entry.Options.OverwriteType; + + return new OverwriteUpdateAuditLogData(entry.TargetId.Value, beforePermissions, afterPermissions, entry.Options.OverwriteTargetId.Value, type); + } + + /// + /// Gets the ID of the channel that the overwrite was updated from. + /// + /// + /// A representing the snowflake identifier for the channel that the overwrite was + /// updated from. + /// + public ulong ChannelId { get; } + /// + /// Gets the overwrite permissions before the changes. + /// + /// + /// An overwrite permissions object representing the overwrite permissions that the overwrite had before + /// the changes were made. + /// + public OverwritePermissions OldPermissions { get; } + /// + /// Gets the overwrite permissions after the changes. + /// + /// + /// An overwrite permissions object representing the overwrite permissions that the overwrite had after the + /// changes. + /// + public OverwritePermissions NewPermissions { get; } + /// + /// Gets the ID of the overwrite that was updated. + /// + /// + /// A representing the snowflake identifier of the overwrite that was updated. + /// + public ulong OverwriteTargetId { get; } + /// + /// Gets the target of the updated permission overwrite. + /// + /// + /// The target of the updated permission overwrite. + /// + public PermissionTarget OverwriteType { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/PruneAuditLogData.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/PruneAuditLogData.cs new file mode 100644 index 0000000..c32d12b --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/PruneAuditLogData.cs @@ -0,0 +1,40 @@ +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to a guild prune. + /// + public class PruneAuditLogData : IAuditLogData + { + private PruneAuditLogData(int pruneDays, int membersRemoved) + { + PruneDays = pruneDays; + MembersRemoved = membersRemoved; + } + + internal static PruneAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + return new PruneAuditLogData(entry.Options.PruneDeleteMemberDays.Value, entry.Options.PruneMembersRemoved.Value); + } + + /// + /// Gets the threshold for a guild member to not be kicked. + /// + /// + /// An representing the amount of days that a member must have been seen in the server, + /// to avoid being kicked. (i.e. If a user has not been seen for more than , they will be + /// kicked from the server) + /// + public int PruneDays { get; } + /// + /// Gets the number of members that were kicked during the purge. + /// + /// + /// An representing the number of members that were removed from this guild for having + /// not been seen within . + /// + public int MembersRemoved { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleCreateAuditLogData.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleCreateAuditLogData.cs new file mode 100644 index 0000000..cee255f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleCreateAuditLogData.cs @@ -0,0 +1,62 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to a role creation. + /// + public class RoleCreateAuditLogData : IAuditLogData + { + private RoleCreateAuditLogData(ulong id, RoleEditInfo props) + { + RoleId = id; + Properties = props; + } + + internal static RoleCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var colorModel = changes.FirstOrDefault(x => x.ChangedProperty == "color"); + var mentionableModel = changes.FirstOrDefault(x => x.ChangedProperty == "mentionable"); + var hoistModel = changes.FirstOrDefault(x => x.ChangedProperty == "hoist"); + var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name"); + var permissionsModel = changes.FirstOrDefault(x => x.ChangedProperty == "permissions"); + + uint? colorRaw = colorModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + bool? mentionable = mentionableModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + bool? hoist = hoistModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + string name = nameModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + ulong? permissionsRaw = permissionsModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + + Color? color = null; + GuildPermissions? permissions = null; + + if (colorRaw.HasValue) + color = new Color(colorRaw.Value); + if (permissionsRaw.HasValue) + permissions = new GuildPermissions(permissionsRaw.Value); + + return new RoleCreateAuditLogData(entry.TargetId.Value, + new RoleEditInfo(color, mentionable, hoist, name, permissions)); + } + + /// + /// Gets the ID of the role that was created. + /// + /// + /// A representing the snowflake identifier to the role that was created. + /// + public ulong RoleId { get; } + /// + /// Gets the role information that was created. + /// + /// + /// An information object representing the properties of the role that was created. + /// + public RoleEditInfo Properties { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleDeleteAuditLogData.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleDeleteAuditLogData.cs new file mode 100644 index 0000000..78b5efc --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleDeleteAuditLogData.cs @@ -0,0 +1,62 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data relating to a role deletion. + /// + public class RoleDeleteAuditLogData : IAuditLogData + { + private RoleDeleteAuditLogData(ulong id, RoleEditInfo props) + { + RoleId = id; + Properties = props; + } + + internal static RoleDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var colorModel = changes.FirstOrDefault(x => x.ChangedProperty == "color"); + var mentionableModel = changes.FirstOrDefault(x => x.ChangedProperty == "mentionable"); + var hoistModel = changes.FirstOrDefault(x => x.ChangedProperty == "hoist"); + var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name"); + var permissionsModel = changes.FirstOrDefault(x => x.ChangedProperty == "permissions"); + + uint? colorRaw = colorModel?.OldValue?.ToObject(discord.ApiClient.Serializer); + bool? mentionable = mentionableModel?.OldValue?.ToObject(discord.ApiClient.Serializer); + bool? hoist = hoistModel?.OldValue?.ToObject(discord.ApiClient.Serializer); + string name = nameModel?.OldValue?.ToObject(discord.ApiClient.Serializer); + ulong? permissionsRaw = permissionsModel?.OldValue?.ToObject(discord.ApiClient.Serializer); + + Color? color = null; + GuildPermissions? permissions = null; + + if (colorRaw.HasValue) + color = new Color(colorRaw.Value); + if (permissionsRaw.HasValue) + permissions = new GuildPermissions(permissionsRaw.Value); + + return new RoleDeleteAuditLogData(entry.TargetId.Value, + new RoleEditInfo(color, mentionable, hoist, name, permissions)); + } + + /// + /// Gets the ID of the role that was deleted. + /// + /// + /// A representing the snowflake identifier to the role that was deleted. + /// + public ulong RoleId { get; } + /// + /// Gets the role information that was deleted. + /// + /// + /// An information object representing the properties of the role that was deleted. + /// + public RoleEditInfo Properties { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleEditInfo.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleEditInfo.cs new file mode 100644 index 0000000..6f3d8d3 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleEditInfo.cs @@ -0,0 +1,59 @@ +namespace Discord.Rest +{ + /// + /// Represents information for a role edit. + /// + public struct RoleEditInfo + { + internal RoleEditInfo(Color? color, bool? mentionable, bool? hoist, string name, + GuildPermissions? permissions) + { + Color = color; + Mentionable = mentionable; + Hoist = hoist; + Name = name; + Permissions = permissions; + } + + /// + /// Gets the color of this role. + /// + /// + /// A color object representing the color assigned to this role; null if this role does not have a + /// color. + /// + public Color? Color { get; } + /// + /// Gets a value that indicates whether this role is mentionable. + /// + /// + /// true if other members can mention this role in a text channel; otherwise false; + /// null if this is not mentioned in this entry. + /// + public bool? Mentionable { get; } + /// + /// Gets a value that indicates whether this role is hoisted (i.e. its members will appear in a separate + /// section on the user list). + /// + /// + /// true if this role's members will appear in a separate section in the user list; otherwise + /// false; null if this is not mentioned in this entry. + /// + public bool? Hoist { get; } + /// + /// Gets the name of this role. + /// + /// + /// A string containing the name of this role. + /// + public string Name { get; } + /// + /// Gets the permissions assigned to this role. + /// + /// + /// A guild permissions object representing the permissions that have been assigned to this role; null + /// if no permissions have been assigned. + /// + public GuildPermissions? Permissions { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleUpdateAuditLogData.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleUpdateAuditLogData.cs new file mode 100644 index 0000000..094e1e0 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleUpdateAuditLogData.cs @@ -0,0 +1,83 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to a role update. + /// + public class RoleUpdateAuditLogData : IAuditLogData + { + private RoleUpdateAuditLogData(ulong id, RoleEditInfo oldProps, RoleEditInfo newProps) + { + RoleId = id; + Before = oldProps; + After = newProps; + } + + internal static RoleUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var colorModel = changes.FirstOrDefault(x => x.ChangedProperty == "color"); + var mentionableModel = changes.FirstOrDefault(x => x.ChangedProperty == "mentionable"); + var hoistModel = changes.FirstOrDefault(x => x.ChangedProperty == "hoist"); + var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name"); + var permissionsModel = changes.FirstOrDefault(x => x.ChangedProperty == "permissions"); + + uint? oldColorRaw = colorModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newColorRaw = colorModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + bool? oldMentionable = mentionableModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newMentionable = mentionableModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + bool? oldHoist = hoistModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newHoist = hoistModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + string oldName = nameModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newName = nameModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + ulong? oldPermissionsRaw = permissionsModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newPermissionsRaw = permissionsModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + + Color? oldColor = null, + newColor = null; + GuildPermissions? oldPermissions = null, + newPermissions = null; + + if (oldColorRaw.HasValue) + oldColor = new Color(oldColorRaw.Value); + if (newColorRaw.HasValue) + newColor = new Color(newColorRaw.Value); + if (oldPermissionsRaw.HasValue) + oldPermissions = new GuildPermissions(oldPermissionsRaw.Value); + if (newPermissionsRaw.HasValue) + newPermissions = new GuildPermissions(newPermissionsRaw.Value); + + var oldProps = new RoleEditInfo(oldColor, oldMentionable, oldHoist, oldName, oldPermissions); + var newProps = new RoleEditInfo(newColor, newMentionable, newHoist, newName, newPermissions); + + return new RoleUpdateAuditLogData(entry.TargetId.Value, oldProps, newProps); + } + + /// + /// Gets the ID of the role that was changed. + /// + /// + /// A representing the snowflake identifier of the role that was changed. + /// + public ulong RoleId { get; } + /// + /// Gets the role information before the changes. + /// + /// + /// A role information object containing the role information before the changes were made. + /// + public RoleEditInfo Before { get; } + /// + /// Gets the role information after the changes. + /// + /// + /// A role information object containing the role information after the changes were made. + /// + public RoleEditInfo After { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/UnbanAuditLogData.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/UnbanAuditLogData.cs new file mode 100644 index 0000000..bc7e7fd --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/UnbanAuditLogData.cs @@ -0,0 +1,32 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to an unban. + /// + public class UnbanAuditLogData : IAuditLogData + { + private UnbanAuditLogData(IUser user) + { + Target = user; + } + + internal static UnbanAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); + return new UnbanAuditLogData(RestUser.Create(discord, userInfo)); + } + + /// + /// Gets the user that was unbanned. + /// + /// + /// A user object representing the user that was unbanned. + /// + public IUser Target { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookCreateAuditLogData.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookCreateAuditLogData.cs new file mode 100644 index 0000000..81d902f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookCreateAuditLogData.cs @@ -0,0 +1,84 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to a webhook creation. + /// + public class WebhookCreateAuditLogData : IAuditLogData + { + private WebhookCreateAuditLogData(IWebhook webhook, ulong webhookId, WebhookType type, string name, ulong channelId) + { + Webhook = webhook; + WebhookId = webhookId; + Name = name; + Type = type; + ChannelId = channelId; + } + + internal static WebhookCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var channelIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "channel_id"); + var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type"); + var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name"); + + var channelId = channelIdModel.NewValue.ToObject(discord.ApiClient.Serializer); + var type = typeModel.NewValue.ToObject(discord.ApiClient.Serializer); + var name = nameModel.NewValue.ToObject(discord.ApiClient.Serializer); + + var webhookInfo = log.Webhooks?.FirstOrDefault(x => x.Id == entry.TargetId); + var webhook = webhookInfo == null ? null : RestWebhook.Create(discord, (IGuild)null, webhookInfo); + + return new WebhookCreateAuditLogData(webhook, entry.TargetId.Value, type, name, channelId); + } + + // Doc Note: Corresponds to the *current* data + + /// + /// Gets the webhook that was created if it still exists. + /// + /// + /// A webhook object representing the webhook that was created if it still exists, otherwise returns null. + /// + public IWebhook Webhook { get; } + + // Doc Note: Corresponds to the *audit log* data + + /// + /// Gets the webhook id. + /// + /// + /// The webhook identifier. + /// + public ulong WebhookId { get; } + + /// + /// Gets the type of webhook that was created. + /// + /// + /// The type of webhook that was created. + /// + public WebhookType Type { get; } + + /// + /// Gets the name of the webhook. + /// + /// + /// A string containing the name of the webhook. + /// + public string Name { get; } + /// + /// Gets the ID of the channel that the webhook could send to. + /// + /// + /// A representing the snowflake identifier of the channel that the webhook could send + /// to. + /// + public ulong ChannelId { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookDeleteAuditLogData.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookDeleteAuditLogData.cs new file mode 100644 index 0000000..308020c --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookDeleteAuditLogData.cs @@ -0,0 +1,76 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to a webhook deletion. + /// + public class WebhookDeleteAuditLogData : IAuditLogData + { + private WebhookDeleteAuditLogData(ulong id, ulong channel, WebhookType type, string name, string avatar) + { + WebhookId = id; + ChannelId = channel; + Name = name; + Type = type; + Avatar = avatar; + } + + internal static WebhookDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var channelIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "channel_id"); + var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type"); + var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name"); + var avatarHashModel = changes.FirstOrDefault(x => x.ChangedProperty == "avatar_hash"); + + var channelId = channelIdModel.OldValue.ToObject(discord.ApiClient.Serializer); + var type = typeModel.OldValue.ToObject(discord.ApiClient.Serializer); + var name = nameModel.OldValue.ToObject(discord.ApiClient.Serializer); + var avatarHash = avatarHashModel?.OldValue?.ToObject(discord.ApiClient.Serializer); + + return new WebhookDeleteAuditLogData(entry.TargetId.Value, channelId, type, name, avatarHash); + } + + /// + /// Gets the ID of the webhook that was deleted. + /// + /// + /// A representing the snowflake identifier of the webhook that was deleted. + /// + public ulong WebhookId { get; } + /// + /// Gets the ID of the channel that the webhook could send to. + /// + /// + /// A representing the snowflake identifier of the channel that the webhook could send + /// to. + /// + public ulong ChannelId { get; } + /// + /// Gets the type of the webhook that was deleted. + /// + /// + /// The type of webhook that was deleted. + /// + public WebhookType Type { get; } + /// + /// Gets the name of the webhook that was deleted. + /// + /// + /// A string containing the name of the webhook that was deleted. + /// + public string Name { get; } + /// + /// Gets the hash value of the webhook's avatar. + /// + /// + /// A string containing the hash of the webhook's avatar. + /// + public string Avatar { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookInfo.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookInfo.cs new file mode 100644 index 0000000..e60157b --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookInfo.cs @@ -0,0 +1,38 @@ +namespace Discord.Rest +{ + /// + /// Represents information for a webhook. + /// + public struct WebhookInfo + { + internal WebhookInfo(string name, ulong? channelId, string avatar) + { + Name = name; + ChannelId = channelId; + Avatar = avatar; + } + + /// + /// Gets the name of this webhook. + /// + /// + /// A string containing the name of this webhook. + /// + public string Name { get; } + /// + /// Gets the ID of the channel that this webhook sends to. + /// + /// + /// A representing the snowflake identifier of the channel that this webhook can send + /// to. + /// + public ulong? ChannelId { get; } + /// + /// Gets the hash value of this webhook's avatar. + /// + /// + /// A string containing the hash of this webhook's avatar. + /// + public string Avatar { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookUpdateAuditLogData.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookUpdateAuditLogData.cs new file mode 100644 index 0000000..18fe865 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookUpdateAuditLogData.cs @@ -0,0 +1,68 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to a webhook update. + /// + public class WebhookUpdateAuditLogData : IAuditLogData + { + private WebhookUpdateAuditLogData(IWebhook webhook, WebhookInfo before, WebhookInfo after) + { + Webhook = webhook; + Before = before; + After = after; + } + + internal static WebhookUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name"); + var channelIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "channel_id"); + var avatarHashModel = changes.FirstOrDefault(x => x.ChangedProperty == "avatar_hash"); + + var oldName = nameModel?.OldValue?.ToObject(discord.ApiClient.Serializer); + var oldChannelId = channelIdModel?.OldValue?.ToObject(discord.ApiClient.Serializer); + var oldAvatar = avatarHashModel?.OldValue?.ToObject(discord.ApiClient.Serializer); + var before = new WebhookInfo(oldName, oldChannelId, oldAvatar); + + var newName = nameModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + var newChannelId = channelIdModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + var newAvatar = avatarHashModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + var after = new WebhookInfo(newName, newChannelId, newAvatar); + + var webhookInfo = log.Webhooks?.FirstOrDefault(x => x.Id == entry.TargetId); + var webhook = webhookInfo != null ? RestWebhook.Create(discord, (IGuild)null, webhookInfo) : null; + + return new WebhookUpdateAuditLogData(webhook, before, after); + } + + /// + /// Gets the webhook that was updated. + /// + /// + /// A webhook object representing the webhook that was updated. + /// + public IWebhook Webhook { get; } + + /// + /// Gets the webhook information before the changes. + /// + /// + /// A webhook information object representing the webhook before the changes were made. + /// + public WebhookInfo Before { get; } + + /// + /// Gets the webhook information after the changes. + /// + /// + /// A webhook information object representing the webhook after the changes were made. + /// + public WebhookInfo After { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/RestAuditLogEntry.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/RestAuditLogEntry.cs new file mode 100644 index 0000000..d604077 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/RestAuditLogEntry.cs @@ -0,0 +1,44 @@ +using System; +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Represents a REST-based audit log entry. + /// + public class RestAuditLogEntry : RestEntity, IAuditLogEntry + { + private RestAuditLogEntry(BaseDiscordClient discord, Model fullLog, EntryModel model, IUser user) + : base(discord, model.Id) + { + Action = model.Action; + Data = AuditLogHelper.CreateData(discord, fullLog, model); + User = user; + Reason = model.Reason; + } + + internal static RestAuditLogEntry Create(BaseDiscordClient discord, Model fullLog, EntryModel model) + { + var userInfo = fullLog.Users.FirstOrDefault(x => x.Id == model.UserId); + IUser user = null; + if (userInfo != null) + user = RestUser.Create(discord, userInfo); + + return new RestAuditLogEntry(discord, fullLog, model, user); + } + + /// + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + /// + public ActionType Action { get; } + /// + public IAuditLogData Data { get; } + /// + public IUser User { get; } + /// + public string Reason { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs new file mode 100644 index 0000000..5fb150c --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -0,0 +1,392 @@ +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; +using UserModel = Discord.API.User; + +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 GuildChannelProperties(); + func(args); + var apiArgs = new API.Rest.ModifyGuildChannelParams + { + Name = args.Name, + Position = args.Position, + CategoryId = args.CategoryId + }; + return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); + } + public static async Task ModifyAsync(ITextChannel channel, BaseDiscordClient client, + Action func, + RequestOptions options) + { + var args = new TextChannelProperties(); + func(args); + var apiArgs = new API.Rest.ModifyTextChannelParams + { + Name = args.Name, + Position = args.Position, + CategoryId = args.CategoryId, + Topic = args.Topic, + IsNsfw = args.IsNsfw, + SlowModeInterval = args.SlowModeInterval, + }; + return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); + } + public static async Task ModifyAsync(IVoiceChannel channel, BaseDiscordClient client, + Action func, + RequestOptions options) + { + var args = new VoiceChannelProperties(); + func(args); + var apiArgs = new API.Rest.ModifyVoiceChannelParams + { + Bitrate = args.Bitrate, + Name = args.Name, + Position = args.Position, + CategoryId = args.CategoryId, + UserLimit = args.UserLimit.IsSpecified ? (args.UserLimit.Value ?? 0) : Optional.Create() + }; + return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); + } + + //Invites + public static async Task> GetInvitesAsync(IGuildChannel channel, BaseDiscordClient client, + RequestOptions options) + { + var models = await client.ApiClient.GetChannelInvitesAsync(channel.Id, options).ConfigureAwait(false); + return models.Select(x => RestInviteMetadata.Create(client, null, channel, x)).ToImmutableArray(); + } + /// + /// may not be equal to zero. + /// -and- + /// and must be greater than zero. + /// -and- + /// must be lesser than 86400. + /// + public static async Task CreateInviteAsync(IGuildChannel channel, BaseDiscordClient client, + int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) + { + var args = new API.Rest.CreateChannelInviteParams + { + IsTemporary = isTemporary, + IsUnique = isUnique, + MaxAge = maxAge ?? 0, + MaxUses = maxUses ?? 0 + }; + var model = await client.ApiClient.CreateChannelInviteAsync(channel.Id, args, options).ConfigureAwait(false); + return RestInviteMetadata.Create(client, null, channel, model); + } + + //Messages + public static async Task GetMessageAsync(IMessageChannel channel, BaseDiscordClient client, + ulong id, RequestOptions options) + { + var guildId = (channel as IGuildChannel)?.GuildId; + var guild = guildId != null ? await (client as IDiscordClient).GetGuildAsync(guildId.Value, CacheMode.CacheOnly).ConfigureAwait(false) : null; + var model = await client.ApiClient.GetChannelMessageAsync(channel.Id, id, options).ConfigureAwait(false); + if (model == null) + return null; + var author = GetAuthor(client, guild, model.Author.Value, model.WebhookId.ToNullable()); + return RestMessage.Create(client, channel, author, model); + } + public static IAsyncEnumerable> GetMessagesAsync(IMessageChannel channel, BaseDiscordClient client, + ulong? fromMessageId, Direction dir, int limit, RequestOptions options) + { + if (dir == Direction.Around) + throw new NotImplementedException(); //TODO: Impl + + var guildId = (channel as IGuildChannel)?.GuildId; + var guild = guildId != null ? (client as IDiscordClient).GetGuildAsync(guildId.Value, CacheMode.CacheOnly).Result : null; + + 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); + var builder = ImmutableArray.CreateBuilder(); + foreach (var model in models) + { + var author = GetAuthor(client, guild, model.Author.Value, model.WebhookId.ToNullable()); + builder.Add(RestMessage.Create(client, channel, author, model)); + } + return builder.ToImmutable(); + }, + nextPage: (info, lastPage) => + { + if (lastPage.Count != DiscordConfig.MaxMessagesPerBatch) + return false; + if (dir == Direction.Before) + info.Position = lastPage.Min(x => x.Id); + else + info.Position = lastPage.Max(x => x.Id); + return true; + }, + start: fromMessageId, + count: limit + ); + } + public static async Task> GetPinnedMessagesAsync(IMessageChannel channel, BaseDiscordClient client, + RequestOptions options) + { + var guildId = (channel as IGuildChannel)?.GuildId; + var guild = guildId != null ? await (client as IDiscordClient).GetGuildAsync(guildId.Value, CacheMode.CacheOnly).ConfigureAwait(false) : null; + var models = await client.ApiClient.GetPinsAsync(channel.Id, options).ConfigureAwait(false); + var builder = ImmutableArray.CreateBuilder(); + foreach (var model in models) + { + var author = GetAuthor(client, guild, model.Author.Value, model.WebhookId.ToNullable()); + builder.Add(RestMessage.Create(client, channel, author, model)); + } + return builder.ToImmutable(); + } + + /// Message content is too long, length must be less or equal to . + public static async Task SendMessageAsync(IMessageChannel channel, BaseDiscordClient client, + string text, bool isTTS, Embed embed, RequestOptions options) + { + var args = new CreateMessageParams(text) { IsTTS = isTTS, Embed = embed?.ToModel() }; + var model = await client.ApiClient.CreateMessageAsync(channel.Id, args, options).ConfigureAwait(false); + return RestUserMessage.Create(client, channel, client.CurrentUser, model); + } + + /// + /// is a zero-length string, contains only white space, or contains one or more + /// invalid characters as defined by . + /// + /// + /// is null. + /// + /// + /// The specified path, file name, or both exceed the system-defined maximum length. For example, on + /// Windows-based platforms, paths must be less than 248 characters, and file names must be less than 260 + /// characters. + /// + /// + /// The specified path is invalid, (for example, it is on an unmapped drive). + /// + /// + /// specified a directory.-or- The caller does not have the required permission. + /// + /// + /// The file specified in was not found. + /// + /// is in an invalid format. + /// An I/O error occurred while opening the file. + /// Message content is too long, length must be less or equal to . + public static async Task SendFileAsync(IMessageChannel channel, BaseDiscordClient client, + string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) + { + string filename = Path.GetFileName(filePath); + using (var file = File.OpenRead(filePath)) + return await SendFileAsync(channel, client, file, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); + } + + /// Message content is too long, length must be less or equal to . + public static async Task SendFileAsync(IMessageChannel channel, BaseDiscordClient client, + Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) + { + var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS, Embed = embed != null ? embed.ToModel() : Optional.Unspecified, IsSpoiler = isSpoiler }; + var model = await client.ApiClient.UploadFileAsync(channel.Id, args, options).ConfigureAwait(false); + return RestUserMessage.Create(client, channel, client.CurrentUser, model); + } + + public static Task DeleteMessageAsync(IMessageChannel channel, ulong messageId, BaseDiscordClient client, + RequestOptions options) + => MessageHelper.DeleteAsync(channel.Id, messageId, client, options); + + public static async Task DeleteMessagesAsync(ITextChannel channel, BaseDiscordClient client, + IEnumerable messageIds, RequestOptions options) + { + const int BATCH_SIZE = 100; + + var msgs = messageIds.ToArray(); + int batches = msgs.Length / BATCH_SIZE; + for (int i = 0; i <= batches; i++) + { + ArraySegment batch; + if (i < batches) + { + batch = new ArraySegment(msgs, i * BATCH_SIZE, BATCH_SIZE); + } + else + { + batch = new ArraySegment(msgs, i * BATCH_SIZE, msgs.Length - batches * BATCH_SIZE); + if (batch.Count == 0) + { + break; + } + } + var args = new DeleteMessagesParams(batch.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 + /// Resolving permissions requires the parent guild to be downloaded. + 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).ViewChannel) + return null; + + return user; + } + /// Resolving permissions requires the parent guild to be downloaded. + 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 client.ApiClient.GetGuildMembersAsync(guild.Id, args, options).ConfigureAwait(false); + return models + .Select(x => RestGuildUser.Create(client, guild, x)) + .Where(x => x.GetPermissions(channel).ViewChannel) + .ToImmutableArray(); + }, + nextPage: (info, lastPage) => + { + if (lastPage.Count != DiscordConfig.MaxMessagesPerBatch) + return false; + info.Position = lastPage.Max(x => x.Id); + return true; + }, + 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(channel, options); + + //Webhooks + public static async Task CreateWebhookAsync(ITextChannel channel, BaseDiscordClient client, string name, Stream avatar, RequestOptions options) + { + var args = new CreateWebhookParams { Name = name }; + if (avatar != null) + args.Avatar = new API.Image(avatar); + + var model = await client.ApiClient.CreateWebhookAsync(channel.Id, args, options).ConfigureAwait(false); + return RestWebhook.Create(client, channel, model); + } + public static async Task GetWebhookAsync(ITextChannel channel, BaseDiscordClient client, ulong id, RequestOptions options) + { + var model = await client.ApiClient.GetWebhookAsync(id, options: options).ConfigureAwait(false); + if (model == null) + return null; + return RestWebhook.Create(client, channel, model); + } + public static async Task> GetWebhooksAsync(ITextChannel channel, BaseDiscordClient client, RequestOptions options) + { + var models = await client.ApiClient.GetChannelWebhooksAsync(channel.Id, options).ConfigureAwait(false); + return models.Select(x => RestWebhook.Create(client, channel, x)) + .ToImmutableArray(); + } + // Categories + public static async Task GetCategoryAsync(INestedChannel channel, BaseDiscordClient client, RequestOptions options) + { + // if no category id specified, return null + if (!channel.CategoryId.HasValue) + return null; + // CategoryId will contain a value here + var model = await client.ApiClient.GetChannelAsync(channel.CategoryId.Value, options).ConfigureAwait(false); + return RestCategoryChannel.Create(client, model) as ICategoryChannel; + } + /// This channel does not have a parent channel. + public static async Task SyncPermissionsAsync(INestedChannel channel, BaseDiscordClient client, RequestOptions options) + { + var category = await GetCategoryAsync(channel, client, options).ConfigureAwait(false); + if (category == null) throw new InvalidOperationException("This channel does not have a parent channel."); + + var apiArgs = new ModifyGuildChannelParams + { + Overwrites = category.PermissionOverwrites + .Select(overwrite => new API.Overwrite{ + TargetId = overwrite.TargetId, + TargetType = overwrite.TargetType, + Allow = overwrite.Permissions.AllowValue, + Deny = overwrite.Permissions.DenyValue + }).ToArray() + }; + await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); + } + + //Helpers + private static IUser GetAuthor(BaseDiscordClient client, IGuild guild, UserModel model, ulong? webhookId) + { + IUser author = null; + if (guild != null) + author = guild.GetUserAsync(model.Id, CacheMode.CacheOnly).Result; + if (author == null) + author = RestUser.Create(client, guild, model, webhookId); + return author; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/IRestAudioChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/IRestAudioChannel.cs new file mode 100644 index 0000000..2c33415 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/IRestAudioChannel.cs @@ -0,0 +1,6 @@ +namespace Discord.Rest +{ + public interface IRestAudioChannel : IAudioChannel + { + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs new file mode 100644 index 0000000..a28170e --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs @@ -0,0 +1,136 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace Discord.Rest +{ + /// + /// Represents a REST-based channel that can send and receive messages. + /// + public interface IRestMessageChannel : IMessageChannel + { + /// + /// Sends a message to this message channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The message to be sent. + /// Determines whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + new Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); + /// + /// Sends a file to this message channel with an optional caption. + /// + /// + /// This method follows the same behavior as described in + /// . Please visit + /// its documentation for more details on this method. + /// + /// The file path of the file. + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false); + /// + /// Sends a file to this message channel with an optional caption. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The of the file to be sent. + /// The name of the attachment. + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false); + + /// + /// Gets a message from this message channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The snowflake identifier of the message. + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous get operation for retrieving the message. The task result contains + /// the retrieved message; null if no message is found with the specified identifier. + /// + Task GetMessageAsync(ulong id, RequestOptions options = null); + /// + /// Gets the last N messages from this message channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The numbers of message to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// + IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null); + /// + /// Gets a collection of messages in this channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The ID of the starting message to get the messages from. + /// The direction of the messages to be gotten from. + /// The numbers of message to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// + IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null); + /// + /// Gets a collection of messages in this channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The starting message to get the messages from. + /// The direction of the messages to be gotten from. + /// The numbers of message to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// + IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null); + /// + /// Gets a collection of pinned messages in this channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation for retrieving pinned messages in this channel. + /// The task result contains a collection of messages found in the pinned messages. + /// + new Task> GetPinnedMessagesAsync(RequestOptions options = null); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/IRestPrivateChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/IRestPrivateChannel.cs new file mode 100644 index 0000000..f387ac2 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/IRestPrivateChannel.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace Discord.Rest +{ + /// + /// Represents a REST-based channel that is private to select recipients. + /// + public interface IRestPrivateChannel : IPrivateChannel + { + /// + /// Users that can access this channel. + /// + new IReadOnlyCollection Recipients { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs new file mode 100644 index 0000000..177bde2 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord.Rest +{ + /// + /// Represents a REST-based category channel. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestCategoryChannel : RestGuildChannel, ICategoryChannel + { + internal RestCategoryChannel(BaseDiscordClient discord, IGuild guild, ulong id) + : base(discord, guild, id) + { + } + internal new static RestCategoryChannel Create(BaseDiscordClient discord, IGuild guild, Model model) + { + var entity = new RestCategoryChannel(discord, guild, model.Id); + entity.Update(model); + return entity; + } + + private string DebuggerDisplay => $"{Name} ({Id}, Category)"; + + //IChannel + /// + /// This method is not supported with category channels. + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + => throw new NotSupportedException(); + /// + /// This method is not supported with category channels. + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => throw new NotSupportedException(); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/RestChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/RestChannel.cs new file mode 100644 index 0000000..6f6a1f0 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/RestChannel.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord.Rest +{ + /// + /// Represents a generic REST-based channel. + /// + public class RestChannel : RestEntity, IChannel, IUpdateable + { + /// + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + + internal RestChannel(BaseDiscordClient discord, ulong id) + : base(discord, id) + { + } + /// Unexpected channel type. + internal static RestChannel Create(BaseDiscordClient discord, Model model) + { + switch (model.Type) + { + case ChannelType.News: + 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; + case ChannelType.Category: + return RestCategoryChannel.Create(discord, new RestGuild(discord, model.GuildId.Value), model); + default: + return new RestChannel(discord, model.Id); + } + } + /// Unexpected channel 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 virtual void Update(Model model) { } + + /// + public virtual Task UpdateAsync(RequestOptions options = null) => Task.Delay(0); + + //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/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs new file mode 100644 index 0000000..446410b --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs @@ -0,0 +1,223 @@ +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 +{ + /// + /// Represents a REST-based direct-message channel. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestDMChannel : RestChannel, IDMChannel, IRestPrivateChannel, IRestMessageChannel + { + /// + /// Gets the current logged-in user. + /// + public RestUser CurrentUser { get; } + + /// + /// Gets the recipient of the channel. + /// + public RestUser Recipient { get; } + + /// + /// Gets a collection that is the current logged-in user and the recipient. + /// + 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); + + + /// + /// Gets a user in this channel from the provided . + /// + /// The snowflake identifier of the user. + /// + /// A object that is a recipient of this channel; otherwise null. + /// + 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, options); + /// + public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, options); + /// + public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, options); + /// + public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, options); + /// + public Task> GetPinnedMessagesAsync(RequestOptions options = null) + => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); + + /// + /// Message content is too long, length must be less or equal to . + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); + + /// + /// + /// is a zero-length string, contains only white space, or contains one or more + /// invalid characters as defined by . + /// + /// + /// is null. + /// + /// + /// The specified path, file name, or both exceed the system-defined maximum length. For example, on + /// Windows-based platforms, paths must be less than 248 characters, and file names must be less than 260 + /// characters. + /// + /// + /// The specified path is invalid, (for example, it is on an unmapped drive). + /// + /// + /// specified a directory.-or- The caller does not have the required permission. + /// + /// + /// The file specified in was not found. + /// + /// is in an invalid format. + /// An I/O error occurred while opening the file. + /// Message content is too long, length must be less or equal to . + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler); + /// + /// Message content is too long, length must be less or equal to . + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler); + + /// + public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) + => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); + /// + public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) + => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options); + + /// + public Task TriggerTypingAsync(RequestOptions options = null) + => ChannelHelper.TriggerTypingAsync(this, Discord, options); + /// + public IDisposable EnterTypingState(RequestOptions options = null) + => ChannelHelper.EnterTypingState(this, Discord, options); + + /// + /// Gets a string that represents the Username#Discriminator of the recipient. + /// + /// + /// A string that resolves to the Recipient of this channel. + /// + 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, Embed embed, RequestOptions options, bool isSpoiler) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); + /// + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); + /// + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) + => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + + //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/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs new file mode 100644 index 0000000..5cfe03f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs @@ -0,0 +1,201 @@ +using Discord.Audio; +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 +{ + /// + /// Represents a REST-based group-message channel. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestGroupChannel : RestChannel, IGroupChannel, IRestPrivateChannel, IRestMessageChannel, IRestAudioChannel + { + 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) + { + if (_users.TryGetValue(id, out RestGroupUser user)) + return user; + return null; + } + + /// + public Task GetMessageAsync(ulong id, RequestOptions options = null) + => ChannelHelper.GetMessageAsync(this, Discord, id, options); + /// + public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, options); + /// + public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, options); + /// + public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, options); + /// + public Task> GetPinnedMessagesAsync(RequestOptions options = null) + => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); + + /// + public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) + => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); + /// + public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) + => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options); + + /// + /// Message content is too long, length must be less or equal to . + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); + + /// + /// + /// is a zero-length string, contains only white space, or contains one or more + /// invalid characters as defined by . + /// + /// + /// is null. + /// + /// + /// The specified path, file name, or both exceed the system-defined maximum length. For example, on + /// Windows-based platforms, paths must be less than 248 characters, and file names must be less than 260 + /// characters. + /// + /// + /// The specified path is invalid, (for example, it is on an unmapped drive). + /// + /// + /// specified a directory.-or- The caller does not have the required permission. + /// + /// + /// The file specified in was not found. + /// + /// is in an invalid format. + /// An I/O error occurred while opening the file. + /// Message content is too long, length must be less or equal to . + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler); + /// + /// Message content is too long, length must be less or equal to . + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler); + + /// + 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, Embed embed, RequestOptions options, bool isSpoiler) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); + + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) + => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + + //IAudioChannel + /// + /// Connecting to a group channel is not supported. + Task IAudioChannel.ConnectAsync(bool selfDeaf, bool selfMute, bool external) { throw new NotSupportedException(); } + Task IAudioChannel.DisconnectAsync() { throw new NotSupportedException(); } + + //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/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs new file mode 100644 index 0000000..fdfee39 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs @@ -0,0 +1,237 @@ +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 +{ + /// + /// Represents a private REST-based group channel. + /// + public class RestGuildChannel : RestChannel, IGuildChannel + { + private ImmutableArray _overwrites; + + /// + public virtual 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.News: + return RestNewsChannel.Create(discord, guild, model); + case ChannelType.Text: + return RestTextChannel.Create(discord, guild, model); + case ChannelType.Voice: + return RestVoiceChannel.Create(discord, guild, model); + case ChannelType.Category: + return RestCategoryChannel.Create(discord, guild, model); + default: + return new RestGuildChannel(discord, guild, model.Id); + } + } + 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(overwrites[i].ToEntity()); + _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); + + /// + /// Gets the permission overwrite for a specific user. + /// + /// The user to get the overwrite from. + /// + /// An overwrite object for the targeted user; null if none is set. + /// + public virtual OverwritePermissions? GetPermissionOverwrite(IUser user) + { + for (int i = 0; i < _overwrites.Length; i++) + { + if (_overwrites[i].TargetId == user.Id) + return _overwrites[i].Permissions; + } + return null; + } + + /// + /// Gets the permission overwrite for a specific role. + /// + /// The role to get the overwrite from. + /// + /// An overwrite object for the targeted role; null if none is set. + /// + public virtual OverwritePermissions? GetPermissionOverwrite(IRole role) + { + for (int i = 0; i < _overwrites.Length; i++) + { + if (_overwrites[i].TargetId == role.Id) + return _overwrites[i].Permissions; + } + return null; + } + + /// + /// Adds or updates the permission overwrite for the given user. + /// + /// The user to add the overwrite to. + /// The overwrite to add to the user. + /// The options to be used when sending the request. + /// + /// A task representing the asynchronous permission operation for adding the specified permissions to the channel. + /// + public virtual async Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null) + { + await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, user, permissions, options).ConfigureAwait(false); + _overwrites = _overwrites.Add(new Overwrite(user.Id, PermissionTarget.User, new OverwritePermissions(permissions.AllowValue, permissions.DenyValue))); + } + /// + /// Adds or updates the permission overwrite for the given role. + /// + /// The role to add the overwrite to. + /// The overwrite to add to the role. + /// The options to be used when sending the request. + /// + /// A task representing the asynchronous permission operation for adding the specified permissions to the channel. + /// + public virtual async Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) + { + await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, role, permissions, options).ConfigureAwait(false); + _overwrites = _overwrites.Add(new Overwrite(role.Id, PermissionTarget.Role, new OverwritePermissions(permissions.AllowValue, permissions.DenyValue))); + } + + /// + /// Removes the permission overwrite for the given user, if one exists. + /// + /// The user to remove the overwrite from. + /// The options to be used when sending the request. + /// + /// A task representing the asynchronous operation for removing the specified permissions from the channel. + /// + public virtual 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; + } + } + } + /// + /// Removes the permission overwrite for the given role, if one exists. + /// + /// The role to remove the overwrite from. + /// The options to be used when sending the request. + /// + /// A task representing the asynchronous operation for removing the specified permissions from the channel. + /// + public virtual 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; + } + } + } + + /// + /// Gets the name of this channel. + /// + /// + /// A string that is the name of this channel. + /// + public override string ToString() => Name; + + //IGuildChannel + /// + IGuild IGuildChannel.Guild + { + get + { + if (Guild != null) + return Guild; + throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); + } + } + + /// + 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>(); //Overridden //Overridden in Text/Voice + /// + Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(null); //Overridden in Text/Voice + + //IChannel + /// + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + => AsyncEnumerable.Empty>(); //Overridden in Text/Voice + /// + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(null); //Overridden in Text/Voice + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/RestNewsChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/RestNewsChannel.cs new file mode 100644 index 0000000..8a334fa --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/RestNewsChannel.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord.Rest +{ + /// + /// Represents a REST-based news channel in a guild that has the same properties as a . + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestNewsChannel : RestTextChannel + { + internal RestNewsChannel(BaseDiscordClient discord, IGuild guild, ulong id) + :base(discord, guild, id) + { + } + internal new static RestNewsChannel Create(BaseDiscordClient discord, IGuild guild, Model model) + { + var entity = new RestNewsChannel(discord, guild, model.Id); + entity.Update(model); + return entity; + } + public override int SlowModeInterval => throw new NotSupportedException("News channels do not support Slow Mode."); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs new file mode 100644 index 0000000..dc86327 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -0,0 +1,324 @@ +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 +{ + /// + /// Represents a REST-based channel in a guild that can send and receive messages. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestTextChannel : RestGuildChannel, IRestMessageChannel, ITextChannel + { + /// + public string Topic { get; private set; } + /// + public virtual int SlowModeInterval { get; private set; } + /// + public ulong? CategoryId { get; private set; } + + /// + public string Mention => MentionUtils.MentionChannel(Id); + /// + public bool IsNsfw { get; private set; } + + 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); + CategoryId = model.CategoryId; + Topic = model.Topic.Value; + SlowModeInterval = model.SlowMode.Value; + IsNsfw = model.Nsfw.GetValueOrDefault(); + } + + /// + public async Task ModifyAsync(Action func, RequestOptions options = null) + { + var model = await ChannelHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); + Update(model); + } + + /// + /// Gets a user in this channel. + /// + /// The snowflake identifier of the user. + /// The options to be used when sending the request. + /// + /// Resolving permissions requires the parent guild to be downloaded. + /// + /// + /// A task representing the asynchronous get operation. The task result contains a guild user object that + /// represents the user; null if none is found. + /// + public Task GetUserAsync(ulong id, RequestOptions options = null) + => ChannelHelper.GetUserAsync(this, Guild, Discord, id, options); + + /// + /// Gets a collection of users that are able to view the channel. + /// + /// The options to be used when sending the request. + /// + /// Resolving permissions requires the parent guild to be downloaded. + /// + /// + /// A paged collection containing a collection of guild users that can access this channel. Flattening the + /// paginated response into a collection of users with + /// is required if you wish to access the users. + /// + 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, options); + /// + public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, options); + /// + public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, options); + /// + public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, options); + /// + public Task> GetPinnedMessagesAsync(RequestOptions options = null) + => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); + + /// + /// Message content is too long, length must be less or equal to . + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); + + /// + /// + /// is a zero-length string, contains only white space, or contains one or more + /// invalid characters as defined by . + /// + /// + /// is null. + /// + /// + /// The specified path, file name, or both exceed the system-defined maximum length. For example, on + /// Windows-based platforms, paths must be less than 248 characters, and file names must be less than 260 + /// characters. + /// + /// + /// The specified path is invalid, (for example, it is on an unmapped drive). + /// + /// + /// specified a directory.-or- The caller does not have the required permission. + /// + /// + /// The file specified in was not found. + /// + /// is in an invalid format. + /// An I/O error occurred while opening the file. + /// Message content is too long, length must be less or equal to . + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler); + + /// + /// Message content is too long, length must be less or equal to . + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler); + + /// + public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) + => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); + /// + public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) + => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options); + + /// + public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + /// + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); + + /// + public Task TriggerTypingAsync(RequestOptions options = null) + => ChannelHelper.TriggerTypingAsync(this, Discord, options); + /// + public IDisposable EnterTypingState(RequestOptions options = null) + => ChannelHelper.EnterTypingState(this, Discord, options); + + /// + /// Creates a webhook in this text channel. + /// + /// The name of the webhook. + /// The avatar of the webhook. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// webhook. + /// + public Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) + => ChannelHelper.CreateWebhookAsync(this, Discord, name, avatar, options); + /// + /// Gets a webhook available in this text channel. + /// + /// The identifier of the webhook. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a webhook associated + /// with the identifier; null if the webhook is not found. + /// + public Task GetWebhookAsync(ulong id, RequestOptions options = null) + => ChannelHelper.GetWebhookAsync(this, Discord, id, options); + /// + /// Gets the webhooks available in this text channel. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of webhooks that is available in this channel. + /// + public Task> GetWebhooksAsync(RequestOptions options = null) + => ChannelHelper.GetWebhooksAsync(this, Discord, options); + + /// + /// Gets the parent (category) channel of this channel. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the category channel + /// representing the parent of this channel; null if none is set. + /// + public Task GetCategoryAsync(RequestOptions options = null) + => ChannelHelper.GetCategoryAsync(this, Discord, options); + /// + public Task SyncPermissionsAsync(RequestOptions options = null) + => ChannelHelper.SyncPermissionsAsync(this, Discord, options); + + //Invites + /// + public async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); + /// + public async Task> GetInvitesAsync(RequestOptions options = null) + => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); + + private string DebuggerDisplay => $"{Name} ({Id}, Text)"; + + //ITextChannel + /// + async Task ITextChannel.CreateWebhookAsync(string name, Stream avatar, RequestOptions options) + => await CreateWebhookAsync(name, avatar, options).ConfigureAwait(false); + /// + async Task ITextChannel.GetWebhookAsync(ulong id, RequestOptions options) + => await GetWebhookAsync(id, options).ConfigureAwait(false); + /// + async Task> ITextChannel.GetWebhooksAsync(RequestOptions options) + => await GetWebhooksAsync(options).ConfigureAwait(false); + + //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, Embed embed, RequestOptions options, bool isSpoiler) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); + + /// + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); + /// + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) + => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + + //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>(); + } + + // INestedChannel + /// + async Task INestedChannel.GetCategoryAsync(CacheMode mode, RequestOptions options) + { + if (CategoryId.HasValue && mode == CacheMode.AllowDownload) + return (await Guild.GetChannelAsync(CategoryId.Value, mode, options).ConfigureAwait(false)) as ICategoryChannel; + return null; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs new file mode 100644 index 0000000..3f3aa96 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs @@ -0,0 +1,97 @@ +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 +{ + /// + /// Represents a REST-based voice channel in a guild. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestVoiceChannel : RestGuildChannel, IVoiceChannel, IRestAudioChannel + { + /// + public int Bitrate { get; private set; } + /// + public int? UserLimit { get; private set; } + /// + public ulong? CategoryId { 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); + CategoryId = model.CategoryId; + Bitrate = model.Bitrate.Value; + UserLimit = model.UserLimit.Value != 0 ? model.UserLimit.Value : (int?)null; + } + + /// + public async Task ModifyAsync(Action func, RequestOptions options = null) + { + var model = await ChannelHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); + Update(model); + } + + /// + /// Gets the parent (category) channel of this channel. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the category channel + /// representing the parent of this channel; null if none is set. + /// + public Task GetCategoryAsync(RequestOptions options = null) + => ChannelHelper.GetCategoryAsync(this, Discord, options); + /// + public Task SyncPermissionsAsync(RequestOptions options = null) + => ChannelHelper.SyncPermissionsAsync(this, Discord, options); + + //Invites + /// + public async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); + /// + public async Task> GetInvitesAsync(RequestOptions options = null) + => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); + + private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; + + //IAudioChannel + /// + /// Connecting to a REST-based channel is not supported. + Task IAudioChannel.ConnectAsync(bool selfDeaf, bool selfMute, bool external) { throw new NotSupportedException(); } + Task IAudioChannel.DisconnectAsync() { 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>(); + + // INestedChannel + /// + async Task INestedChannel.GetCategoryAsync(CacheMode mode, RequestOptions options) + { + if (CategoryId.HasValue && mode == CacheMode.AllowDownload) + return (await Guild.GetChannelAsync(CategoryId.Value, mode, options).ConfigureAwait(false)) as ICategoryChannel; + return null; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs new file mode 100644 index 0000000..790b1e5 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -0,0 +1,480 @@ +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; +using ImageModel = Discord.API.Image; + +namespace Discord.Rest +{ + internal static class GuildHelper + { + //General + /// is null. + public static async Task ModifyAsync(IGuild guild, BaseDiscordClient client, + Action func, RequestOptions options) + { + if (func == null) throw new ArgumentNullException(nameof(func)); + + var args = new GuildProperties(); + func(args); + + var apiArgs = new API.Rest.ModifyGuildParams + { + AfkChannelId = args.AfkChannelId, + AfkTimeout = args.AfkTimeout, + SystemChannelId = args.SystemChannelId, + DefaultMessageNotifications = args.DefaultMessageNotifications, + Icon = args.Icon.IsSpecified ? args.Icon.Value?.ToModel() : Optional.Create(), + Name = args.Name, + Splash = args.Splash.IsSpecified ? args.Splash.Value?.ToModel() : Optional.Create(), + Banner = args.Banner.IsSpecified ? args.Banner.Value?.ToModel() : Optional.Create(), + VerificationLevel = args.VerificationLevel, + ExplicitContentFilter = args.ExplicitContentFilter, + SystemChannelFlags = args.SystemChannelFlags + }; + + if (args.AfkChannel.IsSpecified) + apiArgs.AfkChannelId = args.AfkChannel.Value.Id; + else if (args.AfkChannelId.IsSpecified) + apiArgs.AfkChannelId = args.AfkChannelId.Value; + + if (args.SystemChannel.IsSpecified) + apiArgs.SystemChannelId = args.SystemChannel.Value.Id; + else if (args.SystemChannelId.IsSpecified) + apiArgs.SystemChannelId = args.SystemChannelId.Value; + + if (args.Owner.IsSpecified) + apiArgs.OwnerId = args.Owner.Value.Id; + else if (args.OwnerId.IsSpecified) + apiArgs.OwnerId = args.OwnerId.Value; + + if (args.Region.IsSpecified) + apiArgs.RegionId = args.Region.Value.Id; + else if (args.RegionId.IsSpecified) + apiArgs.RegionId = args.RegionId.Value; + + if (!apiArgs.Banner.IsSpecified && guild.BannerId != null) + apiArgs.Banner = new ImageModel(guild.BannerId); + if (!apiArgs.Splash.IsSpecified && guild.SplashId != null) + apiArgs.Splash = new ImageModel(guild.SplashId); + if (!apiArgs.Icon.IsSpecified && guild.IconId != null) + apiArgs.Icon = new ImageModel(guild.IconId); + + if (args.ExplicitContentFilter.IsSpecified) + apiArgs.ExplicitContentFilter = args.ExplicitContentFilter.Value; + + if (args.SystemChannelFlags.IsSpecified) + apiArgs.SystemChannelFlags = args.SystemChannelFlags.Value; + + // PreferredLocale takes precedence over PreferredCulture + if (args.PreferredLocale.IsSpecified) + apiArgs.PreferredLocale = args.PreferredLocale.Value; + else if (args.PreferredCulture.IsSpecified) + apiArgs.PreferredLocale = args.PreferredCulture.Value.Name; + + return await client.ApiClient.ModifyGuildAsync(guild.Id, apiArgs, options).ConfigureAwait(false); + } + /// is null. + public static async Task ModifyEmbedAsync(IGuild guild, BaseDiscordClient client, + Action func, RequestOptions options) + { + if (func == null) throw new ArgumentNullException(nameof(func)); + + var args = new GuildEmbedProperties(); + func(args); + var apiArgs = new API.Rest.ModifyGuildEmbedParams + { + Enabled = args.Enabled + }; + + if (args.Channel.IsSpecified) + apiArgs.ChannelId = args.Channel.Value?.Id; + else if (args.ChannelId.IsSpecified) + apiArgs.ChannelId = args.ChannelId.Value; + + return await client.ApiClient.ModifyGuildEmbedAsync(guild.Id, apiArgs, options).ConfigureAwait(false); + } + public static async Task ReorderChannelsAsync(IGuild guild, BaseDiscordClient client, + IEnumerable args, RequestOptions options) + { + var apiArgs = args.Select(x => new API.Rest.ModifyGuildChannelsParams(x.Id, x.Position)); + await client.ApiClient.ModifyGuildChannelsAsync(guild.Id, apiArgs, options).ConfigureAwait(false); + } + public static async Task> ReorderRolesAsync(IGuild guild, BaseDiscordClient client, + IEnumerable args, RequestOptions options) + { + var apiArgs = args.Select(x => new API.Rest.ModifyGuildRolesParams(x.Id, x.Position)); + return await client.ApiClient.ModifyGuildRolesAsync(guild.Id, apiArgs, 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 GetBanAsync(IGuild guild, BaseDiscordClient client, ulong userId, RequestOptions options) + { + var model = await client.ApiClient.GetGuildBanAsync(guild.Id, userId, options).ConfigureAwait(false); + return RestBan.Create(client, model); + } + + public static async Task AddBanAsync(IGuild guild, BaseDiscordClient client, + ulong userId, int pruneDays, string reason, RequestOptions options) + { + var args = new CreateGuildBanParams { DeleteMessageDays = pruneDays, Reason = reason }; + 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(); + } + /// is null. + public static async Task CreateTextChannelAsync(IGuild guild, BaseDiscordClient client, + string name, RequestOptions options, Action func = null) + { + if (name == null) throw new ArgumentNullException(paramName: nameof(name)); + + var props = new TextChannelProperties(); + func?.Invoke(props); + + var args = new CreateGuildChannelParams(name, ChannelType.Text) + { + CategoryId = props.CategoryId, + Topic = props.Topic, + IsNsfw = props.IsNsfw, + Position = props.Position + }; + var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); + return RestTextChannel.Create(client, guild, model); + } + /// is null. + public static async Task CreateVoiceChannelAsync(IGuild guild, BaseDiscordClient client, + string name, RequestOptions options, Action func = null) + { + if (name == null) throw new ArgumentNullException(paramName: nameof(name)); + + var props = new VoiceChannelProperties(); + func?.Invoke(props); + + var args = new CreateGuildChannelParams(name, ChannelType.Voice) + { + CategoryId = props.CategoryId, + Bitrate = props.Bitrate, + UserLimit = props.UserLimit, + Position = props.Position + }; + var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); + return RestVoiceChannel.Create(client, guild, model); + } + /// is null. + public static async Task CreateCategoryChannelAsync(IGuild guild, BaseDiscordClient client, + string name, RequestOptions options, Action func = null) + { + if (name == null) throw new ArgumentNullException(paramName: nameof(name)); + + var props = new GuildChannelProperties(); + func?.Invoke(props); + + var args = new CreateGuildChannelParams(name, ChannelType.Category) + { + Position = props.Position + }; + + var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); + return RestCategoryChannel.Create(client, guild, model); + } + + //Voice Regions + public static async Task> GetVoiceRegionsAsync(IGuild guild, BaseDiscordClient client, + RequestOptions options) + { + var models = await client.ApiClient.GetGuildVoiceRegionsAsync(guild.Id, options).ConfigureAwait(false); + return models.Select(x => RestVoiceRegion.Create(client, x)).ToImmutableArray(); + } + + //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, guild, 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, guild, 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, guild, null, x)).ToImmutableArray(); + } + public static async Task GetVanityInviteAsync(IGuild guild, BaseDiscordClient client, + RequestOptions options) + { + var vanityModel = await client.ApiClient.GetVanityInviteAsync(guild.Id, options).ConfigureAwait(false); + if (vanityModel == null) throw new InvalidOperationException("This guild does not have a vanity URL."); + var inviteModel = await client.ApiClient.GetInviteAsync(vanityModel.Code, options).ConfigureAwait(false); + return RestInviteMetadata.Create(client, guild, null, inviteModel); + } + + //Roles + /// is null. + public static async Task CreateRoleAsync(IGuild guild, BaseDiscordClient client, + string name, GuildPermissions? permissions, Color? color, bool isHoisted, bool isMentionable, RequestOptions options) + { + if (name == null) throw new ArgumentNullException(paramName: nameof(name)); + + var model = await client.ApiClient.CreateGuildRoleAsync(guild.Id, options).ConfigureAwait(false); + var role = RestRole.Create(client, guild, model); + + await role.ModifyAsync(x => + { + x.Name = name; + x.Permissions = (permissions ?? role.Permissions); + x.Color = (color ?? Color.Default); + x.Hoist = isHoisted; + x.Mentionable = isMentionable; + }, options).ConfigureAwait(false); + + return role; + } + + //Users + public static async Task AddGuildUserAsync(IGuild guild, BaseDiscordClient client, ulong userId, string accessToken, + Action func, RequestOptions options) + { + var args = new AddGuildUserProperties(); + func?.Invoke(args); + + if (args.Roles.IsSpecified) + { + var ids = args.Roles.Value.Select(r => r.Id); + + if (args.RoleIds.IsSpecified) + args.RoleIds.Value.Concat(ids); + else + args.RoleIds = Optional.Create(ids); + } + var apiArgs = new AddGuildMemberParams + { + AccessToken = accessToken, + Nickname = args.Nickname, + IsDeafened = args.Deaf, + IsMuted = args.Mute, + RoleIds = args.RoleIds.IsSpecified ? args.RoleIds.Value.Distinct().ToArray() : Optional.Create() + }; + + var model = await client.ApiClient.AddGuildMemberAsync(guild.Id, userId, apiArgs, options); + + return model is null ? null : RestGuildUser.Create(client, guild, model); + } + + public static async Task AddGuildUserAsync(ulong guildId, BaseDiscordClient client, ulong userId, string accessToken, + Action func, RequestOptions options) + { + var args = new AddGuildUserProperties(); + func?.Invoke(args); + + if (args.Roles.IsSpecified) + { + var ids = args.Roles.Value.Select(r => r.Id); + + if (args.RoleIds.IsSpecified) + args.RoleIds.Value.Concat(ids); + else + args.RoleIds = Optional.Create(ids); + } + var apiArgs = new AddGuildMemberParams + { + AccessToken = accessToken, + Nickname = args.Nickname, + IsDeafened = args.Deaf, + IsMuted = args.Mute, + RoleIds = args.RoleIds.IsSpecified ? args.RoleIds.Value.Distinct().ToArray() : Optional.Create() + }; + + await client.ApiClient.AddGuildMemberAsync(guildId, userId, apiArgs, options); + } + + 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.MaxUsersPerBatch, + 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).ConfigureAwait(false); + return models.Select(x => RestGuildUser.Create(client, guild, x)).ToImmutableArray(); + }, + nextPage: (info, lastPage) => + { + if (lastPage.Count != DiscordConfig.MaxUsersPerBatch) + return false; + info.Position = lastPage.Max(x => x.Id); + return true; + }, + 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; + } + + // Audit logs + public static IAsyncEnumerable> GetAuditLogsAsync(IGuild guild, BaseDiscordClient client, + ulong? from, int? limit, RequestOptions options, ulong? userId = null, ActionType? actionType = null) + { + return new PagedAsyncEnumerable( + DiscordConfig.MaxAuditLogEntriesPerBatch, + async (info, ct) => + { + var args = new GetAuditLogsParams + { + Limit = info.PageSize + }; + if (info.Position != null) + args.BeforeEntryId = info.Position.Value; + if (userId.HasValue) + args.UserId = userId.Value; + if (actionType.HasValue) + args.ActionType = (int)actionType.Value; + var model = await client.ApiClient.GetAuditLogsAsync(guild.Id, args, options); + return model.Entries.Select((x) => RestAuditLogEntry.Create(client, model, x)).ToImmutableArray(); + }, + nextPage: (info, lastPage) => + { + if (lastPage.Count != DiscordConfig.MaxAuditLogEntriesPerBatch) + return false; + info.Position = lastPage.Min(x => x.Id); + return true; + }, + start: from, + count: limit + ); + } + + //Webhooks + public static async Task GetWebhookAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options) + { + var model = await client.ApiClient.GetWebhookAsync(id, options: options).ConfigureAwait(false); + if (model == null) + return null; + return RestWebhook.Create(client, guild, model); + } + public static async Task> GetWebhooksAsync(IGuild guild, BaseDiscordClient client, RequestOptions options) + { + var models = await client.ApiClient.GetGuildWebhooksAsync(guild.Id, options).ConfigureAwait(false); + return models.Select(x => RestWebhook.Create(client, guild, x)).ToImmutableArray(); + } + + //Emotes + public static async Task GetEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options) + { + var emote = await client.ApiClient.GetGuildEmoteAsync(guild.Id, id, options).ConfigureAwait(false); + return emote.ToEntity(); + } + public static async Task CreateEmoteAsync(IGuild guild, BaseDiscordClient client, string name, Image image, Optional> roles, + RequestOptions options) + { + var apiargs = new CreateGuildEmoteParams + { + Name = name, + Image = image.ToModel() + }; + if (roles.IsSpecified) + apiargs.RoleIds = roles.Value?.Select(xr => xr.Id).ToArray(); + + var emote = await client.ApiClient.CreateGuildEmoteAsync(guild.Id, apiargs, options).ConfigureAwait(false); + return emote.ToEntity(); + } + /// is null. + public static async Task ModifyEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, Action func, + RequestOptions options) + { + if (func == null) throw new ArgumentNullException(paramName: nameof(func)); + + var props = new EmoteProperties(); + func(props); + + var apiargs = new ModifyGuildEmoteParams + { + Name = props.Name + }; + if (props.Roles.IsSpecified) + apiargs.RoleIds = props.Roles.Value?.Select(xr => xr.Id).ToArray(); + + var emote = await client.ApiClient.ModifyGuildEmoteAsync(guild.Id, id, apiargs, options).ConfigureAwait(false); + return emote.ToEntity(); + } + public static Task DeleteEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options) + => client.ApiClient.DeleteGuildEmoteAsync(guild.Id, id, options); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Guilds/RestBan.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Guilds/RestBan.cs new file mode 100644 index 0000000..ec8f60a --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Guilds/RestBan.cs @@ -0,0 +1,45 @@ +using System.Diagnostics; +using Model = Discord.API.Ban; + +namespace Discord.Rest +{ + /// + /// Represents a REST-based ban object. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestBan : IBan + { + /// + /// Gets the banned user. + /// + /// + /// A generic object that was banned. + /// + 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); + } + + /// + /// Gets the name of the banned user. + /// + /// + /// A string containing the name of the user that was banned. + /// + public override string ToString() => User.ToString(); + private string DebuggerDisplay => $"{User}: {Reason}"; + + //IBan + /// + IUser IBan.User => User; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Guilds/RestGuild.cs new file mode 100644 index 0000000..900f504 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -0,0 +1,904 @@ +using Discord.Audio; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using EmbedModel = Discord.API.GuildEmbed; +using Model = Discord.API.Guild; + +namespace Discord.Rest +{ + /// + /// Represents a REST-based guild/server. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestGuild : RestEntity, IGuild, IUpdateable + { + private ImmutableDictionary _roles; + private ImmutableArray _emotes; + 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 ExplicitContentFilterLevel ExplicitContentFilter { get; private set; } + + /// + public ulong? AFKChannelId { get; private set; } + /// + public ulong? EmbedChannelId { get; private set; } + /// + public ulong? SystemChannelId { 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 ulong? ApplicationId { get; private set; } + /// + public PremiumTier PremiumTier { get; private set; } + /// + public string BannerId { get; private set; } + /// + public string VanityURLCode { get; private set; } + /// + public SystemChannelMessageDeny SystemChannelFlags { get; private set; } + /// + public string Description { get; private set; } + /// + public int PremiumSubscriptionCount { get; private set; } + /// + public string PreferredLocale { get; private set; } + + /// + public CultureInfo PreferredCulture { get; private set; } + + /// + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + + [Obsolete("DefaultChannelId is deprecated, use GetDefaultChannelAsync")] + public ulong DefaultChannelId => Id; + /// + public string IconUrl => CDN.GetGuildIconUrl(Id, IconId); + /// + public string SplashUrl => CDN.GetGuildSplashUrl(Id, SplashId); + /// + public string BannerUrl => CDN.GetGuildBannerUrl(Id, BannerId); + + /// + /// Gets the built-in role containing all users in this guild. + /// + public RestRole EveryoneRole => GetRole(Id); + + /// + /// Gets a collection of all roles in this guild. + /// + public IReadOnlyCollection Roles => _roles.ToReadOnlyCollection(); + /// + public IReadOnlyCollection Emotes => _emotes; + /// + 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; + SystemChannelId = model.SystemChannelId; + 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; + ExplicitContentFilter = model.ExplicitContentFilter; + ApplicationId = model.ApplicationId; + PremiumTier = model.PremiumTier; + VanityURLCode = model.VanityURLCode; + BannerId = model.Banner; + SystemChannelFlags = model.SystemChannelFlags; + Description = model.Description; + PremiumSubscriptionCount = model.PremiumSubscriptionCount.GetValueOrDefault(); + PreferredLocale = model.PreferredLocale; + PreferredCulture = new CultureInfo(PreferredLocale); + + if (model.Emojis != null) + { + var emotes = ImmutableArray.CreateBuilder(model.Emojis.Length); + for (int i = 0; i < model.Emojis.Length; i++) + emotes.Add(model.Emojis[i].ToEntity()); + _emotes = emotes.ToImmutableArray(); + } + else + _emotes = 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, this, 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); + + /// + /// is null. + public async Task ModifyAsync(Action func, RequestOptions options = null) + { + var model = await GuildHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); + Update(model); + } + + /// + /// is null. + public async Task ModifyEmbedAsync(Action func, RequestOptions options = null) + { + var model = await GuildHelper.ModifyEmbedAsync(this, Discord, func, options).ConfigureAwait(false); + Update(model); + } + + /// + /// is null. + public async Task ReorderChannelsAsync(IEnumerable args, RequestOptions options = null) + { + var arr = args.ToArray(); + await GuildHelper.ReorderChannelsAsync(this, Discord, arr, options).ConfigureAwait(false); + } + /// + public async Task ReorderRolesAsync(IEnumerable args, RequestOptions options = null) + { + var models = await GuildHelper.ReorderRolesAsync(this, Discord, args, options).ConfigureAwait(false); + foreach (var model in models) + { + var role = GetRole(model.Id); + role?.Update(model); + } + } + + /// + public Task LeaveAsync(RequestOptions options = null) + => GuildHelper.LeaveAsync(this, Discord, options); + + //Bans + //Bans + /// + /// Gets a collection of all users banned in this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// ban objects that this guild currently possesses, with each object containing the user banned and reason + /// behind the ban. + /// + public Task> GetBansAsync(RequestOptions options = null) + => GuildHelper.GetBansAsync(this, Discord, options); + /// + /// Gets a ban object for a banned user. + /// + /// The banned user. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a ban object, which + /// contains the user information and the reason for the ban; null if the ban entry cannot be found. + /// + public Task GetBanAsync(IUser user, RequestOptions options = null) + => GuildHelper.GetBanAsync(this, Discord, user.Id, options); + /// + /// Gets a ban object for a banned user. + /// + /// The snowflake identifier for the banned user. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a ban object, which + /// contains the user information and the reason for the ban; null if the ban entry cannot be found. + /// + public Task GetBanAsync(ulong userId, RequestOptions options = null) + => GuildHelper.GetBanAsync(this, Discord, userId, options); + + /// + public Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null) + => GuildHelper.AddBanAsync(this, Discord, user.Id, pruneDays, reason, options); + /// + public Task AddBanAsync(ulong userId, int pruneDays = 0, string reason = null, RequestOptions options = null) + => GuildHelper.AddBanAsync(this, Discord, userId, pruneDays, reason, 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 + /// + /// Gets a collection of all channels in this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// generic channels found within this guild. + /// + public Task> GetChannelsAsync(RequestOptions options = null) + => GuildHelper.GetChannelsAsync(this, Discord, options); + + /// + /// Gets a channel in this guild. + /// + /// The snowflake identifier for the channel. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the generic channel + /// associated with the specified ; null if none is found. + /// + public Task GetChannelAsync(ulong id, RequestOptions options = null) + => GuildHelper.GetChannelAsync(this, Discord, id, options); + + /// + /// Gets a text channel in this guild. + /// + /// The snowflake identifier for the text channel. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the text channel + /// associated with the specified ; null if none is found. + /// + public async Task GetTextChannelAsync(ulong id, RequestOptions options = null) + { + var channel = await GuildHelper.GetChannelAsync(this, Discord, id, options).ConfigureAwait(false); + return channel as RestTextChannel; + } + + /// + /// Gets a collection of all text channels in this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// message channels found within this guild. + /// + public async Task> GetTextChannelsAsync(RequestOptions options = null) + { + var channels = await GuildHelper.GetChannelsAsync(this, Discord, options).ConfigureAwait(false); + return channels.OfType().ToImmutableArray(); + } + + /// + /// Gets a voice channel in this guild. + /// + /// The snowflake identifier for the voice channel. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the voice channel associated + /// with the specified ; null if none is found. + /// + public async Task GetVoiceChannelAsync(ulong id, RequestOptions options = null) + { + var channel = await GuildHelper.GetChannelAsync(this, Discord, id, options).ConfigureAwait(false); + return channel as RestVoiceChannel; + } + + /// + /// Gets a collection of all voice channels in this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// voice channels found within this guild. + /// + public async Task> GetVoiceChannelsAsync(RequestOptions options = null) + { + var channels = await GuildHelper.GetChannelsAsync(this, Discord, options).ConfigureAwait(false); + return channels.OfType().ToImmutableArray(); + } + + /// + /// Gets a collection of all category channels in this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// category channels found within this guild. + /// + public async Task> GetCategoryChannelsAsync(RequestOptions options = null) + { + var channels = await GuildHelper.GetChannelsAsync(this, Discord, options).ConfigureAwait(false); + return channels.OfType().ToImmutableArray(); + } + + /// + /// Gets the AFK voice channel in this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the voice channel that the + /// AFK users will be moved to after they have idled for too long; null if none is set. + /// + public async Task GetAFKChannelAsync(RequestOptions options = null) + { + var afkId = AFKChannelId; + if (afkId.HasValue) + { + var channel = await GuildHelper.GetChannelAsync(this, Discord, afkId.Value, options).ConfigureAwait(false); + return channel as RestVoiceChannel; + } + return null; + } + + /// + /// Gets the first viewable text channel in this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the first viewable text + /// channel in this guild; null if none is found. + /// + public async Task GetDefaultChannelAsync(RequestOptions options = null) + { + var channels = await GetTextChannelsAsync(options).ConfigureAwait(false); + var user = await GetCurrentUserAsync(options).ConfigureAwait(false); + return channels + .Where(c => user.GetPermissions(c).ViewChannel) + .OrderBy(c => c.Position) + .FirstOrDefault(); + } + + /// + /// Gets the embed channel (i.e. the channel set in the guild's widget settings) in this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the embed channel set + /// within the server's widget settings; null if none is set. + /// + public async Task GetEmbedChannelAsync(RequestOptions options = null) + { + var embedId = EmbedChannelId; + if (embedId.HasValue) + return await GuildHelper.GetChannelAsync(this, Discord, embedId.Value, options).ConfigureAwait(false); + return null; + } + + /// + /// Gets the first viewable text channel in this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the first viewable text + /// channel in this guild; null if none is found. + /// + public async Task GetSystemChannelAsync(RequestOptions options = null) + { + var systemId = SystemChannelId; + if (systemId.HasValue) + { + var channel = await GuildHelper.GetChannelAsync(this, Discord, systemId.Value, options).ConfigureAwait(false); + return channel as RestTextChannel; + } + return null; + } + /// + /// Creates a new text channel in this guild. + /// + /// + /// The following example creates a new text channel under an existing category named Wumpus with a set topic. + /// + /// var categories = await guild.GetCategoriesAsync(); + /// var targetCategory = categories.FirstOrDefault(x => x.Name == "wumpus"); + /// if (targetCategory == null) return; + /// await Context.Guild.CreateTextChannelAsync(name, x => + /// { + /// x.CategoryId = targetCategory.Id; + /// x.Topic = $"This channel was created at {DateTimeOffset.UtcNow} by {user}."; + /// }); + /// + /// + /// The new name for the text channel. + /// The delegate containing the properties to be applied to the channel upon its creation. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// text channel. + /// + public Task CreateTextChannelAsync(string name, Action func = null, RequestOptions options = null) + => GuildHelper.CreateTextChannelAsync(this, Discord, name, options, func); + /// + /// Creates a voice channel with the provided name. + /// + /// The name of the new channel. + /// The delegate containing the properties to be applied to the channel upon its creation. + /// The options to be used when sending the request. + /// is null. + /// + /// The created voice channel. + /// + public Task CreateVoiceChannelAsync(string name, Action func = null, RequestOptions options = null) + => GuildHelper.CreateVoiceChannelAsync(this, Discord, name, options, func); + /// + /// Creates a category channel with the provided name. + /// + /// The name of the new channel. + /// The delegate containing the properties to be applied to the channel upon its creation. + /// The options to be used when sending the request. + /// is null. + /// + /// The created category channel. + /// + public Task CreateCategoryChannelAsync(string name, Action func = null, RequestOptions options = null) + => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options, func); + + /// + /// Gets a collection of all the voice regions this guild can access. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// voice regions the guild can access. + /// + public Task> GetVoiceRegionsAsync(RequestOptions options = null) + => GuildHelper.GetVoiceRegionsAsync(this, Discord, 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 + /// + /// Gets a collection of all invites in this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// invite metadata, each representing information for an invite found within this guild. + /// + public Task> GetInvitesAsync(RequestOptions options = null) + => GuildHelper.GetInvitesAsync(this, Discord, options); + /// + /// Gets the vanity invite URL of this guild. + /// + /// The options to be used when sending the request. + /// + /// A partial metadata of the vanity invite found within this guild. + /// + public Task GetVanityInviteAsync(RequestOptions options = null) + => GuildHelper.GetVanityInviteAsync(this, Discord, options); + + //Roles + /// + /// Gets a role in this guild. + /// + /// The snowflake identifier for the role. + /// + /// A role that is associated with the specified ; null if none is found. + /// + public RestRole GetRole(ulong id) + { + if (_roles.TryGetValue(id, out RestRole value)) + return value; + return null; + } + + /// + public Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), + bool isHoisted = false, RequestOptions options = null) + => CreateRoleAsync(name, permissions, color, isHoisted, false, options); + + /// + /// Creates a new role with the provided name. + /// + /// The new name for the role. + /// The guild permission that the role should possess. + /// The color of the role. + /// Whether the role is separated from others on the sidebar. + /// The options to be used when sending the request. + /// Whether the role can be mentioned. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// role. + /// + public async Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), + bool isHoisted = false, bool isMentionable = false, RequestOptions options = null) + { + var role = await GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, isMentionable, options).ConfigureAwait(false); + _roles = _roles.Add(role.Id, role); + return role; + } + + //Users + /// + /// Gets a collection of all users in this guild. + /// + /// + /// This method retrieves all users found within this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a collection of guild + /// users found within this guild. + /// + public IAsyncEnumerable> GetUsersAsync(RequestOptions options = null) + => GuildHelper.GetUsersAsync(this, Discord, null, null, options); + + /// + public Task AddGuildUserAsync(ulong id, string accessToken, Action func = null, RequestOptions options = null) + => GuildHelper.AddGuildUserAsync(this, Discord, id, accessToken, func, options); + + /// + /// Gets a user from this guild. + /// + /// + /// This method retrieves a user found within this guild. + /// + /// The snowflake identifier of the user. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the guild user + /// associated with the specified ; null if none is found. + /// + public Task GetUserAsync(ulong id, RequestOptions options = null) + => GuildHelper.GetUserAsync(this, Discord, id, options); + + /// + /// Gets the current user for this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the currently logged-in + /// user within this guild. + /// + public Task GetCurrentUserAsync(RequestOptions options = null) + => GuildHelper.GetUserAsync(this, Discord, Discord.CurrentUser.Id, options); + + /// + /// Gets the owner of this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the owner of this guild. + /// + public Task GetOwnerAsync(RequestOptions options = null) + => GuildHelper.GetUserAsync(this, Discord, OwnerId, options); + + /// + /// + /// Prunes inactive users. + /// + /// + /// + /// This method removes all users that have not logged on in the provided number of . + /// + /// + /// If is true, this method will only return the number of users that + /// would be removed without kicking the users. + /// + /// + /// The number of days required for the users to be kicked. + /// Whether this prune action is a simulation. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous prune operation. The task result contains the number of users to + /// be or has been removed from this guild. + /// + public Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null) + => GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options); + + //Audit logs + /// + /// Gets the specified number of audit log entries for this guild. + /// + /// The number of audit log entries to fetch. + /// The options to be used when sending the request. + /// The audit log entry ID to get entries before. + /// The type of actions to filter. + /// The user ID to filter entries for. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of the requested audit log entries. + /// + public IAsyncEnumerable> GetAuditLogsAsync(int limit, RequestOptions options = null, ulong? beforeId = null, ulong? userId = null, ActionType? actionType = null) + => GuildHelper.GetAuditLogsAsync(this, Discord, beforeId, limit, options, userId: userId, actionType: actionType); + + //Webhooks + /// + /// Gets a webhook found within this guild. + /// + /// The identifier for the webhook. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the webhook with the + /// specified ; null if none is found. + /// + public Task GetWebhookAsync(ulong id, RequestOptions options = null) + => GuildHelper.GetWebhookAsync(this, Discord, id, options); + + /// + /// Gets a collection of all webhook from this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of webhooks found within the guild. + /// + public Task> GetWebhooksAsync(RequestOptions options = null) + => GuildHelper.GetWebhooksAsync(this, Discord, options); + + /// + /// Returns the name of the guild. + /// + /// + /// The name of the guild. + /// + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id})"; + + //Emotes + /// + public Task GetEmoteAsync(ulong id, RequestOptions options = null) + => GuildHelper.GetEmoteAsync(this, Discord, id, options); + /// + public Task CreateEmoteAsync(string name, Image image, Optional> roles = default(Optional>), RequestOptions options = null) + => GuildHelper.CreateEmoteAsync(this, Discord, name, image, roles, options); + /// + /// is null. + public Task ModifyEmoteAsync(GuildEmote emote, Action func, RequestOptions options = null) + => GuildHelper.ModifyEmoteAsync(this, Discord, emote.Id, func, options); + /// + public Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null) + => GuildHelper.DeleteEmoteAsync(this, Discord, emote.Id, options); + + //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.GetBanAsync(IUser user, RequestOptions options) + => await GetBanAsync(user, options).ConfigureAwait(false); + /// + async Task IGuild.GetBanAsync(ulong userId, RequestOptions options) + => await GetBanAsync(userId, 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.GetTextChannelsAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetTextChannelsAsync(options).ConfigureAwait(false); + else + return ImmutableArray.Create(); + } + /// + async Task IGuild.GetTextChannelAsync(ulong id, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetTextChannelAsync(id, options).ConfigureAwait(false); + else + return null; + } + /// + async Task> IGuild.GetVoiceChannelsAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetVoiceChannelsAsync(options).ConfigureAwait(false); + else + return ImmutableArray.Create(); + } + /// + async Task> IGuild.GetCategoriesAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetCategoryChannelsAsync(options).ConfigureAwait(false); + else + return null; + } + /// + async Task IGuild.GetVoiceChannelAsync(ulong id, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetVoiceChannelAsync(id, options).ConfigureAwait(false); + else + return null; + } + /// + async Task IGuild.GetAFKChannelAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetAFKChannelAsync(options).ConfigureAwait(false); + else + return null; + } + /// + async Task IGuild.GetDefaultChannelAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetDefaultChannelAsync(options).ConfigureAwait(false); + else + return null; + } + /// + async Task IGuild.GetEmbedChannelAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetEmbedChannelAsync(options).ConfigureAwait(false); + else + return null; + } + /// + async Task IGuild.GetSystemChannelAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetSystemChannelAsync(options).ConfigureAwait(false); + else + return null; + } + /// + async Task IGuild.CreateTextChannelAsync(string name, Action func, RequestOptions options) + => await CreateTextChannelAsync(name, func, options).ConfigureAwait(false); + /// + async Task IGuild.CreateVoiceChannelAsync(string name, Action func, RequestOptions options) + => await CreateVoiceChannelAsync(name, func, options).ConfigureAwait(false); + /// + async Task IGuild.CreateCategoryAsync(string name, Action func, RequestOptions options) + => await CreateCategoryChannelAsync(name, func, options).ConfigureAwait(false); + + /// + async Task> IGuild.GetVoiceRegionsAsync(RequestOptions options) + => await GetVoiceRegionsAsync(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); + /// + async Task IGuild.GetVanityInviteAsync(RequestOptions options) + => await GetVanityInviteAsync(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, false, options).ConfigureAwait(false); + /// + async Task IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted, bool isMentionable, RequestOptions options) + => await CreateRoleAsync(name, permissions, color, isHoisted, isMentionable, options).ConfigureAwait(false); + + /// + async Task IGuild.AddGuildUserAsync(ulong userId, string accessToken, Action func, RequestOptions options) + => await AddGuildUserAsync(userId, accessToken, func, options); + + /// + 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.GetOwnerAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetOwnerAsync(options).ConfigureAwait(false); + else + return null; + } + /// + async Task> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return (await GetUsersAsync(options).FlattenAsync().ConfigureAwait(false)).ToImmutableArray(); + else + return ImmutableArray.Create(); + } + /// + /// Downloading users is not supported for a REST-based guild. + Task IGuild.DownloadUsersAsync() => + throw new NotSupportedException(); + + async Task> IGuild.GetAuditLogsAsync(int limit, CacheMode cacheMode, RequestOptions options, + ulong? beforeId, ulong? userId, ActionType? actionType) + { + if (cacheMode == CacheMode.AllowDownload) + return (await GetAuditLogsAsync(limit, options, beforeId: beforeId, userId: userId, actionType: actionType).FlattenAsync().ConfigureAwait(false)).ToImmutableArray(); + else + return ImmutableArray.Create(); + } + + /// + async Task IGuild.GetWebhookAsync(ulong id, RequestOptions options) + => await GetWebhookAsync(id, options).ConfigureAwait(false); + /// + async Task> IGuild.GetWebhooksAsync(RequestOptions options) + => await GetWebhooksAsync(options).ConfigureAwait(false); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Guilds/RestGuildEmbed.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Guilds/RestGuildEmbed.cs new file mode 100644 index 0000000..41c76eb --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Guilds/RestGuildEmbed.cs @@ -0,0 +1,25 @@ +using System.Diagnostics; +using Model = Discord.API.GuildEmbed; + +namespace Discord.Rest +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public struct RestGuildEmbed + { + public bool IsEnabled { get; private set; } + public ulong? ChannelId { get; private set; } + + internal RestGuildEmbed(bool isEnabled, ulong? channelId) + { + ChannelId = channelId; + IsEnabled = isEnabled; + } + internal static RestGuildEmbed Create(Model model) + { + return new RestGuildEmbed(model.Enabled, model.ChannelId); + } + + public override string ToString() => ChannelId?.ToString() ?? "Unknown"; + private string DebuggerDisplay => $"{ChannelId} ({(IsEnabled ? "Enabled" : "Disabled")})"; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs new file mode 100644 index 0000000..9759e64 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs @@ -0,0 +1,104 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.Integration; + +namespace Discord.Rest +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestGuildIntegration : RestEntity, IGuildIntegration + { + private long _syncedAtTicks; + + /// + public string Name { get; private set; } + /// + public string Type { get; private set; } + /// + public bool IsEnabled { get; private set; } + /// + public bool IsSyncing { get; private set; } + /// + public ulong ExpireBehavior { get; private set; } + /// + public ulong ExpireGracePeriod { get; private set; } + /// + public ulong GuildId { get; private set; } + /// + public ulong RoleId { get; private set; } + public RestUser User { get; private set; } + /// + public IntegrationAccount Account { get; private set; } + internal IGuild Guild { get; private set; } + + /// + public DateTimeOffset SyncedAt => DateTimeUtils.FromTicks(_syncedAtTicks); + + internal RestGuildIntegration(BaseDiscordClient discord, IGuild guild, ulong id) + : base(discord, id) + { + Guild = guild; + } + internal static RestGuildIntegration Create(BaseDiscordClient discord, IGuild guild, Model model) + { + var entity = new RestGuildIntegration(discord, guild, model.Id); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + Name = model.Name; + Type = model.Type; + IsEnabled = model.Enabled; + IsSyncing = model.Syncing; + ExpireBehavior = model.ExpireBehavior; + ExpireGracePeriod = model.ExpireGracePeriod; + _syncedAtTicks = model.SyncedAt.UtcTicks; + + RoleId = model.RoleId; + User = RestUser.Create(Discord, model.User); + } + + public async Task DeleteAsync() + { + await Discord.ApiClient.DeleteGuildIntegrationAsync(GuildId, Id).ConfigureAwait(false); + } + public async Task ModifyAsync(Action func) + { + if (func == null) throw new NullReferenceException(nameof(func)); + + var args = new GuildIntegrationProperties(); + func(args); + var apiArgs = new API.Rest.ModifyGuildIntegrationParams + { + EnableEmoticons = args.EnableEmoticons, + ExpireBehavior = args.ExpireBehavior, + ExpireGracePeriod = args.ExpireGracePeriod + }; + var model = await Discord.ApiClient.ModifyGuildIntegrationAsync(GuildId, Id, apiArgs).ConfigureAwait(false); + + Update(model); + } + public async Task SyncAsync() + { + await Discord.ApiClient.SyncGuildIntegrationAsync(GuildId, Id).ConfigureAwait(false); + } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id}{(IsEnabled ? ", Enabled" : "")})"; + + /// + IGuild IGuildIntegration.Guild + { + get + { + if (Guild != null) + return Guild; + throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); + } + } + /// + IUser IGuildIntegration.User => User; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs new file mode 100644 index 0000000..b75d628 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs @@ -0,0 +1,57 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.UserGuild; + +namespace Discord.Rest +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestUserGuild : RestEntity, IUserGuild + { + private string _iconId; + + /// + public string Name { get; private set; } + /// + public bool IsOwner { get; private set; } + /// + public GuildPermissions Permissions { get; private set; } + + /// + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + /// + public string IconUrl => CDN.GetGuildIconUrl(Id, _iconId); + + internal RestUserGuild(BaseDiscordClient discord, ulong id) + : base(discord, id) + { + } + internal static RestUserGuild Create(BaseDiscordClient discord, Model model) + { + var entity = new RestUserGuild(discord, model.Id); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + _iconId = model.Icon; + IsOwner = model.Owner; + Name = model.Name; + Permissions = new GuildPermissions(model.Permissions); + } + + public async Task LeaveAsync(RequestOptions options = null) + { + await Discord.ApiClient.LeaveGuildAsync(Id, options).ConfigureAwait(false); + } + /// + public async Task DeleteAsync(RequestOptions options = null) + { + await Discord.ApiClient.DeleteGuildAsync(Id, options).ConfigureAwait(false); + } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id}{(IsOwner ? ", Owned" : "")})"; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Guilds/RestVoiceRegion.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Guilds/RestVoiceRegion.cs new file mode 100644 index 0000000..a363f05 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Guilds/RestVoiceRegion.cs @@ -0,0 +1,46 @@ +using Discord.Rest; +using System.Diagnostics; +using Model = Discord.API.VoiceRegion; + +namespace Discord.Rest +{ + /// + /// Represents a REST-based voice region. + /// + [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 bool IsDeprecated { get; private set; } + /// + public bool IsCustom { 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; + IsDeprecated = model.IsDeprecated; + IsCustom = model.IsCustom; + } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id}{(IsVip ? ", VIP" : "")}{(IsOptimal ? ", Optimal" : "")})"; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Invites/InviteHelper.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Invites/InviteHelper.cs new file mode 100644 index 0000000..ebcd937 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Invites/InviteHelper.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; + +namespace Discord.Rest +{ + internal static class InviteHelper + { + public static async Task DeleteAsync(IInvite invite, BaseDiscordClient client, + RequestOptions options) + { + await client.ApiClient.DeleteInviteAsync(invite.Code, options).ConfigureAwait(false); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Invites/RestInvite.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Invites/RestInvite.cs new file mode 100644 index 0000000..153eb6c --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Invites/RestInvite.cs @@ -0,0 +1,97 @@ +using System; +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 ChannelType ChannelType { get; private set; } + /// + public string ChannelName { get; private set; } + /// + public string GuildName { get; private set; } + /// + public int? PresenceCount { get; private set; } + /// + public int? MemberCount { get; private set; } + /// + public ulong ChannelId { get; private set; } + /// + public ulong? GuildId { get; private set; } + internal IChannel Channel { get; } + internal IGuild Guild { get; } + + /// + public string Code => Id; + /// + public string Url => $"{DiscordConfig.InviteUrl}{Code}"; + + internal RestInvite(BaseDiscordClient discord, IGuild guild, IChannel channel, string id) + : base(discord, id) + { + Guild = guild; + Channel = channel; + } + internal static RestInvite Create(BaseDiscordClient discord, IGuild guild, IChannel channel, Model model) + { + var entity = new RestInvite(discord, guild, channel, model.Code); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { + GuildId = model.Guild.IsSpecified ? model.Guild.Value.Id : default(ulong?); + ChannelId = model.Channel.Id; + GuildName = model.Guild.IsSpecified ? model.Guild.Value.Name : null; + ChannelName = model.Channel.Name; + MemberCount = model.MemberCount.IsSpecified ? model.MemberCount.Value : null; + PresenceCount = model.PresenceCount.IsSpecified ? model.PresenceCount.Value : null; + ChannelType = (ChannelType)model.Channel.Type; + } + + /// + 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); + + /// + /// Gets the URL of the invite. + /// + /// + /// A string that resolves to the Url of the invite. + /// + public override string ToString() => Url; + private string DebuggerDisplay => $"{Url} ({GuildName} / {ChannelName})"; + + /// + IGuild IInvite.Guild + { + get + { + if (Guild != null) + return Guild; + if (Channel is IGuildChannel guildChannel) + return guildChannel.Guild; //If it fails, it'll still return this exception + throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); + } + } + /// + IChannel IInvite.Channel + { + get + { + if (Channel != null) + return Channel; + throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); + } + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs new file mode 100644 index 0000000..55acd5f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs @@ -0,0 +1,54 @@ +using System; +using Model = Discord.API.InviteMetadata; + +namespace Discord.Rest +{ + /// Represents additional information regarding the REST-based invite object. + public class RestInviteMetadata : RestInvite, IInviteMetadata + { + private long? _createdAtTicks; + + /// + public bool IsRevoked { get; private set; } + /// + public bool IsTemporary { get; private set; } + /// + public int? MaxAge { get; private set; } + /// + public int? MaxUses { get; private set; } + /// + public int? Uses { get; private set; } + /// + /// Gets the user that created this invite. + /// + public RestUser Inviter { get; private set; } + + /// + public DateTimeOffset? CreatedAt => DateTimeUtils.FromTicks(_createdAtTicks); + + internal RestInviteMetadata(BaseDiscordClient discord, IGuild guild, IChannel channel, string id) + : base(discord, guild, channel, id) + { + } + internal static RestInviteMetadata Create(BaseDiscordClient discord, IGuild guild, IChannel channel, Model model) + { + var entity = new RestInviteMetadata(discord, guild, channel, model.Code); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { + base.Update(model); + Inviter = model.Inviter != null ? RestUser.Create(Discord, model.Inviter) : null; + IsRevoked = model.Revoked; + IsTemporary = model.Temporary; + MaxAge = model.MaxAge.IsSpecified ? model.MaxAge.Value : (int?)null; + MaxUses = model.MaxUses.IsSpecified ? model.MaxUses.Value : (int?)null; + Uses = model.Uses.IsSpecified ? model.Uses.Value : (int?)null; + _createdAtTicks = model.CreatedAt.IsSpecified ? model.CreatedAt.Value.UtcTicks : (long?)null; + } + + /// + IUser IInviteMetadata.Inviter => Inviter; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Messages/Attachment.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Messages/Attachment.cs new file mode 100644 index 0000000..abe0491 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Messages/Attachment.cs @@ -0,0 +1,51 @@ +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); + } + + /// + /// Returns the filename of this attachment. + /// + /// + /// A string containing the filename of this attachment. + /// + public override string ToString() => Filename; + private string DebuggerDisplay => $"{Filename} ({Size} bytes)"; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Messages/MessageHelper.cs new file mode 100644 index 0000000..75892de --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -0,0 +1,278 @@ +using Discord.API.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Model = Discord.API.Message; + +namespace Discord.Rest +{ + internal static class MessageHelper + { + /// + /// Regex used to check if some text is formatted as inline code. + /// + private static readonly Regex InlineCodeRegex = new Regex(@"[^\\]?(`).+?[^\\](`)", RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.Singleline); + + /// + /// Regex used to check if some text is formatted as a code block. + /// + private static readonly Regex BlockCodeRegex = new Regex(@"[^\\]?(```).+?[^\\](```)", RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.Singleline); + + /// Only the author of a message may modify the message. + /// Message content is too long, length must be less or equal to . + public static async Task ModifyAsync(IMessage msg, BaseDiscordClient client, Action func, + RequestOptions options) + { + if (msg.Author.Id != client.CurrentUser.Id) + throw new InvalidOperationException("Only the author of a message may modify the message."); + + var args = new MessageProperties(); + func(args); + + var apiArgs = new API.Rest.ModifyMessageParams + { + Content = args.Content, + Embed = args.Embed.IsSpecified ? args.Embed.Value.ToModel() : Optional.Create() + }; + return await client.ApiClient.ModifyMessageAsync(msg.Channel.Id, msg.Id, apiArgs, options).ConfigureAwait(false); + } + public static Task DeleteAsync(IMessage msg, BaseDiscordClient client, RequestOptions options) + => DeleteAsync(msg.Channel.Id, msg.Id, client, options); + public static async Task DeleteAsync(ulong channelId, ulong msgId, BaseDiscordClient client, + RequestOptions options) + { + await client.ApiClient.DeleteMessageAsync(channelId, msgId, options).ConfigureAwait(false); + } + + public static async Task SuppressEmbedsAsync(IMessage msg, BaseDiscordClient client, bool suppress, RequestOptions options) + { + var apiArgs = new API.Rest.SuppressEmbedParams + { + Suppressed = suppress + }; + await client.ApiClient.SuppressEmbedAsync(msg.Channel.Id, msg.Id, apiArgs, options).ConfigureAwait(false); + } + + public static async Task AddReactionAsync(IMessage msg, IEmote emote, BaseDiscordClient client, RequestOptions options) + { + await client.ApiClient.AddReactionAsync(msg.Channel.Id, msg.Id, emote is Emote e ? $"{e.Name}:{e.Id}" : emote.Name, options).ConfigureAwait(false); + } + + public static async Task RemoveReactionAsync(IMessage msg, ulong userId, IEmote emote, BaseDiscordClient client, RequestOptions options) + { + await client.ApiClient.RemoveReactionAsync(msg.Channel.Id, msg.Id, userId, emote is Emote e ? $"{e.Name}:{e.Id}" : emote.Name, options).ConfigureAwait(false); + } + + public static async Task RemoveAllReactionsAsync(IMessage msg, BaseDiscordClient client, RequestOptions options) + { + await client.ApiClient.RemoveAllReactionsAsync(msg.Channel.Id, msg.Id, options).ConfigureAwait(false); + } + + public static IAsyncEnumerable> GetReactionUsersAsync(IMessage msg, IEmote emote, + int? limit, BaseDiscordClient client, RequestOptions options) + { + Preconditions.NotNull(emote, nameof(emote)); + var emoji = (emote is Emote e ? $"{e.Name}:{e.Id}" : emote.Name); + + return new PagedAsyncEnumerable( + DiscordConfig.MaxUserReactionsPerBatch, + async (info, ct) => + { + var args = new GetReactionUsersParams + { + Limit = info.PageSize + }; + + if (info.Position != null) + args.AfterUserId = info.Position.Value; + + var models = await client.ApiClient.GetReactionUsersAsync(msg.Channel.Id, msg.Id, emoji, args, options).ConfigureAwait(false); + return models.Select(x => RestUser.Create(client, x)).ToImmutableArray(); + }, + nextPage: (info, lastPage) => + { + if (lastPage.Count != DiscordConfig.MaxUserReactionsPerBatch) + return false; + + info.Position = lastPage.Max(x => x.Id); + return true; + }, + count: limit + ); + + } + + 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, IReadOnlyCollection userMentions) + { + var tags = ImmutableArray.CreateBuilder(); + int index = 0; + var codeIndex = 0; + + // checks if the tag being parsed is wrapped in code blocks + bool CheckWrappedCode() + { + // util to check if the index of a tag is within the bounds of the codeblock + bool EnclosedInBlock(Match m) + => m.Groups[1].Index < index && index < m.Groups[2].Index; + + // loop through all code blocks that are before the start of the tag + while (codeIndex < index) + { + var blockMatch = BlockCodeRegex.Match(text, codeIndex); + if (blockMatch.Success) + { + if (EnclosedInBlock(blockMatch)) + return true; + // continue if the end of the current code was before the start of the tag + codeIndex += blockMatch.Groups[2].Index + blockMatch.Groups[2].Length; + if (codeIndex < index) + continue; + return false; + } + var inlineMatch = InlineCodeRegex.Match(text, codeIndex); + if (inlineMatch.Success) + { + if (EnclosedInBlock(inlineMatch)) + return true; + // continue if the end of the current code was before the start of the tag + codeIndex += inlineMatch.Groups[2].Index + inlineMatch.Groups[2].Length; + if (codeIndex < index) + continue; + return false; + } + return false; + } + return false; + } + + while (true) + { + index = text.IndexOf('<', index); + if (index == -1) break; + int endIndex = text.IndexOf('>', index + 1); + if (endIndex == -1) break; + if (CheckWrappedCode()) break; + string content = text.Substring(index, endIndex - index + 1); + + if (MentionUtils.TryParseUser(content, out ulong 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(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(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(new Tag(TagType.RoleMention, index, content.Length, id, mentionedRole)); + } + else if (Emote.TryParse(content, out var emoji)) + tags.Add(new Tag(TagType.Emoji, index, content.Length, emoji.Id, emoji)); + else //Bad Tag + { + index = index + 1; + continue; + } + index = endIndex + 1; + } + + index = 0; + codeIndex = 0; + while (true) + { + index = text.IndexOf("@everyone", index); + if (index == -1) break; + if (CheckWrappedCode()) break; + var tagIndex = FindIndex(tags, index); + if (tagIndex.HasValue) + tags.Insert(tagIndex.Value, new Tag(TagType.EveryoneMention, index, "@everyone".Length, 0, guild?.EveryoneRole)); + index++; + } + + index = 0; + codeIndex = 0; + while (true) + { + index = text.IndexOf("@here", index); + if (index == -1) break; + if (CheckWrappedCode()) break; + var tagIndex = FindIndex(tags, index); + if (tagIndex.HasValue) + tags.Insert(tagIndex.Value, new Tag(TagType.HereMention, index, "@here".Length, 0, guild?.EveryoneRole)); + index++; + } + + return tags.ToImmutable(); + } + private static int? FindIndex(IReadOnlyList tags, int index) + { + int i = 0; + for (; i < tags.Count; i++) + { + var tag = tags[i]; + if (index < tag.Index) + break; //Position before this tag + } + if (i > 0 && index < tags[i - 1].Index + tags[i - 1].Length) + return null; //Overlaps tag before this + return i; + } + 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(); + } + + public static MessageSource GetSource(Model msg) + { + if (msg.Type != MessageType.Default) + return MessageSource.System; + else if (msg.WebhookId.IsSpecified) + return MessageSource.Webhook; + else if (msg.Author.GetValueOrDefault()?.Bot.GetValueOrDefault(false) == true) + return MessageSource.Bot; + return MessageSource.User; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Messages/RestMessage.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Messages/RestMessage.cs new file mode 100644 index 0000000..f457f4f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Messages/RestMessage.cs @@ -0,0 +1,188 @@ +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 +{ + /// + /// Represents a REST-based message. + /// + public abstract class RestMessage : RestEntity, IMessage, IUpdateable + { + private long _timestampTicks; + private ImmutableArray _reactions = ImmutableArray.Create(); + + /// + public IMessageChannel Channel { get; } + /// + /// Gets the Author of the message. + /// + public IUser Author { get; } + /// + public MessageSource Source { get; } + + /// + public string Content { get; private set; } + + /// + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + /// + public virtual bool IsTTS => false; + /// + public virtual bool IsPinned => false; + /// + public virtual bool IsSuppressed => false; + /// + public virtual DateTimeOffset? EditedTimestamp => null; + /// + /// Gets a collection of the 's on the message. + /// + public virtual IReadOnlyCollection Attachments => ImmutableArray.Create(); + /// + /// Gets a collection of the 's on the message. + /// + public virtual IReadOnlyCollection Embeds => ImmutableArray.Create(); + /// + public virtual IReadOnlyCollection MentionedChannelIds => ImmutableArray.Create(); + /// + public virtual IReadOnlyCollection MentionedRoleIds => ImmutableArray.Create(); + /// + /// Gets a collection of the mentioned users in the message. + /// + public virtual IReadOnlyCollection MentionedUsers => ImmutableArray.Create(); + /// + public virtual IReadOnlyCollection Tags => ImmutableArray.Create(); + + /// + public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); + /// + public MessageActivity Activity { get; private set; } + /// + public MessageApplication Application { get; private set; } + /// + public MessageReference Reference { get; private set; } + + internal RestMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author, MessageSource source) + : base(discord, id) + { + Channel = channel; + Author = author; + Source = source; + } + internal static RestMessage Create(BaseDiscordClient discord, IMessageChannel channel, IUser author, Model model) + { + if (model.Type == MessageType.Default) + return RestUserMessage.Create(discord, channel, author, model); + else + return RestSystemMessage.Create(discord, channel, author, model); + } + internal virtual void Update(Model model) + { + if (model.Timestamp.IsSpecified) + _timestampTicks = model.Timestamp.Value.UtcTicks; + + if (model.Content.IsSpecified) + Content = model.Content.Value; + + if (model.Application.IsSpecified) + { + // create a new Application from the API model + Application = new MessageApplication() + { + Id = model.Application.Value.Id, + CoverImage = model.Application.Value.CoverImage, + Description = model.Application.Value.Description, + Icon = model.Application.Value.Icon, + Name = model.Application.Value.Name + }; + } + + if (model.Activity.IsSpecified) + { + // create a new Activity from the API model + Activity = new MessageActivity() + { + Type = model.Activity.Value.Type.Value, + PartyId = model.Activity.Value.PartyId.GetValueOrDefault() + }; + } + + if(model.Reference.IsSpecified) + { + // Creates a new Reference from the API model + Reference = new MessageReference + { + GuildId = model.Reference.Value.GuildId, + ChannelId = model.Reference.Value.ChannelId, + MessageId = model.Reference.Value.MessageId + }; + } + + if (model.Reactions.IsSpecified) + { + var value = model.Reactions.Value; + if (value.Length > 0) + { + var reactions = ImmutableArray.CreateBuilder(value.Length); + for (int i = 0; i < value.Length; i++) + reactions.Add(RestReaction.Create(value[i])); + _reactions = reactions.ToImmutable(); + } + else + _reactions = ImmutableArray.Create(); + } + else + _reactions = ImmutableArray.Create(); + } + + /// + public async Task UpdateAsync(RequestOptions options = null) + { + var model = await Discord.ApiClient.GetChannelMessageAsync(Channel.Id, Id, options).ConfigureAwait(false); + Update(model); + } + /// + public Task DeleteAsync(RequestOptions options = null) + => MessageHelper.DeleteAsync(this, Discord, options); + + /// + /// Gets the of the message. + /// + /// + /// A string that is the of the message. + /// + 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(); + + /// + public IReadOnlyDictionary Reactions => _reactions.ToDictionary(x => x.Emote, x => new ReactionMetadata { ReactionCount = x.Count, IsMe = x.Me }); + + /// + public Task AddReactionAsync(IEmote emote, RequestOptions options = null) + => MessageHelper.AddReactionAsync(this, emote, Discord, options); + /// + public Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null) + => MessageHelper.RemoveReactionAsync(this, user.Id, emote, Discord, options); + /// + public Task RemoveReactionAsync(IEmote emote, ulong userId, RequestOptions options = null) + => MessageHelper.RemoveReactionAsync(this, userId, emote, Discord, options); + /// + public Task RemoveAllReactionsAsync(RequestOptions options = null) + => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); + /// + public IAsyncEnumerable> GetReactionUsersAsync(IEmote emote, int limit, RequestOptions options = null) + => MessageHelper.GetReactionUsersAsync(this, emote, limit, Discord, options); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Messages/RestReaction.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Messages/RestReaction.cs new file mode 100644 index 0000000..c38efe3 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Messages/RestReaction.cs @@ -0,0 +1,37 @@ +using Model = Discord.API.Reaction; + +namespace Discord.Rest +{ + /// + /// Represents a REST reaction object. + /// + public class RestReaction : IReaction + { + /// + public IEmote Emote { get; } + /// + /// Gets the number of reactions added. + /// + public int Count { get; } + /// + /// Gets whether the reactions is added by the user. + /// + public bool Me { get; } + + internal RestReaction(IEmote emote, int count, bool me) + { + Emote = emote; + Count = count; + Me = me; + } + internal static RestReaction Create(Model model) + { + IEmote emote; + if (model.Emoji.Id.HasValue) + emote = new Emote(model.Emoji.Id.Value, model.Emoji.Name, model.Emoji.Animated.GetValueOrDefault()); + else + emote = new Emoji(model.Emoji.Name); + return new RestReaction(emote, model.Count, model.Me); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs new file mode 100644 index 0000000..89a651e --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs @@ -0,0 +1,34 @@ +using System.Diagnostics; +using Model = Discord.API.Message; + +namespace Discord.Rest +{ + /// + /// Represents a REST-based system message. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestSystemMessage : RestMessage, ISystemMessage + { + /// + public MessageType Type { get; private set; } + + internal RestSystemMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author) + : base(discord, id, channel, author, MessageSource.System) + { + } + internal new static RestSystemMessage Create(BaseDiscordClient discord, IMessageChannel channel, IUser author, Model model) + { + var entity = new RestSystemMessage(discord, model.Id, channel, author); + 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/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs new file mode 100644 index 0000000..7d65268 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs @@ -0,0 +1,153 @@ +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 +{ + /// + /// Represents a REST-based message sent by a user. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestUserMessage : RestMessage, IUserMessage + { + private bool _isMentioningEveryone, _isTTS, _isPinned, _isSuppressed; + private long? _editedTimestampTicks; + private ImmutableArray _attachments = ImmutableArray.Create(); + private ImmutableArray _embeds = ImmutableArray.Create(); + private ImmutableArray _tags = ImmutableArray.Create(); + + /// + public override bool IsTTS => _isTTS; + /// + public override bool IsPinned => _isPinned; + /// + public override bool IsSuppressed => _isSuppressed; + /// + 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, IUser author, MessageSource source) + : base(discord, id, channel, author, source) + { + } + internal new static RestUserMessage Create(BaseDiscordClient discord, IMessageChannel channel, IUser author, Model model) + { + var entity = new RestUserMessage(discord, model.Id, channel, author, MessageHelper.GetSource(model)); + 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.Flags.IsSpecified) + { + _isSuppressed = model.Flags.Value.HasFlag(API.MessageFlags.Suppressed); + } + + 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(value[i].ToEntity()); + _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; + var guildId = (Channel as IGuildChannel)?.GuildId; + var guild = guildId != null ? (Discord as IDiscordClient).GetGuildAsync(guildId.Value, CacheMode.CacheOnly).Result : null; + _tags = MessageHelper.ParseTags(text, null, guild, mentions); + model.Content = text; + } + } + + /// + public async Task ModifyAsync(Action func, RequestOptions options = null) + { + var model = await MessageHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); + Update(model); + } + + /// + public Task PinAsync(RequestOptions options = null) + => MessageHelper.PinAsync(this, Discord, options); + /// + public Task UnpinAsync(RequestOptions options = null) + => MessageHelper.UnpinAsync(this, Discord, options); + /// + public Task ModifySuppressionAsync(bool suppressEmbeds, RequestOptions options = null) + => MessageHelper.SuppressEmbedsAsync(this, Discord, suppressEmbeds, options); + + public string Resolve(int startIndex, TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, + TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) + => MentionUtils.Resolve(this, startIndex, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); + /// + 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, 0, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); + + private string DebuggerDisplay => $"{Author}: {Content} ({Id}{(Attachments.Count > 0 ? $", {Attachments.Count} Attachments" : "")})"; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/RestApplication.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/RestApplication.cs new file mode 100644 index 0000000..d033978 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/RestApplication.cs @@ -0,0 +1,74 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.Application; + +namespace Discord.Rest +{ + /// + /// Represents a REST-based entity that contains information about a Discord application created via the developer portal. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestApplication : RestEntity, IApplication + { + protected string _iconId; + + /// + public string Name { get; private set; } + /// + public string Description { get; private set; } + /// + public string[] RPCOrigins { get; private set; } + /// + public ulong Flags { get; private set; } + + /// + public IUser Owner { get; private set; } + + /// + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + /// + public string IconUrl => CDN.GetApplicationIconUrl(Id, _iconId); + + internal RestApplication(BaseDiscordClient discord, ulong id) + : base(discord, id) + { + } + internal static RestApplication Create(BaseDiscordClient discord, Model model) + { + 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; + _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); + } + + /// Unable to update this object from a different application token. + public async Task UpdateAsync() + { + 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); + } + + /// + /// Gets the name of the application. + /// + /// + /// Name of the application. + /// + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id})"; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/RestEntity.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/RestEntity.cs new file mode 100644 index 0000000..2b1bb88 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/RestEntity.cs @@ -0,0 +1,17 @@ +using System; + +namespace Discord.Rest +{ + public abstract class RestEntity : IEntity + where T : IEquatable + { + internal BaseDiscordClient Discord { get; } + public T Id { get; } + + internal RestEntity(BaseDiscordClient discord, T id) + { + Discord = discord; + Id = id; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Roles/RestRole.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Roles/RestRole.cs new file mode 100644 index 0000000..7c1a3aa --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Roles/RestRole.cs @@ -0,0 +1,95 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.Role; + +namespace Discord.Rest +{ + /// + /// Represents a REST-based role. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestRole : RestEntity, IRole + { + internal IGuild 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 => SnowflakeUtils.FromSnowflake(Id); + /// + /// Gets if this role is the @everyone role of the guild or not. + /// + public bool IsEveryone => Id == Guild.Id; + /// + public string Mention => IsEveryone ? "@everyone" : MentionUtils.MentionRole(Id); + + internal RestRole(BaseDiscordClient discord, IGuild guild, ulong id) + : base(discord, id) + { + Guild = guild; + } + internal static RestRole Create(BaseDiscordClient discord, IGuild guild, Model model) + { + var entity = new RestRole(discord, guild, 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 int CompareTo(IRole role) => RoleUtils.Compare(this, role); + + /// + /// Gets the name of the role. + /// + /// + /// A string that is the name of the role. + /// + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id})"; + + //IRole + /// + IGuild IRole.Guild + { + get + { + if (Guild != null) + return Guild; + throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); + } + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Roles/RoleHelper.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Roles/RoleHelper.cs new file mode 100644 index 0000000..d570f07 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Roles/RoleHelper.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading.Tasks; +using Model = Discord.API.Role; +using BulkParams = Discord.API.Rest.ModifyGuildRolesParams; + +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 RoleProperties(); + func(args); + var apiArgs = new API.Rest.ModifyGuildRoleParams + { + Color = args.Color.IsSpecified ? args.Color.Value.RawValue : Optional.Create(), + Hoist = args.Hoist, + Mentionable = args.Mentionable, + Name = args.Name, + Permissions = args.Permissions.IsSpecified ? args.Permissions.Value.RawValue : Optional.Create() + }; + var model = await client.ApiClient.ModifyGuildRoleAsync(role.Guild.Id, role.Id, apiArgs, options).ConfigureAwait(false); + + if (args.Position.IsSpecified) + { + var bulkArgs = new[] { new BulkParams(role.Id, args.Position.Value) }; + await client.ApiClient.ModifyGuildRolesAsync(role.Guild.Id, bulkArgs, options).ConfigureAwait(false); + model.Position = args.Position.Value; + } + return model; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Users/RestConnection.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Users/RestConnection.cs new file mode 100644 index 0000000..1afb813 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Users/RestConnection.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using Model = Discord.API.Connection; + +namespace Discord.Rest +{ + [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()); + } + + /// + /// Gets the name of the connection. + /// + /// + /// Name of the connection. + /// + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id}, {Type}{(IsRevoked ? ", Revoked" : "")})"; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Users/RestGroupUser.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Users/RestGroupUser.cs new file mode 100644 index 0000000..55e9843 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Users/RestGroupUser.cs @@ -0,0 +1,41 @@ +using System.Diagnostics; +using Model = Discord.API.User; + +namespace Discord.Rest +{ + /// + /// Represents a REST-based group user. + /// + [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; + /// + bool IVoiceState.IsStreaming => false; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Users/RestGuildUser.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Users/RestGuildUser.cs new file mode 100644 index 0000000..d6a8c2e --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Users/RestGuildUser.cs @@ -0,0 +1,157 @@ +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; + +namespace Discord.Rest +{ + /// + /// Represents a REST-based guild user. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestGuildUser : RestUser, IGuildUser + { + private long? _premiumSinceTicks; + 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 DateTimeOffset? PremiumSince => DateTimeUtils.FromTicks(_premiumSinceTicks); + /// + public ulong GuildId => Guild.Id; + + /// + /// Resolving permissions requires the parent guild to be downloaded. + 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) + { + base.Update(model.User); + if (model.JoinedAt.IsSpecified) + _joinedAtTicks = model.JoinedAt.Value.UtcTicks; + if (model.Nick.IsSpecified) + Nickname = model.Nick.Value; + if (model.Deaf.IsSpecified) + IsDeafened = model.Deaf.Value; + if (model.Mute.IsSpecified) + IsMuted = model.Mute.Value; + if (model.Roles.IsSpecified) + UpdateRoles(model.Roles.Value); + if (model.PremiumSince.IsSpecified) + _premiumSinceTicks = model.PremiumSince.Value?.UtcTicks; + } + 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.Nickname.IsSpecified) + Nickname = args.Nickname.Value; + if (args.Roles.IsSpecified) + UpdateRoles(args.Roles.Value.Select(x => x.Id).ToArray()); + else if (args.RoleIds.IsSpecified) + UpdateRoles(args.RoleIds.Value.ToArray()); + } + /// + public Task KickAsync(string reason = null, RequestOptions options = null) + => UserHelper.KickAsync(this, Discord, reason, options); + /// + public Task AddRoleAsync(IRole role, RequestOptions options = null) + => AddRolesAsync(new[] { role }, options); + /// + public Task AddRolesAsync(IEnumerable roles, RequestOptions options = null) + => UserHelper.AddRolesAsync(this, Discord, roles, options); + /// + public Task RemoveRoleAsync(IRole role, RequestOptions options = null) + => RemoveRolesAsync(new[] { role }, options); + /// + public Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null) + => UserHelper.RemoveRolesAsync(this, Discord, roles, options); + + /// + /// Resolving permissions requires the parent guild to be downloaded. + public ChannelPermissions GetPermissions(IGuildChannel channel) + { + var guildPerms = GuildPermissions; + return new ChannelPermissions(Permissions.ResolveChannel(Guild, this, channel, guildPerms.RawValue)); + } + + //IGuildUser + /// + IGuild IGuildUser.Guild + { + get + { + if (Guild != null) + return Guild; + throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); + } + } + + //IVoiceState + /// + bool IVoiceState.IsSelfDeafened => false; + /// + bool IVoiceState.IsSelfMuted => false; + /// + bool IVoiceState.IsSuppressed => false; + /// + IVoiceChannel IVoiceState.VoiceChannel => null; + /// + string IVoiceState.VoiceSessionId => null; + /// + bool IVoiceState.IsStreaming => false; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Users/RestSelfUser.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Users/RestSelfUser.cs new file mode 100644 index 0000000..b5ef01c --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Users/RestSelfUser.cs @@ -0,0 +1,76 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.User; + +namespace Discord.Rest +{ + /// + /// Represents the logged-in REST-based user. + /// + [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; } + /// + public UserProperties Flags { get; private set; } + /// + public PremiumType PremiumType { get; private set; } + /// + public string Locale { 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; + if (model.Flags.IsSpecified) + Flags = (UserProperties)model.Flags.Value; + if (model.PremiumType.IsSpecified) + PremiumType = model.PremiumType.Value; + if (model.Locale.IsSpecified) + Locale = model.Locale.Value; + } + + /// + /// Unable to update this object using a different token. + 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); + } + + /// + /// Unable to modify this object using a different token. + 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); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Users/RestUser.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Users/RestUser.cs new file mode 100644 index 0000000..d5fffca --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Globalization; +using System.Threading.Tasks; +using Model = Discord.API.User; + +namespace Discord.Rest +{ + /// + /// Represents a REST-based user. + /// + [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 DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + /// + public string Discriminator => DiscriminatorValue.ToString("D4"); + /// + public string Mention => MentionUtils.MentionUser(Id); + /// + public virtual IActivity Activity => null; + /// + public virtual UserStatus Status => UserStatus.Offline; + /// + public virtual IImmutableSet ActiveClients => ImmutableHashSet.Empty; + /// + public virtual bool IsWebhook => false; + + internal RestUser(BaseDiscordClient discord, ulong id) + : base(discord, id) + { + } + internal static RestUser Create(BaseDiscordClient discord, Model model) + => Create(discord, null, model, null); + internal static RestUser Create(BaseDiscordClient discord, IGuild guild, Model model, ulong? webhookId) + { + RestUser entity; + if (webhookId.HasValue) + entity = new RestWebhookUser(discord, guild, model.Id, webhookId.Value); + else + 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, NumberStyles.None, CultureInfo.InvariantCulture); + 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); + } + + /// + /// Returns a direct message channel to this user, or create one if it does not already exist. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a rest DM channel where the user is the recipient. + /// + public Task GetOrCreateDMChannelAsync(RequestOptions options = null) + => UserHelper.CreateDMChannelAsync(this, Discord, options); + + /// + public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + + /// + public string GetDefaultAvatarUrl() + => CDN.GetDefaultUserAvatarUrl(DiscriminatorValue); + + /// + /// Gets the Username#Discriminator of the user. + /// + /// + /// A string that resolves to Username#Discriminator of the user. + /// + public override string ToString() => $"{Username}#{Discriminator}"; + private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; + + //IUser + /// + async Task IUser.GetOrCreateDMChannelAsync(RequestOptions options) + => await GetOrCreateDMChannelAsync(options).ConfigureAwait(false); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs new file mode 100644 index 0000000..8462cb8 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.User; + +namespace Discord.Rest +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestWebhookUser : RestUser, IWebhookUser + { + /// + public ulong WebhookId { get; } + internal IGuild Guild { get; } + /// + public DateTimeOffset? PremiumSince { get; private set; } + + /// + public override bool IsWebhook => true; + /// + public ulong GuildId => Guild.Id; + + internal RestWebhookUser(BaseDiscordClient discord, IGuild guild, ulong id, ulong webhookId) + : base(discord, id) + { + Guild = guild; + WebhookId = webhookId; + } + internal static RestWebhookUser Create(BaseDiscordClient discord, IGuild guild, Model model, ulong webhookId) + { + var entity = new RestWebhookUser(discord, guild, model.Id, webhookId); + entity.Update(model); + return entity; + } + + //IGuildUser + /// + IGuild IGuildUser.Guild + { + get + { + if (Guild != null) + return Guild; + throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); + } + } + /// + IReadOnlyCollection IGuildUser.RoleIds => ImmutableArray.Create(); + /// + DateTimeOffset? IGuildUser.JoinedAt => null; + /// + string IGuildUser.Nickname => null; + /// + GuildPermissions IGuildUser.GuildPermissions => GuildPermissions.Webhook; + + /// + ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel) => Permissions.ToChannelPerms(channel, GuildPermissions.Webhook.RawValue); + /// + Task IGuildUser.KickAsync(string reason, RequestOptions options) => + throw new NotSupportedException("Webhook users cannot be kicked."); + + /// + Task IGuildUser.ModifyAsync(Action func, RequestOptions options) => + throw new NotSupportedException("Webhook users cannot be modified."); + + /// + Task IGuildUser.AddRoleAsync(IRole role, RequestOptions options) => + throw new NotSupportedException("Roles are not supported on webhook users."); + + /// + Task IGuildUser.AddRolesAsync(IEnumerable roles, RequestOptions options) => + throw new NotSupportedException("Roles are not supported on webhook users."); + + /// + Task IGuildUser.RemoveRoleAsync(IRole role, RequestOptions options) => + throw new NotSupportedException("Roles are not supported on webhook users."); + + /// + Task IGuildUser.RemoveRolesAsync(IEnumerable roles, RequestOptions options) => + throw new NotSupportedException("Roles are not supported on webhook users."); + + //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; + /// + bool IVoiceState.IsStreaming => false; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Users/UserHelper.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Users/UserHelper.cs new file mode 100644 index 0000000..58e8cd4 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Users/UserHelper.cs @@ -0,0 +1,88 @@ +using Discord.API.Rest; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Model = Discord.API.User; +using ImageModel = Discord.API.Image; +using System.Linq; + +namespace Discord.Rest +{ + internal static class UserHelper + { + public static async Task ModifyAsync(ISelfUser user, BaseDiscordClient client, Action func, + RequestOptions options) + { + var args = new SelfUserProperties(); + func(args); + var apiArgs = new API.Rest.ModifyCurrentUserParams + { + Avatar = args.Avatar.IsSpecified ? args.Avatar.Value?.ToModel() : Optional.Create(), + Username = args.Username + }; + + if (!apiArgs.Avatar.IsSpecified && user.AvatarId != null) + apiArgs.Avatar = new ImageModel(user.AvatarId); + + return await client.ApiClient.ModifySelfAsync(apiArgs, options).ConfigureAwait(false); + } + public static async Task ModifyAsync(IGuildUser user, BaseDiscordClient client, Action func, + RequestOptions options) + { + var args = new GuildUserProperties(); + func(args); + var apiArgs = new API.Rest.ModifyGuildMemberParams + { + Deaf = args.Deaf, + Mute = args.Mute, + Nickname = args.Nickname + }; + + if (args.Channel.IsSpecified) + apiArgs.ChannelId = args.Channel.Value?.Id; + else if (args.ChannelId.IsSpecified) + apiArgs.ChannelId = args.ChannelId.Value; + + if (args.Roles.IsSpecified) + apiArgs.RoleIds = args.Roles.Value.Select(x => x.Id).ToArray(); + else if (args.RoleIds.IsSpecified) + apiArgs.RoleIds = args.RoleIds.Value.ToArray(); + + /* + * Ensure that the nick passed in the params of the request is not null. + * string.Empty ("") is the only way to reset the user nick in the API, + * a value of null does not. This is a workaround. + */ + if (apiArgs.Nickname.IsSpecified && apiArgs.Nickname.Value == null) + apiArgs.Nickname = new Optional(string.Empty); + + await client.ApiClient.ModifyGuildMemberAsync(user.GuildId, user.Id, apiArgs, options).ConfigureAwait(false); + return args; + } + + public static async Task KickAsync(IGuildUser user, BaseDiscordClient client, + string reason, RequestOptions options) + { + await client.ApiClient.RemoveGuildMemberAsync(user.GuildId, user.Id, reason, 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)); + } + + public static async Task AddRolesAsync(IGuildUser user, BaseDiscordClient client, IEnumerable roles, RequestOptions options) + { + foreach (var role in roles) + await client.ApiClient.AddRoleAsync(user.Guild.Id, user.Id, role.Id, options).ConfigureAwait(false); + } + + public static async Task RemoveRolesAsync(IGuildUser user, BaseDiscordClient client, IEnumerable roles, RequestOptions options) + { + foreach (var role in roles) + await client.ApiClient.RemoveRoleAsync(user.Guild.Id, user.Id, role.Id, options).ConfigureAwait(false); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Webhooks/RestWebhook.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Webhooks/RestWebhook.cs new file mode 100644 index 0000000..1fdc95a --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Webhooks/RestWebhook.cs @@ -0,0 +1,104 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.Webhook; + +namespace Discord.Rest +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestWebhook : RestEntity, IWebhook, IUpdateable + { + internal IGuild Guild { get; private set; } + internal ITextChannel Channel { get; private set; } + + /// + public ulong ChannelId { get; } + /// + public string Token { get; } + + /// + public string Name { get; private set; } + /// + public string AvatarId { get; private set; } + /// + public ulong? GuildId { get; private set; } + /// + public IUser Creator { get; private set; } + + /// + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + + internal RestWebhook(BaseDiscordClient discord, IGuild guild, ulong id, string token, ulong channelId) + : base(discord, id) + { + Guild = guild; + Token = token; + ChannelId = channelId; + } + internal RestWebhook(BaseDiscordClient discord, ITextChannel channel, ulong id, string token, ulong channelId) + : this(discord, channel.Guild, id, token, channelId) + { + Channel = channel; + } + + internal static RestWebhook Create(BaseDiscordClient discord, IGuild guild, Model model) + { + var entity = new RestWebhook(discord, guild, model.Id, model.Token, model.ChannelId); + entity.Update(model); + return entity; + } + internal static RestWebhook Create(BaseDiscordClient discord, ITextChannel channel, Model model) + { + var entity = new RestWebhook(discord, channel, model.Id, model.Token, model.ChannelId); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + if (model.Avatar.IsSpecified) + AvatarId = model.Avatar.Value; + if (model.Creator.IsSpecified) + Creator = RestUser.Create(Discord, model.Creator.Value); + if (model.GuildId.IsSpecified) + GuildId = model.GuildId.Value; + if (model.Name.IsSpecified) + Name = model.Name.Value; + } + + /// + public async Task UpdateAsync(RequestOptions options = null) + { + var model = await Discord.ApiClient.GetWebhookAsync(Id, options).ConfigureAwait(false); + Update(model); + } + + /// + public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + + public async Task ModifyAsync(Action func, RequestOptions options = null) + { + var model = await WebhookHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); + Update(model); + } + + /// + public Task DeleteAsync(RequestOptions options = null) + => WebhookHelper.DeleteAsync(this, Discord, options); + + public override string ToString() => $"Webhook: {Name}:{Id}"; + private string DebuggerDisplay => $"Webhook: {Name} ({Id})"; + + //IWebhook + /// + IGuild IWebhook.Guild + => Guild ?? throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); + /// + ITextChannel IWebhook.Channel + => Channel ?? throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); + /// + Task IWebhook.ModifyAsync(Action func, RequestOptions options) + => ModifyAsync(func, options); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Webhooks/WebhookHelper.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Webhooks/WebhookHelper.cs new file mode 100644 index 0000000..50e9cab --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Entities/Webhooks/WebhookHelper.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading.Tasks; +using Discord.API.Rest; +using ImageModel = Discord.API.Image; +using Model = Discord.API.Webhook; + +namespace Discord.Rest +{ + internal static class WebhookHelper + { + public static async Task ModifyAsync(IWebhook webhook, BaseDiscordClient client, + Action func, RequestOptions options) + { + var args = new WebhookProperties(); + func(args); + var apiArgs = new ModifyWebhookParams + { + Avatar = args.Image.IsSpecified ? args.Image.Value?.ToModel() : Optional.Create(), + Name = args.Name + }; + + if (!apiArgs.Avatar.IsSpecified && webhook.AvatarId != null) + apiArgs.Avatar = new ImageModel(webhook.AvatarId); + + if (args.Channel.IsSpecified) + apiArgs.ChannelId = args.Channel.Value.Id; + else if (args.ChannelId.IsSpecified) + apiArgs.ChannelId = args.ChannelId.Value; + + return await client.ApiClient.ModifyWebhookAsync(webhook.Id, apiArgs, options).ConfigureAwait(false); + } + public static async Task DeleteAsync(IWebhook webhook, BaseDiscordClient client, RequestOptions options) + { + await client.ApiClient.DeleteWebhookAsync(webhook.Id, options).ConfigureAwait(false); + } + + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Extensions/ClientExtensions.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Extensions/ClientExtensions.cs new file mode 100644 index 0000000..647c7d4 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Extensions/ClientExtensions.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Rest +{ + public static class ClientExtensions + { + /// + /// Adds a user to the specified guild. + /// + /// + /// This method requires you have an OAuth2 access token for the user, requested with the guilds.join scope, and that the bot have the MANAGE_INVITES permission in the guild. + /// + /// The Discord client object. + /// The snowflake identifier of the guild. + /// The snowflake identifier of the user. + /// The OAuth2 access token for the user, requested with the guilds.join scope. + /// The delegate containing the properties to be applied to the user upon being added to the guild. + /// The options to be used when sending the request. + public static Task AddGuildUserAsync(this BaseDiscordClient client, ulong guildId, ulong userId, string accessToken, Action func = null, RequestOptions options = null) + => GuildHelper.AddGuildUserAsync(guildId, client, userId, accessToken, func, options); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Extensions/EntityExtensions.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Extensions/EntityExtensions.cs new file mode 100644 index 0000000..e265f99 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Extensions/EntityExtensions.cs @@ -0,0 +1,136 @@ +using System.Collections.Immutable; +using System.Linq; + +namespace Discord.Rest +{ + internal static class EntityExtensions + { + public static IEmote ToIEmote(this API.Emoji model) + { + if (model.Id.HasValue) + return model.ToEntity(); + return new Emoji(model.Name); + } + + public static GuildEmote ToEntity(this API.Emoji model) + => new GuildEmote(model.Id.Value, + model.Name, + model.Animated.GetValueOrDefault(), + model.Managed, + model.RequireColons, + ImmutableArray.Create(model.Roles), + model.User.IsSpecified ? model.User.Value.Id : (ulong?)null); + + public static Embed ToEntity(this API.Embed model) + { + return new Embed(model.Type, model.Title, model.Description, model.Url, model.Timestamp, + model.Color.HasValue ? new Color(model.Color.Value) : (Color?)null, + model.Image.IsSpecified ? model.Image.Value.ToEntity() : (EmbedImage?)null, + model.Video.IsSpecified ? model.Video.Value.ToEntity() : (EmbedVideo?)null, + model.Author.IsSpecified ? model.Author.Value.ToEntity() : (EmbedAuthor?)null, + model.Footer.IsSpecified ? model.Footer.Value.ToEntity() : (EmbedFooter?)null, + model.Provider.IsSpecified ? model.Provider.Value.ToEntity() : (EmbedProvider?)null, + model.Thumbnail.IsSpecified ? model.Thumbnail.Value.ToEntity() : (EmbedThumbnail?)null, + model.Fields.IsSpecified ? model.Fields.Value.Select(x => x.ToEntity()).ToImmutableArray() : ImmutableArray.Create()); + } + public static API.Embed ToModel(this Embed entity) + { + if (entity == null) return null; + var model = new API.Embed + { + Type = entity.Type, + Title = entity.Title, + Description = entity.Description, + Url = entity.Url, + Timestamp = entity.Timestamp, + Color = entity.Color?.RawValue + }; + if (entity.Author != null) + model.Author = entity.Author.Value.ToModel(); + model.Fields = entity.Fields.Select(x => x.ToModel()).ToArray(); + if (entity.Footer != null) + model.Footer = entity.Footer.Value.ToModel(); + if (entity.Image != null) + model.Image = entity.Image.Value.ToModel(); + if (entity.Provider != null) + model.Provider = entity.Provider.Value.ToModel(); + if (entity.Thumbnail != null) + model.Thumbnail = entity.Thumbnail.Value.ToModel(); + if (entity.Video != null) + model.Video = entity.Video.Value.ToModel(); + return model; + } + public static EmbedAuthor ToEntity(this API.EmbedAuthor model) + { + return new EmbedAuthor(model.Name, model.Url, model.IconUrl, model.ProxyIconUrl); + } + public static API.EmbedAuthor ToModel(this EmbedAuthor entity) + { + return new API.EmbedAuthor { Name = entity.Name, Url = entity.Url, IconUrl = entity.IconUrl }; + } + public static EmbedField ToEntity(this API.EmbedField model) + { + return new EmbedField(model.Name, model.Value, model.Inline); + } + public static API.EmbedField ToModel(this EmbedField entity) + { + return new API.EmbedField { Name = entity.Name, Value = entity.Value, Inline = entity.Inline }; + } + public static EmbedFooter ToEntity(this API.EmbedFooter model) + { + return new EmbedFooter(model.Text, model.IconUrl, model.ProxyIconUrl); + } + public static API.EmbedFooter ToModel(this EmbedFooter entity) + { + return new API.EmbedFooter { Text = entity.Text, IconUrl = entity.IconUrl }; + } + public static EmbedImage ToEntity(this API.EmbedImage model) + { + return new EmbedImage(model.Url, model.ProxyUrl, + model.Height.IsSpecified ? model.Height.Value : (int?)null, + model.Width.IsSpecified ? model.Width.Value : (int?)null); + } + public static API.EmbedImage ToModel(this EmbedImage entity) + { + return new API.EmbedImage { Url = entity.Url }; + } + public static EmbedProvider ToEntity(this API.EmbedProvider model) + { + return new EmbedProvider(model.Name, model.Url); + } + public static API.EmbedProvider ToModel(this EmbedProvider entity) + { + return new API.EmbedProvider { Name = entity.Name, Url = entity.Url }; + } + public static EmbedThumbnail ToEntity(this API.EmbedThumbnail model) + { + return new EmbedThumbnail(model.Url, model.ProxyUrl, + model.Height.IsSpecified ? model.Height.Value : (int?)null, + model.Width.IsSpecified ? model.Width.Value : (int?)null); + } + public static API.EmbedThumbnail ToModel(this EmbedThumbnail entity) + { + return new API.EmbedThumbnail { Url = entity.Url }; + } + public static EmbedVideo ToEntity(this API.EmbedVideo model) + { + return new EmbedVideo(model.Url, + model.Height.IsSpecified ? model.Height.Value : (int?)null, + model.Width.IsSpecified ? model.Width.Value : (int?)null); + } + public static API.EmbedVideo ToModel(this EmbedVideo entity) + { + return new API.EmbedVideo { Url = entity.Url }; + } + + public static API.Image ToModel(this Image entity) + { + return new API.Image(entity.Stream); + } + + public static Overwrite ToEntity(this API.Overwrite model) + { + return new Overwrite(model.TargetId, model.TargetType, new OverwritePermissions(model.Allow, model.Deny)); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/ArrayConverter.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/ArrayConverter.cs new file mode 100644 index 0000000..3cededb --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/ArrayConverter.cs @@ -0,0 +1,59 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; + +namespace Discord.Net.Converters +{ + internal class ArrayConverter : JsonConverter + { + private readonly JsonConverter _innerConverter; + + public override bool CanConvert(Type objectType) => true; + public override bool CanRead => true; + public override bool CanWrite => true; + + public ArrayConverter(JsonConverter innerConverter) + { + _innerConverter = innerConverter; + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var result = new List(); + if (reader.TokenType == JsonToken.StartArray) + { + reader.Read(); + while (reader.TokenType != JsonToken.EndArray) + { + T obj; + if (_innerConverter != null) + obj = (T)_innerConverter.ReadJson(reader, typeof(T), null, serializer); + else + obj = serializer.Deserialize(reader); + result.Add(obj); + reader.Read(); + } + } + return result.ToArray(); + } + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value != null) + { + writer.WriteStartArray(); + var a = (T[])value; + for (int i = 0; i < a.Length; i++) + { + if (_innerConverter != null) + _innerConverter.WriteJson(writer, a[i], serializer); + else + serializer.Serialize(writer, a[i], typeof(T)); + } + + writer.WriteEndArray(); + } + else + writer.WriteNull(); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs new file mode 100644 index 0000000..a1ed20c --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs @@ -0,0 +1,109 @@ +using Discord.API; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Discord.Net.Converters +{ + internal class DiscordContractResolver : DefaultContractResolver + { + private static readonly TypeInfo _ienumerable = typeof(IEnumerable).GetTypeInfo(); + private static readonly MethodInfo _shouldSerialize = typeof(DiscordContractResolver).GetTypeInfo().GetDeclaredMethod("ShouldSerialize"); + + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) + { + var property = base.CreateProperty(member, memberSerialization); + if (property.Ignored) + return property; + + if (member is PropertyInfo propInfo) + { + var converter = GetConverter(property, propInfo, propInfo.PropertyType, 0); + if (converter != null) + { + property.Converter = converter; + } + } + else + throw new InvalidOperationException($"{member.DeclaringType.FullName}.{member.Name} is not a property."); + return property; + } + + private static JsonConverter GetConverter(JsonProperty property, PropertyInfo propInfo, Type type, int depth) + { + if (type.IsArray) + return MakeGenericConverter(property, propInfo, typeof(ArrayConverter<>), type.GetElementType(), depth); + if (type.IsConstructedGenericType) + { + Type genericType = type.GetGenericTypeDefinition(); + if (depth == 0 && genericType == typeof(Optional<>)) + { + var typeInput = propInfo.DeclaringType; + var innerTypeOutput = type.GenericTypeArguments[0]; + + var getter = typeof(Func<,>).MakeGenericType(typeInput, type); + var getterDelegate = propInfo.GetMethod.CreateDelegate(getter); + var shouldSerialize = _shouldSerialize.MakeGenericMethod(typeInput, innerTypeOutput); + var shouldSerializeDelegate = (Func)shouldSerialize.CreateDelegate(typeof(Func)); + property.ShouldSerialize = x => shouldSerializeDelegate(x, getterDelegate); + + return MakeGenericConverter(property, propInfo, typeof(OptionalConverter<>), innerTypeOutput, depth); + } + else if (genericType == typeof(Nullable<>)) + return MakeGenericConverter(property, propInfo, typeof(NullableConverter<>), type.GenericTypeArguments[0], depth); + else if (genericType == typeof(EntityOrId<>)) + return MakeGenericConverter(property, propInfo, typeof(UInt64EntityOrIdConverter<>), type.GenericTypeArguments[0], depth); + } + + //Primitives + bool hasInt53 = propInfo.GetCustomAttribute() != null; + if (!hasInt53) + { + if (type == typeof(ulong)) + return UInt64Converter.Instance; + } + bool hasUnixStamp = propInfo.GetCustomAttribute() != null; + if (hasUnixStamp) + { + if (type == typeof(DateTimeOffset)) + return UnixTimestampConverter.Instance; + } + + //Enums + if (type == typeof(PermissionTarget)) + return PermissionTargetConverter.Instance; + if (type == typeof(UserStatus)) + return UserStatusConverter.Instance; + if (type == typeof(EmbedType)) + return EmbedTypeConverter.Instance; + + //Special + if (type == typeof(API.Image)) + return ImageConverter.Instance; + + //Entities + var typeInfo = type.GetTypeInfo(); + if (typeInfo.ImplementedInterfaces.Any(x => x == typeof(IEntity))) + return UInt64EntityConverter.Instance; + if (typeInfo.ImplementedInterfaces.Any(x => x == typeof(IEntity))) + return StringEntityConverter.Instance; + + return null; + } + + private static bool ShouldSerialize(object owner, Delegate getter) + { + return (getter as Func>)((TOwner)owner).IsSpecified; + } + + private static JsonConverter MakeGenericConverter(JsonProperty property, PropertyInfo propInfo, Type converterType, Type innerType, int depth) + { + var genericType = converterType.MakeGenericType(innerType).GetTypeInfo(); + var innerConverter = GetConverter(property, propInfo, innerType, depth + 1); + return genericType.DeclaredConstructors.First().Invoke(new object[] { innerConverter }) as JsonConverter; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/EmbedTypeConverter.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/EmbedTypeConverter.cs new file mode 100644 index 0000000..1e03fb6 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/EmbedTypeConverter.cs @@ -0,0 +1,73 @@ +using System; +using Newtonsoft.Json; + +namespace Discord.Net.Converters +{ + internal class EmbedTypeConverter : JsonConverter + { + public static readonly EmbedTypeConverter Instance = new EmbedTypeConverter(); + + public override bool CanConvert(Type objectType) => true; + public override bool CanRead => true; + public override bool CanWrite => true; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + switch ((string)reader.Value) + { + case "rich": + return EmbedType.Rich; + case "link": + return EmbedType.Link; + case "video": + return EmbedType.Video; + case "image": + return EmbedType.Image; + case "gifv": + return EmbedType.Gifv; + case "article": + return EmbedType.Article; + case "tweet": + return EmbedType.Tweet; + case "html": + return EmbedType.Html; + case "application_news": // TODO 2.2 EmbedType.News + default: + return EmbedType.Unknown; + } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + switch ((EmbedType)value) + { + case EmbedType.Rich: + writer.WriteValue("rich"); + break; + case EmbedType.Link: + writer.WriteValue("link"); + break; + case EmbedType.Video: + writer.WriteValue("video"); + break; + case EmbedType.Image: + writer.WriteValue("image"); + break; + case EmbedType.Gifv: + writer.WriteValue("gifv"); + break; + case EmbedType.Article: + writer.WriteValue("article"); + break; + case EmbedType.Tweet: + writer.WriteValue("tweet"); + break; + case EmbedType.Html: + writer.WriteValue("html"); + break; + default: + throw new JsonSerializationException("Invalid embed type"); + } + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/ImageConverter.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/ImageConverter.cs new file mode 100644 index 0000000..941a35b --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/ImageConverter.cs @@ -0,0 +1,54 @@ +using System; +using System.IO; +using Newtonsoft.Json; +using Model = Discord.API.Image; + +namespace Discord.Net.Converters +{ + internal class ImageConverter : JsonConverter + { + public static readonly ImageConverter Instance = new ImageConverter(); + + public override bool CanConvert(Type objectType) => true; + public override bool CanRead => true; + public override bool CanWrite => true; + + /// Cannot read from image. + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new InvalidOperationException(); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var image = (Model)value; + + if (image.Stream != null) + { + byte[] bytes; + int length; + if (image.Stream.CanSeek) + { + bytes = new byte[image.Stream.Length - image.Stream.Position]; + length = image.Stream.Read(bytes, 0, bytes.Length); + } + else + { + using (var cloneStream = new MemoryStream()) + { + image.Stream.CopyTo(cloneStream); + bytes = new byte[cloneStream.Length]; + cloneStream.Position = 0; + cloneStream.Read(bytes, 0, bytes.Length); + length = (int)cloneStream.Length; + } + } + + string base64 = Convert.ToBase64String(bytes, 0, length); + writer.WriteValue($"data:image/jpeg;base64,{base64}"); + } + else if (image.Hash != null) + writer.WriteValue(image.Hash); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/NullableConverter.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/NullableConverter.cs new file mode 100644 index 0000000..0b149e7 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/NullableConverter.cs @@ -0,0 +1,50 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.Net.Converters +{ + internal class NullableConverter : JsonConverter + where T : struct + { + private readonly JsonConverter _innerConverter; + + public override bool CanConvert(Type objectType) => true; + public override bool CanRead => true; + public override bool CanWrite => true; + + public NullableConverter(JsonConverter innerConverter) + { + _innerConverter = innerConverter; + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + object value = reader.Value; + if (value == null) + return null; + else + { + T obj; + if (_innerConverter != null) + obj = (T)_innerConverter.ReadJson(reader, typeof(T), null, serializer); + else + obj = serializer.Deserialize(reader); + return obj; + } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value == null) + writer.WriteNull(); + else + { + var nullable = (T?)value; + if (_innerConverter != null) + _innerConverter.WriteJson(writer, nullable.Value, serializer); + else + serializer.Serialize(writer, nullable.Value, typeof(T)); + } + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/OptionalConverter.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/OptionalConverter.cs new file mode 100644 index 0000000..d3d6191 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/OptionalConverter.cs @@ -0,0 +1,46 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.Net.Converters +{ + internal class OptionalConverter : JsonConverter + { + private readonly JsonConverter _innerConverter; + + public override bool CanConvert(Type objectType) => true; + public override bool CanRead => true; + public override bool CanWrite => true; + + public OptionalConverter(JsonConverter innerConverter) + { + _innerConverter = innerConverter; + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + T obj; + // custom converters need to be able to safely fail; move this check in here to prevent wasteful casting when parsing primitives + if (_innerConverter != null) + { + object o = _innerConverter.ReadJson(reader, typeof(T), null, serializer); + if (o is Optional) + return o; + + obj = (T)o; + } + else + obj = serializer.Deserialize(reader); + + return new Optional(obj); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + value = ((Optional)value).Value; + if (_innerConverter != null) + _innerConverter.WriteJson(writer, value, serializer); + else + serializer.Serialize(writer, value, typeof(T)); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/PermissionTargetConverter.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/PermissionTargetConverter.cs new file mode 100644 index 0000000..de2e379 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/PermissionTargetConverter.cs @@ -0,0 +1,44 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.Net.Converters +{ + internal class PermissionTargetConverter : JsonConverter + { + public static readonly PermissionTargetConverter Instance = new PermissionTargetConverter(); + + public override bool CanConvert(Type objectType) => true; + public override bool CanRead => true; + public override bool CanWrite => true; + + /// Unknown permission target. + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + switch ((string)reader.Value) + { + case "member": + return PermissionTarget.User; + case "role": + return PermissionTarget.Role; + default: + throw new JsonSerializationException("Unknown permission target."); + } + } + + /// Invalid permission target. + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + switch ((PermissionTarget)value) + { + case PermissionTarget.User: + writer.WriteValue("member"); + break; + case PermissionTarget.Role: + writer.WriteValue("role"); + break; + default: + throw new JsonSerializationException("Invalid permission target."); + } + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/StringEntityConverter.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/StringEntityConverter.cs new file mode 100644 index 0000000..d7dd58d --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/StringEntityConverter.cs @@ -0,0 +1,27 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.Net.Converters +{ + internal class StringEntityConverter : JsonConverter + { + public static readonly StringEntityConverter Instance = new StringEntityConverter(); + + public override bool CanConvert(Type objectType) => true; + public override bool CanRead => false; + public override bool CanWrite => true; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new InvalidOperationException(); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value != null) + writer.WriteValue((value as IEntity).Id); + else + writer.WriteNull(); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/UInt64Converter.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/UInt64Converter.cs new file mode 100644 index 0000000..27cbe92 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/UInt64Converter.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; +using System; +using System.Globalization; + +namespace Discord.Net.Converters +{ + internal class UInt64Converter : JsonConverter + { + public static readonly UInt64Converter Instance = new UInt64Converter(); + + public override bool CanConvert(Type objectType) => true; + public override bool CanRead => true; + public override bool CanWrite => true; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + return ulong.Parse((string)reader.Value, NumberStyles.None, CultureInfo.InvariantCulture); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue(((ulong)value).ToString(CultureInfo.InvariantCulture)); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/UInt64EntityConverter.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/UInt64EntityConverter.cs new file mode 100644 index 0000000..b8d8f10 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/UInt64EntityConverter.cs @@ -0,0 +1,28 @@ +using Newtonsoft.Json; +using System; +using System.Globalization; + +namespace Discord.Net.Converters +{ + internal class UInt64EntityConverter : JsonConverter + { + public static readonly UInt64EntityConverter Instance = new UInt64EntityConverter(); + + public override bool CanConvert(Type objectType) => true; + public override bool CanRead => false; + public override bool CanWrite => true; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new InvalidOperationException(); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value != null) + writer.WriteValue((value as IEntity).Id.ToString(CultureInfo.InvariantCulture)); + else + writer.WriteNull(); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/UInt64EntityOrIdConverter.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/UInt64EntityOrIdConverter.cs new file mode 100644 index 0000000..e555348 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/UInt64EntityOrIdConverter.cs @@ -0,0 +1,43 @@ +using Discord.API; +using Newtonsoft.Json; +using System; +using System.Globalization; + +namespace Discord.Net.Converters +{ + internal class UInt64EntityOrIdConverter : JsonConverter + { + private readonly JsonConverter _innerConverter; + + public override bool CanConvert(Type objectType) => true; + public override bool CanRead => true; + public override bool CanWrite => false; + + public UInt64EntityOrIdConverter(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 EntityOrId(ulong.Parse(reader.ReadAsString(), NumberStyles.None, CultureInfo.InvariantCulture)); + default: + T obj; + if (_innerConverter != null) + obj = (T)_innerConverter.ReadJson(reader, typeof(T), null, serializer); + else + obj = serializer.Deserialize(reader); + return new EntityOrId(obj); + } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new InvalidOperationException(); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs new file mode 100644 index 0000000..0b50cb1 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs @@ -0,0 +1,33 @@ +using System; +using Newtonsoft.Json; + +namespace Discord.Net.Converters +{ + public class UnixTimestampConverter : JsonConverter + { + public static readonly UnixTimestampConverter Instance = new UnixTimestampConverter(); + + public override bool CanConvert(Type objectType) => true; + public override bool CanRead => true; + public override bool CanWrite => true; + + // 1e13 unix ms = year 2286 + // necessary to prevent discord.js from sending values in the e15 and overflowing a DTO + private const long MaxSaneMs = 1_000_000_000_000_0; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + // Discord doesn't validate if timestamps contain decimals or not, and they also don't validate if timestamps are reasonably sized + if (reader.Value is double d && d < MaxSaneMs) + return new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, TimeSpan.Zero).AddMilliseconds(d); + else if (reader.Value is long l && l < MaxSaneMs) + return new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, TimeSpan.Zero).AddMilliseconds(l); + return Optional.Unspecified; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/UserStatusConverter.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/UserStatusConverter.cs new file mode 100644 index 0000000..c0a287c --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Converters/UserStatusConverter.cs @@ -0,0 +1,58 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.Net.Converters +{ + internal class UserStatusConverter : JsonConverter + { + public static readonly UserStatusConverter Instance = new UserStatusConverter(); + + public override bool CanConvert(Type objectType) => true; + public override bool CanRead => true; + public override bool CanWrite => true; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + switch ((string)reader.Value) + { + case "online": + return UserStatus.Online; + case "idle": + return UserStatus.Idle; + case "dnd": + return UserStatus.DoNotDisturb; + case "invisible": + return UserStatus.Invisible; //Should never happen + case "offline": + return UserStatus.Offline; + default: + throw new JsonSerializationException("Unknown user status"); + } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + switch ((UserStatus)value) + { + case UserStatus.Online: + writer.WriteValue("online"); + break; + case UserStatus.Idle: + case UserStatus.AFK: + writer.WriteValue("idle"); + break; + case UserStatus.DoNotDisturb: + writer.WriteValue("dnd"); + break; + case UserStatus.Invisible: + writer.WriteValue("invisible"); + break; + case UserStatus.Offline: + writer.WriteValue("offline"); + break; + default: + throw new JsonSerializationException("Invalid user status"); + } + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/DefaultRestClient.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/DefaultRestClient.cs new file mode 100644 index 0000000..b5036d9 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/DefaultRestClient.cs @@ -0,0 +1,162 @@ +using Discord.Net.Converters; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.Rest +{ + internal sealed class DefaultRestClient : IRestClient, IDisposable + { + private const int HR_SECURECHANNELFAILED = -2146233079; + + private readonly HttpClient _client; + private readonly string _baseUrl; + private readonly JsonSerializer _errorDeserializer; + private CancellationToken _cancelToken; + private bool _isDisposed; + + public DefaultRestClient(string baseUrl, bool useProxy = false) + { + _baseUrl = baseUrl; + +#pragma warning disable IDISP014 + _client = new HttpClient(new HttpClientHandler + { + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, + UseCookies = false, + UseProxy = useProxy, + }); +#pragma warning restore IDISP014 + SetHeader("accept-encoding", "gzip, deflate"); + + _cancelToken = CancellationToken.None; + _errorDeserializer = new JsonSerializer(); + } + private void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + _client.Dispose(); + _isDisposed = true; + } + } + public void Dispose() + { + Dispose(true); + } + + public void SetHeader(string key, string value) + { + _client.DefaultRequestHeaders.Remove(key); + if (value != null) + _client.DefaultRequestHeaders.Add(key, value); + } + public void SetCancelToken(CancellationToken cancelToken) + { + _cancelToken = cancelToken; + } + + public async Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly, string reason = null) + { + string uri = Path.Combine(_baseUrl, endpoint); + using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) + { + if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); + return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); + } + } + public async Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly, string reason = null) + { + string uri = Path.Combine(_baseUrl, endpoint); + using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) + { + if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); + restRequest.Content = new StringContent(json, Encoding.UTF8, "application/json"); + return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); + } + } + + /// Unsupported param type. + public async Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly, string reason = null) + { + string uri = Path.Combine(_baseUrl, endpoint); + using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) + { + if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); + var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture)); + MemoryStream memoryStream = null; + if (multipartParams != null) + { + foreach (var p in multipartParams) + { + switch (p.Value) + { +#pragma warning disable IDISP004 + case string stringValue: { content.Add(new StringContent(stringValue), p.Key); continue; } + case byte[] byteArrayValue: { content.Add(new ByteArrayContent(byteArrayValue), p.Key); continue; } + case Stream streamValue: { content.Add(new StreamContent(streamValue), p.Key); continue; } + case MultipartFile fileValue: + { + var stream = fileValue.Stream; + if (!stream.CanSeek) + { + memoryStream = new MemoryStream(); + await stream.CopyToAsync(memoryStream).ConfigureAwait(false); + memoryStream.Position = 0; +#pragma warning disable IDISP001 + stream = memoryStream; +#pragma warning restore IDISP001 + } + content.Add(new StreamContent(stream), p.Key, fileValue.Filename); +#pragma warning restore IDISP004 + continue; + } + default: throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\"."); + } + } + } + restRequest.Content = content; + var result = await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); + memoryStream?.Dispose(); + return result; + } + } + + private async Task SendInternalAsync(HttpRequestMessage request, CancellationToken cancelToken, bool headerOnly) + { + using (var cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken, cancelToken)) + { + cancelToken = cancelTokenSource.Token; + HttpResponseMessage response = await _client.SendAsync(request, cancelToken).ConfigureAwait(false); + + var headers = response.Headers.ToDictionary(x => x.Key, x => x.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase); + var stream = !headerOnly ? await response.Content.ReadAsStreamAsync().ConfigureAwait(false) : null; + + return new RestResponse(response.StatusCode, headers, stream); + } + } + + private static readonly HttpMethod Patch = new HttpMethod("PATCH"); + private HttpMethod GetMethod(string method) + { + switch (method) + { + case "DELETE": return HttpMethod.Delete; + case "GET": return HttpMethod.Get; + case "PATCH": return Patch; + case "POST": return HttpMethod.Post; + case "PUT": return HttpMethod.Put; + default: throw new ArgumentOutOfRangeException(nameof(method), $"Unknown HttpMethod: {method}"); + } + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/DefaultRestClientProvider.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/DefaultRestClientProvider.cs new file mode 100644 index 0000000..67b4709 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/DefaultRestClientProvider.cs @@ -0,0 +1,25 @@ +using System; + +namespace Discord.Net.Rest +{ + public static class DefaultRestClientProvider + { + public static readonly RestClientProvider Instance = Create(); + + /// The default RestClientProvider is not supported on this platform. + public static RestClientProvider Create(bool useProxy = false) + { + return url => + { + try + { + return new DefaultRestClient(url, useProxy); + } + catch (PlatformNotSupportedException ex) + { + throw new PlatformNotSupportedException("The default RestClientProvider is not supported on this platform.", ex); + } + }; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Queue/ClientBucket.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Queue/ClientBucket.cs new file mode 100644 index 0000000..cd9d8aa --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Queue/ClientBucket.cs @@ -0,0 +1,50 @@ +using System.Collections.Immutable; + +namespace Discord.Net.Queue +{ + public enum ClientBucketType + { + Unbucketed = 0, + SendEdit = 1 + } + internal struct ClientBucket + { + private static readonly ImmutableDictionary DefsByType; + private static readonly ImmutableDictionary DefsById; + + static ClientBucket() + { + var buckets = new[] + { + new ClientBucket(ClientBucketType.Unbucketed, "", 10, 10), + new ClientBucket(ClientBucketType.SendEdit, "", 10, 10) + }; + + var builder = ImmutableDictionary.CreateBuilder(); + foreach (var bucket in buckets) + builder.Add(bucket.Type, bucket); + DefsByType = builder.ToImmutable(); + + var builder2 = ImmutableDictionary.CreateBuilder(); + foreach (var bucket in buckets) + builder2.Add(bucket.Id, bucket); + DefsById = builder2.ToImmutable(); + } + + public static ClientBucket Get(ClientBucketType type) => DefsByType[type]; + public static ClientBucket Get(string id) => DefsById[id]; + + public ClientBucketType Type { get; } + public string Id { get; } + public int WindowCount { get; } + public int WindowSeconds { get; } + + public ClientBucket(ClientBucketType type, string id, int count, int seconds) + { + Type = type; + Id = id; + WindowCount = count; + WindowSeconds = seconds; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Queue/RequestQueue.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Queue/RequestQueue.cs new file mode 100644 index 0000000..4baf764 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Queue/RequestQueue.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Concurrent; +#if DEBUG_LIMITS +using System.Diagnostics; +#endif +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.Queue +{ + internal class RequestQueue : IDisposable + { + public event Func RateLimitTriggered; + + private readonly ConcurrentDictionary _buckets; + private readonly SemaphoreSlim _tokenLock; + private readonly CancellationTokenSource _cancelTokenSource; //Dispose token + private CancellationTokenSource _clearToken; + private CancellationToken _parentToken; + private CancellationTokenSource _requestCancelTokenSource; + private CancellationToken _requestCancelToken; //Parent token + Clear token + private DateTimeOffset _waitUntil; + + private Task _cleanupTask; + + public RequestQueue() + { + _tokenLock = new SemaphoreSlim(1, 1); + + _clearToken = new CancellationTokenSource(); + _cancelTokenSource = 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; + _requestCancelTokenSource?.Dispose(); + _requestCancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _clearToken.Token); + _requestCancelToken = _requestCancelTokenSource.Token; + } + finally { _tokenLock.Release(); } + } + public async Task ClearAsync() + { + await _tokenLock.WaitAsync().ConfigureAwait(false); + try + { + _clearToken?.Cancel(); + _clearToken?.Dispose(); + _clearToken = new CancellationTokenSource(); + if (_parentToken != null) + { + _requestCancelTokenSource?.Dispose(); + _requestCancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_clearToken.Token, _parentToken); + _requestCancelToken = _requestCancelTokenSource.Token; + } + else + _requestCancelToken = _clearToken.Token; + } + finally { _tokenLock.Release(); } + } + + public async Task SendAsync(RestRequest request) + { + CancellationTokenSource createdTokenSource = null; + if (request.Options.CancelToken.CanBeCanceled) + { + createdTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_requestCancelToken, request.Options.CancelToken); + request.Options.CancelToken = createdTokenSource.Token; + } + else + request.Options.CancelToken = _requestCancelToken; + + var bucket = GetOrCreateBucket(request.Options.BucketId, request); + var result = await bucket.SendAsync(request).ConfigureAwait(false); + createdTokenSource?.Dispose(); + return result; + } + 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) + { +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Sleeping {millis} ms (Pre-emptive) [Global]"); +#endif + await Task.Delay(millis).ConfigureAwait(false); + } + } + internal void PauseGlobal(RateLimitInfo info) + { + _waitUntil = DateTimeOffset.UtcNow.AddMilliseconds(info.RetryAfter.Value + (info.Lag?.TotalMilliseconds ?? 0.0)); + } + + private RequestBucket GetOrCreateBucket(string id, RestRequest request) + { + return _buckets.GetOrAdd(id, x => new RequestBucket(this, request, x)); + } + internal async Task RaiseRateLimitTriggered(string bucketId, RateLimitInfo? info) + { + await RateLimitTriggered(bucketId, info).ConfigureAwait(false); + } + + private async Task RunCleanup() + { + try + { + while (!_cancelTokenSource.IsCancellationRequested) + { + var now = DateTimeOffset.UtcNow; + foreach (var bucket in _buckets.Select(x => x.Value)) + { + if ((now - bucket.LastAttemptAt).TotalMinutes > 1.0) + _buckets.TryRemove(bucket.Id, out _); + } + await Task.Delay(60000, _cancelTokenSource.Token).ConfigureAwait(false); //Runs each minute + } + } + catch (OperationCanceledException) { } + catch (ObjectDisposedException) { } + } + + public void Dispose() + { + _cancelTokenSource?.Dispose(); + _tokenLock?.Dispose(); + _clearToken?.Dispose(); + _requestCancelTokenSource?.Dispose(); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs new file mode 100644 index 0000000..72dd164 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs @@ -0,0 +1,329 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +#if DEBUG_LIMITS +using System.Diagnostics; +#endif +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, RestRequest request, string id) + { + _queue = queue; + Id = id; + + _lock = new object(); + + if (request.Options.IsClientBucket) + WindowCount = ClientBucket.Get(request.Options.BucketId).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); +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Start"); +#endif + LastAttemptAt = DateTimeOffset.UtcNow; + while (true) + { + await _queue.EnterGlobalAsync(id, request).ConfigureAwait(false); + await EnterAsync(id, request).ConfigureAwait(false); + +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Sending..."); +#endif + RateLimitInfo info = default(RateLimitInfo); + try + { + var response = await request.SendAsync().ConfigureAwait(false); + info = new RateLimitInfo(response.Headers); + + if (response.StatusCode < (HttpStatusCode)200 || response.StatusCode >= (HttpStatusCode)300) + { + switch (response.StatusCode) + { + case (HttpStatusCode)429: + if (info.IsGlobal) + { +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] (!) 429 [Global]"); +#endif + _queue.PauseGlobal(info); + } + else + { +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] (!) 429"); +#endif + UpdateRateLimit(id, request, info, true); + } + await _queue.RaiseRateLimitTriggered(Id, info).ConfigureAwait(false); + continue; //Retry + case HttpStatusCode.BadGateway: //502 +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] (!) 502"); +#endif + if ((request.Options.RetryMode & RetryMode.Retry502) == 0) + throw new HttpException(HttpStatusCode.BadGateway, request, null); + + continue; //Retry + default: + int? code = null; + 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); + try { code = json.Value("code"); } catch { }; + try { reason = json.Value("message"); } catch { }; + } + } + catch { } + } + throw new HttpException(response.StatusCode, request, code, reason); + } + } + else + { +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Success"); +#endif + return response.Stream; + } + } + //catch (HttpException) { throw; } //Pass through + catch (TimeoutException) + { +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Timeout"); +#endif + if ((request.Options.RetryMode & RetryMode.RetryTimeouts) == 0) + throw; + + await Task.Delay(500).ConfigureAwait(false); + continue; //Retry + } + /*catch (Exception) + { +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Error"); +#endif + if ((request.Options.RetryMode & RetryMode.RetryErrors) == 0) + throw; + + await Task.Delay(500); + continue; //Retry + }*/ + finally + { + UpdateRateLimit(id, request, info, false); +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Stop"); +#endif + } + } + } + + private async Task EnterAsync(int id, RestRequest request) + { + int windowCount; + DateTimeOffset? resetAt; + bool isRateLimited = false; + + while (true) + { + if (DateTimeOffset.UtcNow > request.TimeoutAt || request.Options.CancelToken.IsCancellationRequested) + { + if (!isRateLimited) + throw new TimeoutException(); + else + ThrowRetryLimit(request); + } + + lock (_lock) + { + windowCount = WindowCount; + resetAt = _resetTick; + } + + DateTimeOffset? timeoutAt = request.TimeoutAt; + if (windowCount > 0 && Interlocked.Decrement(ref _semaphore) < 0) + { + if (!isRateLimited) + { + isRateLimited = true; + await _queue.RaiseRateLimitTriggered(Id, null).ConfigureAwait(false); + } + + ThrowRetryLimit(request); + + if (resetAt.HasValue) + { + if (resetAt > timeoutAt) + ThrowRetryLimit(request); + int millis = (int)Math.Ceiling((resetAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds); +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Sleeping {millis} ms (Pre-emptive)"); +#endif + if (millis > 0) + await Task.Delay(millis, request.Options.CancelToken).ConfigureAwait(false); + } + else + { + if ((timeoutAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds < 500.0) + ThrowRetryLimit(request); +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Sleeping 500* ms (Pre-emptive)"); +#endif + await Task.Delay(500, request.Options.CancelToken).ConfigureAwait(false); + } + continue; + } +#if DEBUG_LIMITS + else + Debug.WriteLine($"[{id}] Entered Semaphore ({_semaphore}/{WindowCount} remaining)"); +#endif + break; + } + } + + private void UpdateRateLimit(int id, RestRequest request, RateLimitInfo info, bool is429) + { + if (WindowCount == 0) + return; + + lock (_lock) + { + bool hasQueuedReset = _resetTick != null; + if (info.Limit.HasValue && WindowCount != info.Limit.Value) + { + WindowCount = info.Limit.Value; + _semaphore = info.Remaining.Value; +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Upgraded Semaphore to {info.Remaining.Value}/{WindowCount}"); +#endif + } + + var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + DateTimeOffset? resetTick = null; + + //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); +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Retry-After: {info.RetryAfter.Value} ({info.RetryAfter.Value} ms)"); +#endif + } + else if (info.ResetAfter.HasValue && (request.Options.UseSystemClock.HasValue ? !request.Options.UseSystemClock.Value : false)) + { + resetTick = DateTimeOffset.Now.Add(info.ResetAfter.Value); + } + else if (info.Reset.HasValue) + { + resetTick = info.Reset.Value.AddSeconds(info.Lag?.TotalSeconds ?? 1.0); + + /* millisecond precision makes this unnecessary, retaining in case of regression + + if (request.Options.IsReactionBucket) + resetTick = DateTimeOffset.Now.AddMilliseconds(250); + */ + + int diff = (int)(resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds; +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] X-RateLimit-Reset: {info.Reset.Value.ToUnixTimeSeconds()} ({diff} ms, {info.Lag?.TotalMilliseconds} ms lag)"); +#endif + } + else if (request.Options.IsClientBucket && request.Options.BucketId != null) + { + resetTick = DateTimeOffset.UtcNow.AddSeconds(ClientBucket.Get(request.Options.BucketId).WindowSeconds); +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Client Bucket ({ClientBucket.Get(request.Options.BucketId).WindowSeconds * 1000} ms)"); +#endif + } + + if (resetTick == null) + { + WindowCount = 0; //No rate limit info, disable limits on this bucket (should only ever happen with a user token) +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Disabled Semaphore"); +#endif + return; + } + + if (!hasQueuedReset || resetTick > _resetTick) + { + _resetTick = resetTick; + LastAttemptAt = resetTick.Value; //Make sure we dont destroy this until after its been reset +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Reset in {(int)Math.Ceiling((resetTick - DateTimeOffset.UtcNow).Value.TotalMilliseconds)} ms"); +#endif + + 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 + { +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] * Reset *"); +#endif + _semaphore = WindowCount; + _resetTick = null; + return; + } + } + } + } + + private void ThrowRetryLimit(RestRequest request) + { + if ((request.Options.RetryMode & RetryMode.RetryRatelimit) == 0) + throw new RateLimitedException(request); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Queue/Requests/JsonRestRequest.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Queue/Requests/JsonRestRequest.cs new file mode 100644 index 0000000..2949bab --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/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 json, RequestOptions options) + : base(client, method, endpoint, options) + { + Json = json; + } + + public override async Task SendAsync() + { + return await Client.SendAsync(Method, Endpoint, Json, Options.CancelToken, Options.HeaderOnly, Options.AuditLogReason).ConfigureAwait(false); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Queue/Requests/MultipartRestRequest.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Queue/Requests/MultipartRestRequest.cs new file mode 100644 index 0000000..c8d97bb --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/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, IReadOnlyDictionary multipartParams, RequestOptions options) + : base(client, method, endpoint, options) + { + MultipartParams = multipartParams; + } + + public override async Task SendAsync() + { + return await Client.SendAsync(Method, Endpoint, MultipartParams, Options.CancelToken, Options.HeaderOnly, Options.AuditLogReason).ConfigureAwait(false); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs new file mode 100644 index 0000000..bb5840c --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs @@ -0,0 +1,34 @@ +using Discord.Net.Rest; +using System; +using System.IO; +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 DateTimeOffset? TimeoutAt { get; } + public TaskCompletionSource Promise { get; } + public RequestOptions Options { get; } + + public RestRequest(IRestClient client, string method, string endpoint, RequestOptions options) + { + Preconditions.NotNull(options, nameof(options)); + + Client = client; + Method = method; + Endpoint = endpoint; + 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.CancelToken, Options.HeaderOnly, Options.AuditLogReason).ConfigureAwait(false); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Queue/Requests/WebSocketRequest.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/Queue/Requests/WebSocketRequest.cs new file mode 100644 index 0000000..81eb40b --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/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/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/RateLimitInfo.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/RateLimitInfo.cs new file mode 100644 index 0000000..13e9e39 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Net/RateLimitInfo.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace Discord.Net +{ + internal struct RateLimitInfo + { + public bool IsGlobal { get; } + public int? Limit { get; } + public int? Remaining { get; } + public int? RetryAfter { get; } + public DateTimeOffset? Reset { get; } + public TimeSpan? ResetAfter { get; } + public TimeSpan? Lag { get; } + + internal RateLimitInfo(Dictionary headers) + { + IsGlobal = headers.TryGetValue("X-RateLimit-Global", out string temp) && + bool.TryParse(temp, out var isGlobal) && isGlobal; + Limit = headers.TryGetValue("X-RateLimit-Limit", out temp) && + int.TryParse(temp, NumberStyles.None, CultureInfo.InvariantCulture, out var limit) ? limit : (int?)null; + Remaining = headers.TryGetValue("X-RateLimit-Remaining", out temp) && + int.TryParse(temp, NumberStyles.None, CultureInfo.InvariantCulture, out var remaining) ? remaining : (int?)null; + Reset = headers.TryGetValue("X-RateLimit-Reset", out temp) && + double.TryParse(temp, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var reset) ? DateTimeOffset.FromUnixTimeMilliseconds((long)(reset * 1000)) : (DateTimeOffset?)null; + RetryAfter = headers.TryGetValue("Retry-After", out temp) && + int.TryParse(temp, NumberStyles.None, CultureInfo.InvariantCulture, out var retryAfter) ? retryAfter : (int?)null; + ResetAfter = headers.TryGetValue("X-RateLimit-Reset-After", out temp) && + float.TryParse(temp, out var resetAfter) ? TimeSpan.FromMilliseconds((long)(resetAfter * 1000)) : (TimeSpan?)null; + Lag = headers.TryGetValue("Date", out temp) && + DateTimeOffset.TryParse(temp, CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ? DateTimeOffset.UtcNow - date : (TimeSpan?)null; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Utils/TypingNotifier.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Utils/TypingNotifier.cs new file mode 100644 index 0000000..745dbd3 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Rest/Utils/TypingNotifier.cs @@ -0,0 +1,48 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Rest +{ + internal class TypingNotifier : IDisposable + { + private readonly CancellationTokenSource _cancelToken; + private readonly IMessageChannel _channel; + private readonly RequestOptions _options; + + public TypingNotifier(IMessageChannel channel, RequestOptions options) + { + _cancelToken = new CancellationTokenSource(); + _channel = channel; + _options = options; + _ = RunAsync(); + } + + private async Task RunAsync() + { + try + { + var token = _cancelToken.Token; + while (!_cancelToken.IsCancellationRequested) + { + try + { + await _channel.TriggerTypingAsync(_options).ConfigureAwait(false); + } + catch + { + // ignored + } + + await Task.Delay(9500, token).ConfigureAwait(false); + } + } + catch (OperationCanceledException) { } + } + + public void Dispose() + { + _cancelToken.Cancel(); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/ExtendedGuild.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/ExtendedGuild.cs new file mode 100644 index 0000000..910f6d9 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/ExtendedGuild.cs @@ -0,0 +1,25 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; +using System; + +namespace Discord.API.Gateway +{ + internal class ExtendedGuild : Guild + { + [JsonProperty("unavailable")] + public bool? Unavailable { get; set; } + [JsonProperty("member_count")] + public int MemberCount { get; set; } + [JsonProperty("large")] + public bool Large { get; set; } + + [JsonProperty("presences")] + public Presence[] Presences { get; set; } + [JsonProperty("members")] + public GuildMember[] Members { get; set; } + [JsonProperty("channels")] + public Channel[] Channels { get; set; } + [JsonProperty("joined_at")] + public DateTimeOffset JoinedAt { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GatewayOpCode.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GatewayOpCode.cs new file mode 100644 index 0000000..13a2bb4 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GatewayOpCode.cs @@ -0,0 +1,33 @@ +#pragma warning disable CS1591 +namespace Discord.API.Gateway +{ + internal enum GatewayOpCode : byte + { + /// C←S - Used to send most events. + Dispatch = 0, + /// C↔S - Used to keep the connection alive and measure latency. + Heartbeat = 1, + /// C→S - Used to associate a connection with a token and specify configuration. + Identify = 2, + /// C→S - Used to update client's status and current game id. + StatusUpdate = 3, + /// C→S - Used to join a particular voice channel. + VoiceStateUpdate = 4, + /// C→S - Used to ensure the guild's voice server is alive. + VoiceServerPing = 5, + /// C→S - Used to resume a connection after a redirect occurs. + Resume = 6, + /// C←S - Used to notify a client that they must reconnect to another gateway. + Reconnect = 7, + /// C→S - Used to request members that were withheld by large_threshold + RequestGuildMembers = 8, + /// C←S - Used to notify the client that their session has expired and cannot be resumed. + InvalidSession = 9, + /// C←S - Used to provide information to the client immediately on connection. + Hello = 10, + /// C←S - Used to reply to a client's heartbeat. + HeartbeatAck = 11, + /// C→S - Used to request presence updates from particular guilds. + GuildSync = 12 + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GuildBanEvent.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GuildBanEvent.cs new file mode 100644 index 0000000..59a3304 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GuildBanEvent.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class GuildBanEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("user")] + public User User { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GuildEmojiUpdateEvent.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GuildEmojiUpdateEvent.cs new file mode 100644 index 0000000..715341d --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GuildEmojiUpdateEvent.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class GuildEmojiUpdateEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("emojis")] + public Emoji[] Emojis { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GuildMemberAddEvent.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GuildMemberAddEvent.cs new file mode 100644 index 0000000..350652f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GuildMemberAddEvent.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class GuildMemberAddEvent : GuildMember + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GuildMemberRemoveEvent.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GuildMemberRemoveEvent.cs new file mode 100644 index 0000000..501408a --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GuildMemberRemoveEvent.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class GuildMemberRemoveEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("user")] + public User User { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GuildMemberUpdateEvent.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GuildMemberUpdateEvent.cs new file mode 100644 index 0000000..a234d6d --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GuildMemberUpdateEvent.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class GuildMemberUpdateEvent : GuildMember + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GuildMembersChunkEvent.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GuildMembersChunkEvent.cs new file mode 100644 index 0000000..e401d7f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GuildMembersChunkEvent.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class GuildMembersChunkEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("members")] + public GuildMember[] Members { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GuildRoleCreateEvent.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GuildRoleCreateEvent.cs new file mode 100644 index 0000000..3409b1c --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GuildRoleCreateEvent.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class GuildRoleCreateEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("role")] + public Role Role { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GuildRoleDeleteEvent.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GuildRoleDeleteEvent.cs new file mode 100644 index 0000000..dbdaeff --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GuildRoleDeleteEvent.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class GuildRoleDeleteEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("role_id")] + public ulong RoleId { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GuildRoleUpdateEvent.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GuildRoleUpdateEvent.cs new file mode 100644 index 0000000..b04ecb1 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GuildRoleUpdateEvent.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class GuildRoleUpdateEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("role")] + public Role Role { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GuildSyncEvent.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GuildSyncEvent.cs new file mode 100644 index 0000000..6b2e6c0 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/GuildSyncEvent.cs @@ -0,0 +1,18 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class GuildSyncEvent + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("large")] + public bool Large { get; set; } + + [JsonProperty("presences")] + public Presence[] Presences { get; set; } + [JsonProperty("members")] + public GuildMember[] Members { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/HelloEvent.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/HelloEvent.cs new file mode 100644 index 0000000..e1ed946 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/HelloEvent.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class HelloEvent + { + [JsonProperty("heartbeat_interval")] + public int HeartbeatInterval { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs new file mode 100644 index 0000000..1e0bf71 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs @@ -0,0 +1,21 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Discord.API.Gateway +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class IdentifyParams + { + [JsonProperty("token")] + public string Token { get; set; } + [JsonProperty("properties")] + public IDictionary Properties { get; set; } + [JsonProperty("large_threshold")] + public int LargeThreshold { get; set; } + [JsonProperty("shard")] + public Optional ShardingParams { get; set; } + [JsonProperty("guild_subscriptions")] + public Optional GuildSubscriptions { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/MessageDeleteBulkEvent.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/MessageDeleteBulkEvent.cs new file mode 100644 index 0000000..a4cf7d7 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/MessageDeleteBulkEvent.cs @@ -0,0 +1,14 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Discord.API.Gateway +{ + internal class MessageDeleteBulkEvent + { + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + [JsonProperty("ids")] + public ulong[] Ids { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/Reaction.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/Reaction.cs new file mode 100644 index 0000000..62de456 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/Reaction.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class Reaction + { + [JsonProperty("user_id")] + public ulong UserId { get; set; } + [JsonProperty("message_id")] + public ulong MessageId { get; set; } + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + [JsonProperty("emoji")] + public Emoji Emoji { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/ReadyEvent.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/ReadyEvent.cs new file mode 100644 index 0000000..ab92d8c --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/ReadyEvent.cs @@ -0,0 +1,38 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class ReadyEvent + { + public class ReadState + { + [JsonProperty("id")] + public string ChannelId { get; set; } + [JsonProperty("mention_count")] + public int MentionCount { get; set; } + [JsonProperty("last_message_id")] + public string LastMessageId { get; set; } + } + + [JsonProperty("v")] + public int Version { get; set; } + [JsonProperty("user")] + public User User { get; set; } + [JsonProperty("session_id")] + public string SessionId { get; set; } + [JsonProperty("read_state")] + public ReadState[] ReadStates { get; set; } + [JsonProperty("guilds")] + public ExtendedGuild[] Guilds { get; set; } + [JsonProperty("private_channels")] + public Channel[] PrivateChannels { get; set; } + [JsonProperty("relationships")] + public Relationship[] Relationships { get; set; } + + //Ignored + /*[JsonProperty("user_settings")] + [JsonProperty("user_guild_settings")] + [JsonProperty("tutorial")]*/ + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/RecipientEvent.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/RecipientEvent.cs new file mode 100644 index 0000000..336ffd0 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/RecipientEvent.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class RecipientEvent + { + [JsonProperty("user")] + public User User { get; set; } + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/RemoveAllReactionsEvent.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/RemoveAllReactionsEvent.cs new file mode 100644 index 0000000..4833c51 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/RemoveAllReactionsEvent.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class RemoveAllReactionsEvent + { + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + [JsonProperty("message_id")] + public ulong MessageId { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/RequestMembersParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/RequestMembersParams.cs new file mode 100644 index 0000000..6a8d283 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/RequestMembersParams.cs @@ -0,0 +1,18 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Discord.API.Gateway +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class RequestMembersParams + { + [JsonProperty("query")] + public string Query { get; set; } + [JsonProperty("limit")] + public int Limit { get; set; } + + [JsonProperty("guild_id")] + public IEnumerable GuildIds { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/ResumeParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/ResumeParams.cs new file mode 100644 index 0000000..ffb4632 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/ResumeParams.cs @@ -0,0 +1,16 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class ResumeParams + { + [JsonProperty("token")] + public string Token { get; set; } + [JsonProperty("session_id")] + public string SessionId { get; set; } + [JsonProperty("seq")] + public int Sequence { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/ResumedEvent.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/ResumedEvent.cs new file mode 100644 index 0000000..d1347be --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/ResumedEvent.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class ResumedEvent + { + [JsonProperty("heartbeat_interval")] + public int HeartbeatInterval { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/StatusUpdateParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/StatusUpdateParams.cs new file mode 100644 index 0000000..fc0964c --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/StatusUpdateParams.cs @@ -0,0 +1,18 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class StatusUpdateParams + { + [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/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/TypingStartEvent.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/TypingStartEvent.cs new file mode 100644 index 0000000..5ceae4b --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/TypingStartEvent.cs @@ -0,0 +1,19 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class TypingStartEvent + { + [JsonProperty("user_id")] + public ulong UserId { get; set; } + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("member")] + public GuildMember Member { get; set; } + [JsonProperty("timestamp")] + public int Timestamp { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/VoiceServerUpdateEvent.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/VoiceServerUpdateEvent.cs new file mode 100644 index 0000000..29167c1 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/VoiceServerUpdateEvent.cs @@ -0,0 +1,15 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class VoiceServerUpdateEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("endpoint")] + public string Endpoint { get; set; } + [JsonProperty("token")] + public string Token { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/VoiceStateUpdateParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/VoiceStateUpdateParams.cs new file mode 100644 index 0000000..5211601 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/VoiceStateUpdateParams.cs @@ -0,0 +1,19 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class VoiceStateUpdateParams + { + [JsonProperty("self_mute")] + public bool SelfMute { get; set; } + [JsonProperty("self_deaf")] + public bool SelfDeaf { get; set; } + + [JsonProperty("guild_id")] + public ulong? GuildId { get; set; } + [JsonProperty("channel_id")] + public ulong? ChannelId { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/WebhookUpdateEvent.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/WebhookUpdateEvent.cs new file mode 100644 index 0000000..e5c7afe --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Gateway/WebhookUpdateEvent.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class WebhookUpdateEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/SocketFrame.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/SocketFrame.cs new file mode 100644 index 0000000..fae7414 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/SocketFrame.cs @@ -0,0 +1,17 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class SocketFrame + { + [JsonProperty("op")] + public int Operation { get; set; } + [JsonProperty("t", NullValueHandling = NullValueHandling.Ignore)] + public string Type { get; set; } + [JsonProperty("s", NullValueHandling = NullValueHandling.Ignore)] + public int? Sequence { get; set; } + [JsonProperty("d")] + public object Payload { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Voice/HelloEvent.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Voice/HelloEvent.cs new file mode 100644 index 0000000..8fdb080 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Voice/HelloEvent.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Discord.API.Voice +{ + internal class HelloEvent + { + [JsonProperty("heartbeat_interval")] + public int HeartbeatInterval { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Voice/IdentifyParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Voice/IdentifyParams.cs new file mode 100644 index 0000000..d446867 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Voice/IdentifyParams.cs @@ -0,0 +1,17 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Voice +{ + internal class IdentifyParams + { + [JsonProperty("server_id")] + public ulong GuildId { get; set; } + [JsonProperty("user_id")] + public ulong UserId { get; set; } + [JsonProperty("session_id")] + public string SessionId { get; set; } + [JsonProperty("token")] + public string Token { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Voice/ReadyEvent.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Voice/ReadyEvent.cs new file mode 100644 index 0000000..7188cd8 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Voice/ReadyEvent.cs @@ -0,0 +1,21 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; +using System; + +namespace Discord.API.Voice +{ + internal class ReadyEvent + { + [JsonProperty("ssrc")] + public uint SSRC { get; set; } + [JsonProperty("ip")] + public string Ip { get; set; } + [JsonProperty("port")] + public ushort Port { get; set; } + [JsonProperty("modes")] + public string[] Modes { get; set; } + [JsonProperty("heartbeat_interval")] + [Obsolete("This field is errorneous and should not be used", true)] + public int HeartbeatInterval { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Voice/SelectProtocolParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Voice/SelectProtocolParams.cs new file mode 100644 index 0000000..8c577e5 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Voice/SelectProtocolParams.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Voice +{ + internal class SelectProtocolParams + { + [JsonProperty("protocol")] + public string Protocol { get; set; } + [JsonProperty("data")] + public UdpProtocolInfo Data { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Voice/SessionDescriptionEvent.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Voice/SessionDescriptionEvent.cs new file mode 100644 index 0000000..45befad --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Voice/SessionDescriptionEvent.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Voice +{ + internal class SessionDescriptionEvent + { + [JsonProperty("secret_key")] + public byte[] SecretKey { get; set; } + [JsonProperty("mode")] + public string Mode { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Voice/SpeakingEvent.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Voice/SpeakingEvent.cs new file mode 100644 index 0000000..0272a8f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Voice/SpeakingEvent.cs @@ -0,0 +1,15 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Voice +{ + internal class SpeakingEvent + { + [JsonProperty("user_id")] + public ulong UserId { get; set; } + [JsonProperty("ssrc")] + public uint Ssrc { get; set; } + [JsonProperty("speaking")] + public bool Speaking { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Voice/SpeakingParams.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Voice/SpeakingParams.cs new file mode 100644 index 0000000..abdf906 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Voice/SpeakingParams.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Voice +{ + internal class SpeakingParams + { + [JsonProperty("speaking")] + public bool IsSpeaking { get; set; } + [JsonProperty("delay")] + public int Delay { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Voice/UdpProtocolInfo.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Voice/UdpProtocolInfo.cs new file mode 100644 index 0000000..6f4719e --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Voice/UdpProtocolInfo.cs @@ -0,0 +1,15 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Voice +{ + internal class UdpProtocolInfo + { + [JsonProperty("address")] + public string Address { get; set; } + [JsonProperty("port")] + public int Port { get; set; } + [JsonProperty("mode")] + public string Mode { get; set; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Voice/VoiceOpCode.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Voice/VoiceOpCode.cs new file mode 100644 index 0000000..67afe61 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/API/Voice/VoiceOpCode.cs @@ -0,0 +1,29 @@ +#pragma warning disable CS1591 +namespace Discord.API.Voice +{ + internal enum VoiceOpCode : byte + { + /// C→S - Used to associate a connection with a token. + Identify = 0, + /// C→S - Used to specify configuration. + SelectProtocol = 1, + /// C←S - Used to notify that the voice connection was successful and informs the client of available protocols. + Ready = 2, + /// C→S - Used to keep the connection alive and measure latency. + Heartbeat = 3, + /// C←S - Used to provide an encryption key to the client. + SessionDescription = 4, + /// C↔S - Used to inform that a certain user is speaking. + Speaking = 5, + /// C←S - Used to reply to a client's heartbeat. + HeartbeatAck = 6, + /// C→S - Used to resume a connection. + Resume = 7, + /// C←S - Used to inform the client the heartbeat interval. + Hello = 8, + /// C←S - Used to acknowledge a resumed connection. + Resumed = 9, + /// C←S - Used to notify that a client has disconnected. + ClientDisconnect = 13, + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/AssemblyInfo.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/AssemblyInfo.cs new file mode 100644 index 0000000..442ec7d --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Discord.Net.Relay")] +[assembly: InternalsVisibleTo("Discord.Net.Tests")] +[assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")] diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/AudioClient.Events.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/AudioClient.Events.cs new file mode 100644 index 0000000..b3e438a --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/AudioClient.Events.cs @@ -0,0 +1,51 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Audio +{ + internal partial class AudioClient + { + 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 LatencyUpdated + { + add { _latencyUpdatedEvent.Add(value); } + remove { _latencyUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _latencyUpdatedEvent = new AsyncEvent>(); + public event Func UdpLatencyUpdated + { + add { _udpLatencyUpdatedEvent.Add(value); } + remove { _udpLatencyUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _udpLatencyUpdatedEvent = new AsyncEvent>(); + public event Func StreamCreated + { + add { _streamCreatedEvent.Add(value); } + remove { _streamCreatedEvent.Remove(value); } + } + private readonly AsyncEvent> _streamCreatedEvent = new AsyncEvent>(); + public event Func StreamDestroyed + { + add { _streamDestroyedEvent.Add(value); } + remove { _streamDestroyedEvent.Remove(value); } + } + private readonly AsyncEvent> _streamDestroyedEvent = new AsyncEvent>(); + public event Func SpeakingUpdated + { + add { _speakingUpdatedEvent.Add(value); } + remove { _speakingUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _speakingUpdatedEvent = new AsyncEvent>(); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/AudioClient.cs new file mode 100644 index 0000000..2210e01 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -0,0 +1,476 @@ +using Discord.API.Voice; +using Discord.Audio.Streams; +using Discord.Logging; +using Discord.Net.Converters; +using Discord.WebSocket; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace Discord.Audio +{ + //TODO: Add audio reconnecting + internal partial class AudioClient : IAudioClient + { + internal struct StreamPair + { + public AudioInStream Reader; + public AudioOutStream Writer; + + public StreamPair(AudioInStream reader, AudioOutStream writer) + { + Reader = reader; + Writer = writer; + } + } + + private readonly Logger _audioLogger; + private readonly JsonSerializer _serializer; + private readonly ConnectionManager _connection; + private readonly SemaphoreSlim _stateLock; + private readonly ConcurrentQueue _heartbeatTimes; + private readonly ConcurrentQueue> _keepaliveTimes; + private readonly ConcurrentDictionary _ssrcMap; + private readonly ConcurrentDictionary _streams; + + private Task _heartbeatTask, _keepaliveTask; + private long _lastMessageTime; + private string _url, _sessionId, _token; + private ulong _userId; + private uint _ssrc; + private bool _isSpeaking; + + public SocketGuild Guild { get; } + public DiscordVoiceAPIClient ApiClient { get; private set; } + public int Latency { get; private set; } + public int UdpLatency { get; private set; } + public ulong ChannelId { get; internal set; } + internal byte[] SecretKey { get; private set; } + + private DiscordSocketClient Discord => Guild.Discord; + public ConnectionState ConnectionState => _connection.State; + + /// Creates a new REST/WebSocket discord client. + internal AudioClient(SocketGuild guild, int clientId, ulong channelId) + { + Guild = guild; + ChannelId = channelId; + _audioLogger = Discord.LogManager.CreateLogger($"Audio #{clientId}"); + + ApiClient = new DiscordVoiceAPIClient(guild.Id, Discord.WebSocketProvider, Discord.UdpSocketProvider); + ApiClient.SentGatewayMessage += async opCode => await _audioLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false); + ApiClient.SentDiscovery += async () => await _audioLogger.DebugAsync("Sent Discovery").ConfigureAwait(false); + //ApiClient.SentData += async bytes => await _audioLogger.DebugAsync($"Sent {bytes} Bytes").ConfigureAwait(false); + ApiClient.ReceivedEvent += ProcessMessageAsync; + ApiClient.ReceivedPacket += ProcessPacketAsync; + + _stateLock = new SemaphoreSlim(1, 1); + _connection = new ConnectionManager(_stateLock, _audioLogger, 30000, + OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x); + _connection.Connected += () => _connectedEvent.InvokeAsync(); + _connection.Disconnected += (ex, recon) => _disconnectedEvent.InvokeAsync(ex); + _heartbeatTimes = new ConcurrentQueue(); + _keepaliveTimes = new ConcurrentQueue>(); + _ssrcMap = new ConcurrentDictionary(); + _streams = new ConcurrentDictionary(); + + _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; + _serializer.Error += (s, e) => + { + _audioLogger.WarningAsync(e.ErrorContext.Error).GetAwaiter().GetResult(); + e.ErrorContext.Handled = true; + }; + + LatencyUpdated += async (old, val) => await _audioLogger.DebugAsync($"Latency = {val} ms").ConfigureAwait(false); + UdpLatencyUpdated += async (old, val) => await _audioLogger.DebugAsync($"UDP Latency = {val} ms").ConfigureAwait(false); + } + + internal async Task StartAsync(string url, ulong userId, string sessionId, string token) + { + _url = url; + _userId = userId; + _sessionId = sessionId; + _token = token; + await _connection.StartAsync().ConfigureAwait(false); + } + public async Task StopAsync() + { + await _connection.StopAsync().ConfigureAwait(false); + } + + private async Task OnConnectingAsync() + { + await _audioLogger.DebugAsync("Connecting ApiClient").ConfigureAwait(false); + await ApiClient.ConnectAsync("wss://" + _url + "?v=" + DiscordConfig.VoiceAPIVersion).ConfigureAwait(false); + await _audioLogger.DebugAsync("Listening on port " + ApiClient.UdpPort).ConfigureAwait(false); + await _audioLogger.DebugAsync("Sending Identity").ConfigureAwait(false); + await ApiClient.SendIdentityAsync(_userId, _sessionId, _token).ConfigureAwait(false); + + //Wait for READY + await _connection.WaitAsync().ConfigureAwait(false); + } + private async Task OnDisconnectingAsync(Exception ex) + { + await _audioLogger.DebugAsync("Disconnecting ApiClient").ConfigureAwait(false); + await ApiClient.DisconnectAsync().ConfigureAwait(false); + + //Wait for tasks to complete + await _audioLogger.DebugAsync("Waiting for heartbeater").ConfigureAwait(false); + var heartbeatTask = _heartbeatTask; + if (heartbeatTask != null) + await heartbeatTask.ConfigureAwait(false); + _heartbeatTask = null; + var keepaliveTask = _keepaliveTask; + if (keepaliveTask != null) + await keepaliveTask.ConfigureAwait(false); + _keepaliveTask = null; + + while (_heartbeatTimes.TryDequeue(out _)) { } + _lastMessageTime = 0; + + await ClearInputStreamsAsync().ConfigureAwait(false); + + await _audioLogger.DebugAsync("Sending Voice State").ConfigureAwait(false); + await Discord.ApiClient.SendVoiceStateUpdateAsync(Guild.Id, null, false, false).ConfigureAwait(false); + } + + public AudioOutStream CreateOpusStream(int bufferMillis) + { + var outputStream = new OutputStream(ApiClient); //Ignores header + var sodiumEncrypter = new SodiumEncryptStream( outputStream, this); //Passes header + var rtpWriter = new RTPWriteStream(sodiumEncrypter, _ssrc); //Consumes header, passes + return new BufferedWriteStream(rtpWriter, this, bufferMillis, _connection.CancelToken, _audioLogger); //Generates header + } + public AudioOutStream CreateDirectOpusStream() + { + var outputStream = new OutputStream(ApiClient); //Ignores header + var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); //Passes header + return new RTPWriteStream(sodiumEncrypter, _ssrc); //Consumes header (external input), passes + } + public AudioOutStream CreatePCMStream(AudioApplication application, int? bitrate, int bufferMillis, int packetLoss) + { + var outputStream = new OutputStream(ApiClient); //Ignores header + var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); //Passes header + var rtpWriter = new RTPWriteStream(sodiumEncrypter, _ssrc); //Consumes header, passes + var bufferedStream = new BufferedWriteStream(rtpWriter, this, bufferMillis, _connection.CancelToken, _audioLogger); //Ignores header, generates header + return new OpusEncodeStream(bufferedStream, bitrate ?? (96 * 1024), application, packetLoss); //Generates header + } + public AudioOutStream CreateDirectPCMStream(AudioApplication application, int? bitrate, int packetLoss) + { + var outputStream = new OutputStream(ApiClient); //Ignores header + var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); //Passes header + var rtpWriter = new RTPWriteStream(sodiumEncrypter, _ssrc); //Consumes header, passes + return new OpusEncodeStream(rtpWriter, bitrate ?? (96 * 1024), application, packetLoss); //Generates header + } + + internal async Task CreateInputStreamAsync(ulong userId) + { + //Assume Thread-safe + if (!_streams.ContainsKey(userId)) + { + var readerStream = new InputStream(); //Consumes header + var opusDecoder = new OpusDecodeStream(readerStream); //Passes header + //var jitterBuffer = new JitterBuffer(opusDecoder, _audioLogger); + var rtpReader = new RTPReadStream(opusDecoder); //Generates header + var decryptStream = new SodiumDecryptStream(rtpReader, this); //No header + _streams.TryAdd(userId, new StreamPair(readerStream, decryptStream)); + await _streamCreatedEvent.InvokeAsync(userId, readerStream); + } + } + internal AudioInStream GetInputStream(ulong id) + { + if (_streams.TryGetValue(id, out StreamPair streamPair)) + return streamPair.Reader; + return null; + } + internal async Task RemoveInputStreamAsync(ulong userId) + { + if (_streams.TryRemove(userId, out var pair)) + { + await _streamDestroyedEvent.InvokeAsync(userId).ConfigureAwait(false); + pair.Reader.Dispose(); + } + } + internal async Task ClearInputStreamsAsync() + { + foreach (var pair in _streams) + { + await _streamDestroyedEvent.InvokeAsync(pair.Key).ConfigureAwait(false); + pair.Value.Reader.Dispose(); + } + _ssrcMap.Clear(); + _streams.Clear(); + } + + private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload) + { + _lastMessageTime = Environment.TickCount; + + try + { + switch (opCode) + { + case VoiceOpCode.Ready: + { + await _audioLogger.DebugAsync("Received Ready").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); + + _ssrc = data.SSRC; + + if (!data.Modes.Contains(DiscordVoiceAPIClient.Mode)) + throw new InvalidOperationException($"Discord does not support {DiscordVoiceAPIClient.Mode}"); + + ApiClient.SetUdpEndpoint(data.Ip, data.Port); + await ApiClient.SendDiscoveryAsync(_ssrc).ConfigureAwait(false); + + + _heartbeatTask = RunHeartbeatAsync(41250, _connection.CancelToken); + } + break; + case VoiceOpCode.SessionDescription: + { + await _audioLogger.DebugAsync("Received SessionDescription").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); + + if (data.Mode != DiscordVoiceAPIClient.Mode) + throw new InvalidOperationException($"Discord selected an unexpected mode: {data.Mode}"); + + SecretKey = data.SecretKey; + _isSpeaking = false; + await ApiClient.SendSetSpeaking(false).ConfigureAwait(false); + _keepaliveTask = RunKeepaliveAsync(5000, _connection.CancelToken); + + var _ = _connection.CompleteAsync(); + } + break; + case VoiceOpCode.HeartbeatAck: + { + await _audioLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false); + + if (_heartbeatTimes.TryDequeue(out long time)) + { + int latency = (int)(Environment.TickCount - time); + int before = Latency; + Latency = latency; + + await _latencyUpdatedEvent.InvokeAsync(before, latency).ConfigureAwait(false); + } + } + break; + case VoiceOpCode.Speaking: + { + await _audioLogger.DebugAsync("Received Speaking").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + _ssrcMap[data.Ssrc] = data.UserId; //TODO: Memory Leak: SSRCs are never cleaned up + + await _speakingUpdatedEvent.InvokeAsync(data.UserId, data.Speaking); + } + break; + default: + await _audioLogger.WarningAsync($"Unknown OpCode ({opCode})").ConfigureAwait(false); + return; + } + } + catch (Exception ex) + { + await _audioLogger.ErrorAsync($"Error handling {opCode}", ex).ConfigureAwait(false); + return; + } + } + private async Task ProcessPacketAsync(byte[] packet) + { + try + { + if (_connection.State == ConnectionState.Connecting) + { + if (packet.Length != 70) + { + await _audioLogger.DebugAsync("Malformed Packet").ConfigureAwait(false); + return; + } + string ip; + int port; + try + { + ip = Encoding.UTF8.GetString(packet, 4, 70 - 6).TrimEnd('\0'); + port = (packet[69] << 8) | packet[68]; + } + catch (Exception ex) + { + await _audioLogger.DebugAsync("Malformed Packet", ex).ConfigureAwait(false); + return; + } + + await _audioLogger.DebugAsync("Received Discovery").ConfigureAwait(false); + await ApiClient.SendSelectProtocol(ip, port).ConfigureAwait(false); + } + else if (_connection.State == ConnectionState.Connected) + { + if (packet.Length == 8) + { + await _audioLogger.DebugAsync("Received Keepalive").ConfigureAwait(false); + + ulong value = + ((ulong)packet[0] >> 0) | + ((ulong)packet[1] >> 8) | + ((ulong)packet[2] >> 16) | + ((ulong)packet[3] >> 24) | + ((ulong)packet[4] >> 32) | + ((ulong)packet[5] >> 40) | + ((ulong)packet[6] >> 48) | + ((ulong)packet[7] >> 56); + + while (_keepaliveTimes.TryDequeue(out var pair)) + { + if (pair.Key == value) + { + int latency = Environment.TickCount - pair.Value; + int before = UdpLatency; + UdpLatency = latency; + + await _udpLatencyUpdatedEvent.InvokeAsync(before, latency).ConfigureAwait(false); + break; + } + } + } + else + { + if (!RTPReadStream.TryReadSsrc(packet, 0, out var ssrc)) + { + await _audioLogger.DebugAsync("Malformed Frame").ConfigureAwait(false); + return; + } + if (!_ssrcMap.TryGetValue(ssrc, out var userId)) + { + await _audioLogger.DebugAsync($"Unknown SSRC {ssrc}").ConfigureAwait(false); + return; + } + if (!_streams.TryGetValue(userId, out var pair)) + { + await _audioLogger.DebugAsync($"Unknown User {userId}").ConfigureAwait(false); + return; + } + try + { + await pair.Writer.WriteAsync(packet, 0, packet.Length).ConfigureAwait(false); + } + catch (Exception ex) + { + await _audioLogger.DebugAsync("Malformed Frame", ex).ConfigureAwait(false); + return; + } + //await _audioLogger.DebugAsync($"Received {packet.Length} bytes from user {userId}").ConfigureAwait(false); + } + } + } + catch (Exception ex) + { + await _audioLogger.WarningAsync("Failed to process UDP packet", ex).ConfigureAwait(false); + return; + } + } + + private async Task RunHeartbeatAsync(int intervalMillis, CancellationToken cancelToken) + { + //TODO: Clean this up when Discord's session patch is live + try + { + await _audioLogger.DebugAsync("Heartbeat Started").ConfigureAwait(false); + while (!cancelToken.IsCancellationRequested) + { + var now = Environment.TickCount; + + //Did server respond to our last heartbeat? + if (_heartbeatTimes.Count != 0 && (now - _lastMessageTime) > intervalMillis && + ConnectionState == ConnectionState.Connected) + { + _connection.Error(new Exception("Server missed last heartbeat")); + return; + } + + _heartbeatTimes.Enqueue(now); + try + { + await ApiClient.SendHeartbeatAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + await _audioLogger.WarningAsync("Failed to send heartbeat", ex).ConfigureAwait(false); + } + + await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); + } + await _audioLogger.DebugAsync("Heartbeat Stopped").ConfigureAwait(false); + } + catch (OperationCanceledException) + { + await _audioLogger.DebugAsync("Heartbeat Stopped").ConfigureAwait(false); + } + catch (Exception ex) + { + await _audioLogger.ErrorAsync("Heartbeat Errored", ex).ConfigureAwait(false); + } + } + private async Task RunKeepaliveAsync(int intervalMillis, CancellationToken cancelToken) + { + try + { + await _audioLogger.DebugAsync("Keepalive Started").ConfigureAwait(false); + while (!cancelToken.IsCancellationRequested) + { + var now = Environment.TickCount; + + try + { + ulong value = await ApiClient.SendKeepaliveAsync().ConfigureAwait(false); + if (_keepaliveTimes.Count < 12) //No reply for 60 Seconds + _keepaliveTimes.Enqueue(new KeyValuePair(value, now)); + } + catch (Exception ex) + { + await _audioLogger.WarningAsync("Failed to send keepalive", ex).ConfigureAwait(false); + } + + await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); + } + await _audioLogger.DebugAsync("Keepalive Stopped").ConfigureAwait(false); + } + catch (OperationCanceledException) + { + await _audioLogger.DebugAsync("Keepalive Stopped").ConfigureAwait(false); + } + catch (Exception ex) + { + await _audioLogger.ErrorAsync("Keepalive Errored", ex).ConfigureAwait(false); + } + } + + public async Task SetSpeakingAsync(bool value) + { + if (_isSpeaking != value) + { + _isSpeaking = value; + await ApiClient.SendSetSpeaking(value).ConfigureAwait(false); + } + } + + internal void Dispose(bool disposing) + { + if (disposing) + { + StopAsync().GetAwaiter().GetResult(); + ApiClient.Dispose(); + _stateLock?.Dispose(); + } + } + /// + public void Dispose() => Dispose(true); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Opus/OpusApplication.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Opus/OpusApplication.cs new file mode 100644 index 0000000..e288bb6 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Opus/OpusApplication.cs @@ -0,0 +1,9 @@ +namespace Discord.Audio +{ + internal enum OpusApplication : int + { + Voice = 2048, + MusicOrMixed = 2049, + LowLatency = 2051 + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Opus/OpusConverter.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Opus/OpusConverter.cs new file mode 100644 index 0000000..4179ce9 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Opus/OpusConverter.cs @@ -0,0 +1,47 @@ +using System; + +namespace Discord.Audio +{ + internal abstract class OpusConverter : IDisposable + { + protected IntPtr _ptr; + + public const int SamplingRate = 48000; + public const int Channels = 2; + public const int FrameMillis = 20; + + public const int SampleBytes = sizeof(short) * Channels; + + public const int FrameSamplesPerChannel = SamplingRate / 1000 * FrameMillis; + public const int FrameSamples = FrameSamplesPerChannel * Channels; + public const int FrameBytes = FrameSamplesPerChannel * SampleBytes; + + protected bool _isDisposed = false; + + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + _isDisposed = true; + } + ~OpusConverter() + { + Dispose(false); + } + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected static void CheckError(int result) + { + if (result < 0) + throw new Exception($"Opus Error: {(OpusError)result}"); + } + protected static void CheckError(OpusError error) + { + if ((int)error < 0) + throw new Exception($"Opus Error: {error}"); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Opus/OpusCtl.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Opus/OpusCtl.cs new file mode 100644 index 0000000..0b6a4e3 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Opus/OpusCtl.cs @@ -0,0 +1,12 @@ +namespace Discord.Audio +{ + //https://github.com/gcp/opus/blob/master/include/opus_defines.h + internal enum OpusCtl : int + { + SetBitrate = 4002, + SetBandwidth = 4008, + SetInbandFEC = 4012, + SetPacketLossPercent = 4014, + SetSignal = 4024 + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs new file mode 100644 index 0000000..41c48e1 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs @@ -0,0 +1,43 @@ +using System; +using System.Runtime.InteropServices; + +namespace Discord.Audio +{ + internal unsafe class OpusDecoder : OpusConverter + { + [DllImport("opus", EntryPoint = "opus_decoder_create", CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr CreateDecoder(int Fs, int channels, out OpusError error); + [DllImport("opus", EntryPoint = "opus_decoder_destroy", CallingConvention = CallingConvention.Cdecl)] + private static extern void DestroyDecoder(IntPtr decoder); + [DllImport("opus", EntryPoint = "opus_decode", CallingConvention = CallingConvention.Cdecl)] + private static extern int Decode(IntPtr st, byte* data, int len, byte* pcm, int max_frame_size, int decode_fec); + [DllImport("opus", EntryPoint = "opus_decoder_ctl", CallingConvention = CallingConvention.Cdecl)] + private static extern int DecoderCtl(IntPtr st, OpusCtl request, int value); + + public OpusDecoder() + { + _ptr = CreateDecoder(SamplingRate, Channels, out var error); + CheckError(error); + } + + public unsafe int DecodeFrame(byte[] input, int inputOffset, int inputCount, byte[] output, int outputOffset, bool decodeFEC) + { + int result = 0; + fixed (byte* inPtr = input) + fixed (byte* outPtr = output) + result = Decode(_ptr, inPtr + inputOffset, inputCount, outPtr + outputOffset, FrameSamplesPerChannel, decodeFEC ? 1 : 0); + CheckError(result); + return result * SampleBytes; + } + + protected override void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (_ptr != IntPtr.Zero) + DestroyDecoder(_ptr); + base.Dispose(disposing); + } + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs new file mode 100644 index 0000000..1ff5a5d --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs @@ -0,0 +1,76 @@ +using System; +using System.Runtime.InteropServices; + +namespace Discord.Audio +{ + internal unsafe class OpusEncoder : OpusConverter + { + [DllImport("opus", EntryPoint = "opus_encoder_create", CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr CreateEncoder(int Fs, int channels, int application, out OpusError error); + [DllImport("opus", EntryPoint = "opus_encoder_destroy", CallingConvention = CallingConvention.Cdecl)] + private static extern void DestroyEncoder(IntPtr encoder); + [DllImport("opus", EntryPoint = "opus_encode", CallingConvention = CallingConvention.Cdecl)] + private static extern int Encode(IntPtr st, byte* pcm, int frame_size, byte* data, int max_data_bytes); + [DllImport("opus", EntryPoint = "opus_encoder_ctl", CallingConvention = CallingConvention.Cdecl)] + private static extern OpusError EncoderCtl(IntPtr st, OpusCtl request, int value); + + public AudioApplication Application { get; } + public int BitRate { get;} + + public OpusEncoder(int bitrate, AudioApplication application, int packetLoss) + { + if (bitrate < 1 || bitrate > DiscordVoiceAPIClient.MaxBitrate) + throw new ArgumentOutOfRangeException(nameof(bitrate)); + + Application = application; + BitRate = bitrate; + + OpusApplication opusApplication; + OpusSignal opusSignal; + switch (application) + { + case AudioApplication.Mixed: + opusApplication = OpusApplication.MusicOrMixed; + opusSignal = OpusSignal.Auto; + break; + case AudioApplication.Music: + opusApplication = OpusApplication.MusicOrMixed; + opusSignal = OpusSignal.Music; + break; + case AudioApplication.Voice: + opusApplication = OpusApplication.Voice; + opusSignal = OpusSignal.Voice; + break; + default: + throw new ArgumentOutOfRangeException(nameof(application)); + } + + _ptr = CreateEncoder(SamplingRate, Channels, (int)opusApplication, out var error); + CheckError(error); + CheckError(EncoderCtl(_ptr, OpusCtl.SetSignal, (int)opusSignal)); + CheckError(EncoderCtl(_ptr, OpusCtl.SetPacketLossPercent, packetLoss)); //% + CheckError(EncoderCtl(_ptr, OpusCtl.SetInbandFEC, 1)); //True + CheckError(EncoderCtl(_ptr, OpusCtl.SetBitrate, bitrate)); + } + + public unsafe int EncodeFrame(byte[] input, int inputOffset, byte[] output, int outputOffset) + { + int result = 0; + fixed (byte* inPtr = input) + fixed (byte* outPtr = output) + result = Encode(_ptr, inPtr + inputOffset, FrameSamplesPerChannel, outPtr + outputOffset, output.Length - outputOffset); + CheckError(result); + return result; + } + + protected override void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (_ptr != IntPtr.Zero) + DestroyEncoder(_ptr); + base.Dispose(disposing); + } + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Opus/OpusError.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Opus/OpusError.cs new file mode 100644 index 0000000..d29d8b9 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Opus/OpusError.cs @@ -0,0 +1,14 @@ +namespace Discord.Audio +{ + internal enum OpusError : int + { + OK = 0, + BadArg = -1, + BufferToSmall = -2, + InternalError = -3, + InvalidPacket = -4, + Unimplemented = -5, + InvalidState = -6, + AllocFail = -7 + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Opus/OpusSignal.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Opus/OpusSignal.cs new file mode 100644 index 0000000..3f95183 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Opus/OpusSignal.cs @@ -0,0 +1,9 @@ +namespace Discord.Audio +{ + internal enum OpusSignal : int + { + Auto = -1000, + Voice = 3001, + Music = 3002, + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Sodium/SecretBox.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Sodium/SecretBox.cs new file mode 100644 index 0000000..4187c9f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Sodium/SecretBox.cs @@ -0,0 +1,36 @@ +using System; +using System.Runtime.InteropServices; + +namespace Discord.Audio +{ + public unsafe static class SecretBox + { + [DllImport("libsodium", EntryPoint = "crypto_secretbox_easy", CallingConvention = CallingConvention.Cdecl)] + private static extern int SecretBoxEasy(byte* output, byte* input, long inputLength, byte[] nonce, byte[] secret); + [DllImport("libsodium", EntryPoint = "crypto_secretbox_open_easy", CallingConvention = CallingConvention.Cdecl)] + private static extern int SecretBoxOpenEasy(byte* output, byte* input, long inputLength, byte[] nonce, byte[] secret); + + public static int Encrypt(byte[] input, int inputOffset, int inputLength, byte[] output, int outputOffset, byte[] nonce, byte[] secret) + { + fixed (byte* inPtr = input) + fixed (byte* outPtr = output) + { + int error = SecretBoxEasy(outPtr + outputOffset, inPtr + inputOffset, inputLength, nonce, secret); + if (error != 0) + throw new Exception($"Sodium Error: {error}"); + return inputLength + 16; + } + } + public static int Decrypt(byte[] input, int inputOffset, int inputLength, byte[] output, int outputOffset, byte[] nonce, byte[] secret) + { + fixed (byte* inPtr = input) + fixed (byte* outPtr = output) + { + int error = SecretBoxOpenEasy(outPtr + outputOffset, inPtr + inputOffset, inputLength, nonce, secret); + if (error != 0) + throw new Exception($"Sodium Error: {error}"); + return inputLength - 16; + } + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs new file mode 100644 index 0000000..16ad0ae --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs @@ -0,0 +1,190 @@ +using Discord.Logging; +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Audio.Streams +{ + /// Wraps another stream with a timed buffer. + public class BufferedWriteStream : AudioOutStream + { + private const int MaxSilenceFrames = 10; + + private struct Frame + { + public Frame(byte[] buffer, int bytes) + { + Buffer = buffer; + Bytes = bytes; + } + + public readonly byte[] Buffer; + public readonly int Bytes; + } + + private static readonly byte[] _silenceFrame = new byte[0]; + + private readonly AudioClient _client; + private readonly AudioStream _next; + private readonly CancellationTokenSource _disposeTokenSource, _cancelTokenSource; + private readonly CancellationToken _cancelToken; + private readonly Task _task; + private readonly ConcurrentQueue _queuedFrames; + private readonly ConcurrentQueue _bufferPool; + private readonly SemaphoreSlim _queueLock; + private readonly Logger _logger; + private readonly int _ticksPerFrame, _queueLength; + private bool _isPreloaded; + private int _silenceFrames; + + public BufferedWriteStream(AudioStream next, IAudioClient client, int bufferMillis, CancellationToken cancelToken, int maxFrameSize = 1500) + : this(next, client as AudioClient, bufferMillis, cancelToken, null, maxFrameSize) { } + internal BufferedWriteStream(AudioStream next, AudioClient client, int bufferMillis, CancellationToken cancelToken, Logger logger, int maxFrameSize = 1500) + { + //maxFrameSize = 1275 was too limiting at 128kbps,2ch,60ms + _next = next; + _client = client; + _ticksPerFrame = OpusEncoder.FrameMillis; + _logger = logger; + _queueLength = (bufferMillis + (_ticksPerFrame - 1)) / _ticksPerFrame; //Round up + + _disposeTokenSource = new CancellationTokenSource(); + _cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_disposeTokenSource.Token, cancelToken); + _cancelToken = _cancelTokenSource.Token; + _queuedFrames = new ConcurrentQueue(); + _bufferPool = new ConcurrentQueue(); + for (int i = 0; i < _queueLength; i++) + _bufferPool.Enqueue(new byte[maxFrameSize]); + _queueLock = new SemaphoreSlim(_queueLength, _queueLength); + _silenceFrames = MaxSilenceFrames; + + _task = Run(); + } + protected override void Dispose(bool disposing) + { + if (disposing) + { + _disposeTokenSource?.Cancel(); + _disposeTokenSource?.Dispose(); + _cancelTokenSource?.Dispose(); + _queueLock?.Dispose(); + } + base.Dispose(disposing); + } + + private Task Run() + { + return Task.Run(async () => + { + try + { + while (!_isPreloaded && !_cancelToken.IsCancellationRequested) + await Task.Delay(1).ConfigureAwait(false); + + long nextTick = Environment.TickCount; + ushort seq = 0; + uint timestamp = 0; + while (!_cancelToken.IsCancellationRequested) + { + long tick = Environment.TickCount; + long dist = nextTick - tick; + if (dist <= 0) + { + if (_queuedFrames.TryDequeue(out Frame frame)) + { + await _client.SetSpeakingAsync(true).ConfigureAwait(false); + _next.WriteHeader(seq, timestamp, false); + await _next.WriteAsync(frame.Buffer, 0, frame.Bytes).ConfigureAwait(false); + _bufferPool.Enqueue(frame.Buffer); + _queueLock.Release(); + nextTick += _ticksPerFrame; + seq++; + timestamp += OpusEncoder.FrameSamplesPerChannel; + _silenceFrames = 0; +#if DEBUG + var _ = _logger?.DebugAsync($"Sent {frame.Bytes} bytes ({_queuedFrames.Count} frames buffered)"); +#endif + } + else + { + while ((nextTick - tick) <= 0) + { + if (_silenceFrames++ < MaxSilenceFrames) + { + _next.WriteHeader(seq, timestamp, false); + await _next.WriteAsync(_silenceFrame, 0, _silenceFrame.Length).ConfigureAwait(false); + } + else + await _client.SetSpeakingAsync(false).ConfigureAwait(false); + nextTick += _ticksPerFrame; + seq++; + timestamp += OpusEncoder.FrameSamplesPerChannel; + } +#if DEBUG + var _ = _logger?.DebugAsync("Buffer underrun"); +#endif + } + } + else + await Task.Delay((int)(dist)/*, _cancelToken*/).ConfigureAwait(false); + } + } + catch (OperationCanceledException) { } + }); + } + + public override void WriteHeader(ushort seq, uint timestamp, bool missed) { } //Ignore, we use our own timing + public override async Task WriteAsync(byte[] data, int offset, int count, CancellationToken cancelToken) + { + CancellationTokenSource writeCancelToken = null; + if (cancelToken.CanBeCanceled) + { + writeCancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _cancelToken); + cancelToken = writeCancelToken.Token; + } + else + cancelToken = _cancelToken; + + await _queueLock.WaitAsync(-1, cancelToken).ConfigureAwait(false); + if (!_bufferPool.TryDequeue(out byte[] buffer)) + { +#if DEBUG + var _ = _logger?.DebugAsync("Buffer overflow"); //Should never happen because of the queueLock +#endif +#pragma warning disable IDISP016 + writeCancelToken?.Dispose(); +#pragma warning restore IDISP016 + return; + } + Buffer.BlockCopy(data, offset, buffer, 0, count); + _queuedFrames.Enqueue(new Frame(buffer, count)); + if (!_isPreloaded && _queuedFrames.Count == _queueLength) + { +#if DEBUG + var _ = _logger?.DebugAsync("Preloaded"); +#endif + _isPreloaded = true; + } + writeCancelToken?.Dispose(); + } + + public override async Task FlushAsync(CancellationToken cancelToken) + { + while (true) + { + cancelToken.ThrowIfCancellationRequested(); + if (_queuedFrames.Count == 0) + return; + await Task.Delay(250, cancelToken).ConfigureAwait(false); + } + } + public override Task ClearAsync(CancellationToken cancelToken) + { + do + cancelToken.ThrowIfCancellationRequested(); + while (_queuedFrames.TryDequeue(out _)); + return Task.Delay(0); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Streams/InputStream.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Streams/InputStream.cs new file mode 100644 index 0000000..6233c47 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Streams/InputStream.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Audio.Streams +{ + /// Reads the payload from an RTP frame + public class InputStream : AudioInStream + { + private const int MaxFrames = 100; //1-2 Seconds + + private ConcurrentQueue _frames; + private SemaphoreSlim _signal; + private ushort _nextSeq; + private uint _nextTimestamp; + private bool _nextMissed; + private bool _hasHeader; + private bool _isDisposed; + + public override bool CanRead => !_isDisposed; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override int AvailableFrames => _signal.CurrentCount; + + public InputStream() + { + _frames = new ConcurrentQueue(); + _signal = new SemaphoreSlim(0, MaxFrames); + } + + public override bool TryReadFrame(CancellationToken cancelToken, out RTPFrame frame) + { + cancelToken.ThrowIfCancellationRequested(); + + if (_signal.Wait(0)) + { + _frames.TryDequeue(out frame); + return true; + } + frame = default(RTPFrame); + return false; + } + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) + { + cancelToken.ThrowIfCancellationRequested(); + + var frame = await ReadFrameAsync(cancelToken).ConfigureAwait(false); + if (count < frame.Payload.Length) + throw new InvalidOperationException("Buffer is too small."); + Buffer.BlockCopy(frame.Payload, 0, buffer, offset, frame.Payload.Length); + return frame.Payload.Length; + } + public override async Task ReadFrameAsync(CancellationToken cancelToken) + { + cancelToken.ThrowIfCancellationRequested(); + + await _signal.WaitAsync(cancelToken).ConfigureAwait(false); + _frames.TryDequeue(out RTPFrame frame); + return frame; + } + + public override void WriteHeader(ushort seq, uint timestamp, bool missed) + { + if (_hasHeader) + throw new InvalidOperationException("Header received with no payload"); + _hasHeader = true; + _nextSeq = seq; + _nextTimestamp = timestamp; + _nextMissed = missed; + } + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) + { + cancelToken.ThrowIfCancellationRequested(); + + if (_signal.CurrentCount >= MaxFrames) //1-2 seconds + { + _hasHeader = false; + return Task.Delay(0); //Buffer overloaded + } + if (!_hasHeader) + throw new InvalidOperationException("Received payload without an RTP header"); + _hasHeader = false; + byte[] payload = new byte[count]; + Buffer.BlockCopy(buffer, offset, payload, 0, count); + + _frames.Enqueue(new RTPFrame( + sequence: _nextSeq, + timestamp: _nextTimestamp, + missed: _nextMissed, + payload: payload + )); + _signal.Release(); + return Task.Delay(0); + } + + protected override void Dispose(bool isDisposing) + { + if (!_isDisposed) + { + if (isDisposing) + { + _signal?.Dispose(); + } + + _isDisposed = true; + } + + base.Dispose(isDisposing); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Streams/JitterBuffer.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Streams/JitterBuffer.cs new file mode 100644 index 0000000..10f842a --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Streams/JitterBuffer.cs @@ -0,0 +1,246 @@ +/*using Discord.Logging; +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Audio.Streams +{ + /// Wraps another stream with a timed buffer and packet loss detection. + public class JitterBuffer : AudioOutStream + { + private struct Frame + { + public Frame(byte[] buffer, int bytes, ushort sequence, uint timestamp) + { + Buffer = buffer; + Bytes = bytes; + Sequence = sequence; + Timestamp = timestamp; + } + + public readonly byte[] Buffer; + public readonly int Bytes; + public readonly ushort Sequence; + public readonly uint Timestamp; + } + + private static readonly byte[] _silenceFrame = new byte[0]; + + private readonly AudioStream _next; + private readonly CancellationTokenSource _cancelTokenSource; + private readonly CancellationToken _cancelToken; + private readonly Task _task; + private readonly ConcurrentQueue _queuedFrames; + private readonly ConcurrentQueue _bufferPool; + private readonly SemaphoreSlim _queueLock; + private readonly Logger _logger; + private readonly int _ticksPerFrame, _queueLength; + private bool _isPreloaded, _hasHeader; + + private ushort _seq, _nextSeq; + private uint _timestamp, _nextTimestamp; + private bool _isFirst; + + public JitterBuffer(AudioStream next, int bufferMillis = 60, int maxFrameSize = 1500) + : this(next, null, bufferMillis, maxFrameSize) { } + internal JitterBuffer(AudioStream next, Logger logger, int bufferMillis = 60, int maxFrameSize = 1500) + { + //maxFrameSize = 1275 was too limiting at 128kbps,2ch,60ms + _next = next; + _ticksPerFrame = OpusEncoder.FrameMillis; + _logger = logger; + _queueLength = (bufferMillis + (_ticksPerFrame - 1)) / _ticksPerFrame; //Round up + + _cancelTokenSource = new CancellationTokenSource(); + _cancelToken = _cancelTokenSource.Token; + _queuedFrames = new ConcurrentQueue(); + _bufferPool = new ConcurrentQueue(); + for (int i = 0; i < _queueLength; i++) + _bufferPool.Enqueue(new byte[maxFrameSize]); + _queueLock = new SemaphoreSlim(_queueLength, _queueLength); + + _isFirst = true; + _task = Run(); + } + protected override void Dispose(bool disposing) + { + if (disposing) + _cancelTokenSource.Cancel(); + base.Dispose(disposing); + } + + private Task Run() + { + return Task.Run(async () => + { + try + { + long nextTick = Environment.TickCount; + int silenceFrames = 0; + while (!_cancelToken.IsCancellationRequested) + { + long tick = Environment.TickCount; + long dist = nextTick - tick; + if (dist > 0) + { + await Task.Delay((int)dist).ConfigureAwait(false); + continue; + } + nextTick += _ticksPerFrame; + if (!_isPreloaded) + { + await Task.Delay(_ticksPerFrame).ConfigureAwait(false); + continue; + } + + if (_queuedFrames.TryPeek(out Frame frame)) + { + silenceFrames = 0; + uint distance = (uint)(frame.Timestamp - _timestamp); + bool restartSeq = _isFirst; + if (!_isFirst) + { + if (distance > uint.MaxValue - (OpusEncoder.FrameSamplesPerChannel * 50 * 5)) //Negative distances wraps + { + _queuedFrames.TryDequeue(out frame); + _bufferPool.Enqueue(frame.Buffer); + _queueLock.Release(); +#if DEBUG + var _ = _logger?.DebugAsync($"Dropped frame {_timestamp} ({_queuedFrames.Count} frames buffered)"); +#endif + continue; //This is a missed packet less than five seconds old, ignore it + } + } + + if (distance == 0 || restartSeq) + { + //This is the frame we expected + _seq = frame.Sequence; + _timestamp = frame.Timestamp; + _isFirst = false; + silenceFrames = 0; + + _next.WriteHeader(_seq++, _timestamp, false); + await _next.WriteAsync(frame.Buffer, 0, frame.Bytes).ConfigureAwait(false); + _queuedFrames.TryDequeue(out frame); + _bufferPool.Enqueue(frame.Buffer); + _queueLock.Release(); +#if DEBUG + var _ = _logger?.DebugAsync($"Read frame {_timestamp} ({_queuedFrames.Count} frames buffered)"); +#endif + } + else if (distance == OpusEncoder.FrameSamplesPerChannel) + { + //Missed this frame, but the next queued one might have FEC info + _next.WriteHeader(_seq++, _timestamp, true); + await _next.WriteAsync(frame.Buffer, 0, frame.Bytes).ConfigureAwait(false); +#if DEBUG + var _ = _logger?.DebugAsync($"Recreated Frame {_timestamp} (Next is {frame.Timestamp}) ({_queuedFrames.Count} frames buffered)"); +#endif + } + else + { + //Missed this frame and we have no FEC data to work with + _next.WriteHeader(_seq++, _timestamp, true); + await _next.WriteAsync(null, 0, 0).ConfigureAwait(false); +#if DEBUG + var _ = _logger?.DebugAsync($"Missed Frame {_timestamp} (Next is {frame.Timestamp}) ({_queuedFrames.Count} frames buffered)"); +#endif + } + } + else if (!_isFirst) + { + //Missed this frame and we have no FEC data to work with + _next.WriteHeader(_seq++, _timestamp, true); + await _next.WriteAsync(null, 0, 0).ConfigureAwait(false); + if (silenceFrames < 5) + silenceFrames++; + else + { + _isFirst = true; + _isPreloaded = false; + } +#if DEBUG + var _ = _logger?.DebugAsync($"Missed Frame {_timestamp} ({_queuedFrames.Count} frames buffered)"); +#endif + } + _timestamp += OpusEncoder.FrameSamplesPerChannel; + } + } + catch (OperationCanceledException) { } + }); + } + + public override void WriteHeader(ushort seq, uint timestamp, bool missed) + { + if (_hasHeader) + throw new InvalidOperationException("Header received with no payload"); + _nextSeq = seq; + _nextTimestamp = timestamp; + _hasHeader = true; + } + public override async Task WriteAsync(byte[] data, int offset, int count, CancellationToken cancelToken) + { + if (cancelToken.CanBeCanceled) + cancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _cancelToken).Token; + else + cancelToken = _cancelToken; + + if (!_hasHeader) + throw new InvalidOperationException("Received payload without an RTP header"); + _hasHeader = false; + + uint distance = (uint)(_nextTimestamp - _timestamp); + if (!_isFirst && (distance == 0 || distance > OpusEncoder.FrameSamplesPerChannel * 50 * 5)) //Negative distances wraps + { +#if DEBUG + var _ = _logger?.DebugAsync($"Frame {_nextTimestamp} was {distance} samples off. Ignoring."); +#endif + return; //This is an old frame, ignore + } + + if (!await _queueLock.WaitAsync(0).ConfigureAwait(false)) + { +#if DEBUG + var _ = _logger?.DebugAsync($"Buffer overflow"); +#endif + return; + } + _bufferPool.TryDequeue(out byte[] buffer); + + Buffer.BlockCopy(data, offset, buffer, 0, count); +#if DEBUG + { + var _ = _logger?.DebugAsync($"Queued Frame {_nextTimestamp}."); + } +#endif + _queuedFrames.Enqueue(new Frame(buffer, count, _nextSeq, _nextTimestamp)); + if (!_isPreloaded && _queuedFrames.Count >= _queueLength) + { +#if DEBUG + var _ = _logger?.DebugAsync($"Preloaded"); +#endif + _isPreloaded = true; + } + } + + public override async Task FlushAsync(CancellationToken cancelToken) + { + while (true) + { + cancelToken.ThrowIfCancellationRequested(); + if (_queuedFrames.Count == 0) + return; + await Task.Delay(250, cancelToken).ConfigureAwait(false); + } + } + public override Task ClearAsync(CancellationToken cancelToken) + { + do + cancelToken.ThrowIfCancellationRequested(); + while (_queuedFrames.TryDequeue(out Frame ignored)); + return Task.Delay(0); + } + } +}*/ \ No newline at end of file diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs new file mode 100644 index 0000000..1861e35 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs @@ -0,0 +1,77 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Audio.Streams +{ + /// Converts Opus to PCM + public class OpusDecodeStream : AudioOutStream + { + public const int SampleRate = OpusEncodeStream.SampleRate; + + private readonly AudioStream _next; + private readonly OpusDecoder _decoder; + private readonly byte[] _buffer; + private bool _nextMissed; + private bool _hasHeader; + + public OpusDecodeStream(AudioStream next) + { + _next = next; + _buffer = new byte[OpusConverter.FrameBytes]; + _decoder = new OpusDecoder(); + } + + /// Header received with no payload. + public override void WriteHeader(ushort seq, uint timestamp, bool missed) + { + if (_hasHeader) + throw new InvalidOperationException("Header received with no payload."); + _hasHeader = true; + + _nextMissed = missed; + _next.WriteHeader(seq, timestamp, missed); + } + + /// Received payload without an RTP header. + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) + { + if (!_hasHeader) + throw new InvalidOperationException("Received payload without an RTP header."); + _hasHeader = false; + + if (!_nextMissed) + { + count = _decoder.DecodeFrame(buffer, offset, count, _buffer, 0, false); + await _next.WriteAsync(_buffer, 0, count, cancelToken).ConfigureAwait(false); + } + else if (count > 0) + { + count = _decoder.DecodeFrame(buffer, offset, count, _buffer, 0, true); + await _next.WriteAsync(_buffer, 0, count, cancelToken).ConfigureAwait(false); + } + else + { + count = _decoder.DecodeFrame(null, 0, 0, _buffer, 0, true); + await _next.WriteAsync(_buffer, 0, count, cancelToken).ConfigureAwait(false); + } + } + + public override async Task FlushAsync(CancellationToken cancelToken) + { + await _next.FlushAsync(cancelToken).ConfigureAwait(false); + } + public override async Task ClearAsync(CancellationToken cancelToken) + { + await _next.ClearAsync(cancelToken).ConfigureAwait(false); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + _decoder.Dispose(); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs new file mode 100644 index 0000000..035b92b --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs @@ -0,0 +1,98 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Audio.Streams +{ + /// Converts PCM to Opus + public class OpusEncodeStream : AudioOutStream + { + public const int SampleRate = 48000; + + private readonly AudioStream _next; + private readonly OpusEncoder _encoder; + private readonly byte[] _buffer; + private int _partialFramePos; + private ushort _seq; + private uint _timestamp; + + public OpusEncodeStream(AudioStream next, int bitrate, AudioApplication application, int packetLoss) + { + _next = next; + _encoder = new OpusEncoder(bitrate, application, packetLoss); + _buffer = new byte[OpusConverter.FrameBytes]; + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) + { + //Assume thread-safe + while (count > 0) + { + if (_partialFramePos == 0 && count >= OpusConverter.FrameBytes) + { + //We have enough data and no partial frames. Pass the buffer directly to the encoder + int encFrameSize = _encoder.EncodeFrame(buffer, offset, _buffer, 0); + _next.WriteHeader(_seq, _timestamp, false); + await _next.WriteAsync(_buffer, 0, encFrameSize, cancelToken).ConfigureAwait(false); + + offset += OpusConverter.FrameBytes; + count -= OpusConverter.FrameBytes; + _seq++; + _timestamp += OpusConverter.FrameSamplesPerChannel; + } + else if (_partialFramePos + count >= OpusConverter.FrameBytes) + { + //We have enough data to complete a previous partial frame. + int partialSize = OpusConverter.FrameBytes - _partialFramePos; + Buffer.BlockCopy(buffer, offset, _buffer, _partialFramePos, partialSize); + int encFrameSize = _encoder.EncodeFrame(_buffer, 0, _buffer, 0); + _next.WriteHeader(_seq, _timestamp, false); + await _next.WriteAsync(_buffer, 0, encFrameSize, cancelToken).ConfigureAwait(false); + + offset += partialSize; + count -= partialSize; + _partialFramePos = 0; + _seq++; + _timestamp += OpusConverter.FrameSamplesPerChannel; + } + else + { + //Not enough data to build a complete frame, store this part for later + Buffer.BlockCopy(buffer, offset, _buffer, _partialFramePos, count); + _partialFramePos += count; + break; + } + } + } + + /* //Opus throws memory errors on bad frames + public override async Task FlushAsync(CancellationToken cancelToken) + { + try + { + int encFrameSize = _encoder.EncodeFrame(_partialFrameBuffer, 0, _partialFramePos, _buffer, 0); + base.Write(_buffer, 0, encFrameSize); + } + catch (Exception) { } //Incomplete frame + _partialFramePos = 0; + await base.FlushAsync(cancelToken).ConfigureAwait(false); + }*/ + + public override async Task FlushAsync(CancellationToken cancelToken) + { + await _next.FlushAsync(cancelToken).ConfigureAwait(false); + } + public override async Task ClearAsync(CancellationToken cancelToken) + { + await _next.ClearAsync(cancelToken).ConfigureAwait(false); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + _encoder.Dispose(); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Streams/OutputStream.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Streams/OutputStream.cs new file mode 100644 index 0000000..cba4e3c --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Streams/OutputStream.cs @@ -0,0 +1,24 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Audio.Streams +{ + /// Wraps an IAudioClient, sending voice data on write. + public class OutputStream : AudioOutStream + { + private readonly DiscordVoiceAPIClient _client; + public OutputStream(IAudioClient client) + : this((client as AudioClient).ApiClient) { } + internal OutputStream(DiscordVoiceAPIClient client) + { + _client = client; + } + + public override void WriteHeader(ushort seq, uint timestamp, bool missed) { } //Ignore + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) + { + cancelToken.ThrowIfCancellationRequested(); + await _client.SendAsync(buffer, offset, count).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs new file mode 100644 index 0000000..120f67e --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs @@ -0,0 +1,80 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Audio.Streams +{ + /// Reads the payload from an RTP frame + public class RTPReadStream : AudioOutStream + { + private readonly AudioStream _next; + private readonly byte[] _buffer, _nonce; + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => true; + + public RTPReadStream(AudioStream next, int bufferSize = 4000) + { + _next = next; + _buffer = new byte[bufferSize]; + _nonce = new byte[24]; + } + + /// The token has had cancellation requested. + /// The associated has been disposed. + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) + { + cancelToken.ThrowIfCancellationRequested(); + + int headerSize = GetHeaderSize(buffer, offset); + + ushort seq = (ushort)((buffer[offset + 2] << 8) | + (buffer[offset + 3] << 0)); + + uint timestamp = (uint)((buffer[offset + 4] << 24) | + (buffer[offset + 5] << 16) | + (buffer[offset + 6] << 8) | + (buffer[offset + 7] << 0)); + + _next.WriteHeader(seq, timestamp, false); + await _next.WriteAsync(buffer, offset + headerSize, count - headerSize, cancelToken).ConfigureAwait(false); + } + + public static bool TryReadSsrc(byte[] buffer, int offset, out uint ssrc) + { + ssrc = 0; + if (buffer.Length - offset < 12) + return false; + + int version = (buffer[offset + 0] & 0b1100_0000) >> 6; + if (version != 2) + return false; + int type = (buffer[offset + 1] & 0b01111_1111); + if (type != 120) //Dynamic Discord type + return false; + + ssrc = (uint)((buffer[offset + 8] << 24) | + (buffer[offset + 9] << 16) | + (buffer[offset + 10] << 8) | + (buffer[offset + 11] << 0)); + return true; + } + + public static int GetHeaderSize(byte[] buffer, int offset) + { + byte headerByte = buffer[offset]; + bool extension = (headerByte & 0b0001_0000) != 0; + int csics = (headerByte & 0b0000_1111) >> 4; + + if (!extension) + return 12 + csics * 4; + + int extensionOffset = offset + 12 + (csics * 4); + int extensionLength = + (buffer[extensionOffset + 2] << 8) | + (buffer[extensionOffset + 3]); + return extensionOffset + 4 + (extensionLength * 4); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs new file mode 100644 index 0000000..ce407ea --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs @@ -0,0 +1,73 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Audio.Streams +{ + /// Wraps data in an RTP frame + public class RTPWriteStream : AudioOutStream + { + private readonly AudioStream _next; + private readonly byte[] _header; + protected readonly byte[] _buffer; + private uint _ssrc; + private ushort _nextSeq; + private uint _nextTimestamp; + private bool _hasHeader; + + public RTPWriteStream(AudioStream next, uint ssrc, int bufferSize = 4000) + { + _next = next; + _ssrc = ssrc; + _buffer = new byte[bufferSize]; + _header = new byte[24]; + _header[0] = 0x80; + _header[1] = 0x78; + _header[8] = (byte)(_ssrc >> 24); + _header[9] = (byte)(_ssrc >> 16); + _header[10] = (byte)(_ssrc >> 8); + _header[11] = (byte)(_ssrc >> 0); + } + + public override void WriteHeader(ushort seq, uint timestamp, bool missed) + { + if (_hasHeader) + throw new InvalidOperationException("Header received with no payload"); + + _hasHeader = true; + _nextSeq = seq; + _nextTimestamp = timestamp; + } + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) + { + cancelToken.ThrowIfCancellationRequested(); + if (!_hasHeader) + throw new InvalidOperationException("Received payload without an RTP header"); + _hasHeader = false; + + unchecked + { + _header[2] = (byte)(_nextSeq >> 8); + _header[3] = (byte)(_nextSeq >> 0); + _header[4] = (byte)(_nextTimestamp >> 24); + _header[5] = (byte)(_nextTimestamp >> 16); + _header[6] = (byte)(_nextTimestamp >> 8); + _header[7] = (byte)(_nextTimestamp >> 0); + } + Buffer.BlockCopy(_header, 0, _buffer, 0, 12); //Copy RTP header from to the buffer + Buffer.BlockCopy(buffer, offset, _buffer, 12, count); + + _next.WriteHeader(_nextSeq, _nextTimestamp, false); + await _next.WriteAsync(_buffer, 0, count + 12).ConfigureAwait(false); + } + + public override async Task FlushAsync(CancellationToken cancelToken) + { + await _next.FlushAsync(cancelToken).ConfigureAwait(false); + } + public override async Task ClearAsync(CancellationToken cancelToken) + { + await _next.ClearAsync(cancelToken).ConfigureAwait(false); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Streams/SodiumDecryptStream.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Streams/SodiumDecryptStream.cs new file mode 100644 index 0000000..2b1a97f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Streams/SodiumDecryptStream.cs @@ -0,0 +1,48 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Audio.Streams +{ + /// + /// Decrypts an RTP frame using libsodium. + /// + public class SodiumDecryptStream : AudioOutStream + { + private readonly AudioClient _client; + private readonly AudioStream _next; + private readonly byte[] _nonce; + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => true; + + public SodiumDecryptStream(AudioStream next, IAudioClient client) + { + _next = next; + _client = (AudioClient)client; + _nonce = new byte[24]; + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) + { + cancelToken.ThrowIfCancellationRequested(); + + if (_client.SecretKey == null) + return; + + Buffer.BlockCopy(buffer, 0, _nonce, 0, 12); //Copy RTP header to nonce + count = SecretBox.Decrypt(buffer, offset + 12, count - 12, buffer, offset + 12, _nonce, _client.SecretKey); + await _next.WriteAsync(buffer, 0, count + 12, cancelToken).ConfigureAwait(false); + } + + public override async Task FlushAsync(CancellationToken cancelToken) + { + await _next.FlushAsync(cancelToken).ConfigureAwait(false); + } + public override async Task ClearAsync(CancellationToken cancelToken) + { + await _next.ClearAsync(cancelToken).ConfigureAwait(false); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs new file mode 100644 index 0000000..8b3f0e3 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs @@ -0,0 +1,64 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Audio.Streams +{ + /// + /// Encrypts an RTP frame using libsodium. + /// + public class SodiumEncryptStream : AudioOutStream + { + private readonly AudioClient _client; + private readonly AudioStream _next; + private readonly byte[] _nonce; + private bool _hasHeader; + private ushort _nextSeq; + private uint _nextTimestamp; + + public SodiumEncryptStream(AudioStream next, IAudioClient client) + { + _next = next; + _client = (AudioClient)client; + _nonce = new byte[24]; + } + + /// Header received with no payload. + public override void WriteHeader(ushort seq, uint timestamp, bool missed) + { + if (_hasHeader) + throw new InvalidOperationException("Header received with no payload."); + + _nextSeq = seq; + _nextTimestamp = timestamp; + _hasHeader = true; + } + /// Received payload without an RTP header. + /// The token has had cancellation requested. + /// The associated has been disposed. + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) + { + cancelToken.ThrowIfCancellationRequested(); + if (!_hasHeader) + throw new InvalidOperationException("Received payload without an RTP header."); + _hasHeader = false; + + if (_client.SecretKey == null) + return; + + Buffer.BlockCopy(buffer, offset, _nonce, 0, 12); //Copy nonce from RTP header + count = SecretBox.Encrypt(buffer, offset + 12, count - 12, buffer, 12, _nonce, _client.SecretKey); + _next.WriteHeader(_nextSeq, _nextTimestamp, false); + await _next.WriteAsync(buffer, 0, count + 12, cancelToken).ConfigureAwait(false); + } + + public override async Task FlushAsync(CancellationToken cancelToken) + { + await _next.FlushAsync(cancelToken).ConfigureAwait(false); + } + public override async Task ClearAsync(CancellationToken cancelToken) + { + await _next.ClearAsync(cancelToken).ConfigureAwait(false); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/BaseSocketClient.Events.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/BaseSocketClient.Events.cs new file mode 100644 index 0000000..908314f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/BaseSocketClient.Events.cs @@ -0,0 +1,371 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Discord.WebSocket +{ + public partial class BaseSocketClient + { + //Channels + /// Fired when a channel is created. + /// + /// + /// This event is fired when a generic channel has been created. The event handler must return a + /// and accept a as its parameter. + /// + /// + /// The newly created channel is passed into the event handler parameter. The given channel type may + /// include, but not limited to, Private Channels (DM, Group), Guild Channels (Text, Voice, Category); + /// see the derived classes of for more details. + /// + /// + /// + /// + /// + public event Func ChannelCreated + { + add { _channelCreatedEvent.Add(value); } + remove { _channelCreatedEvent.Remove(value); } + } + internal readonly AsyncEvent> _channelCreatedEvent = new AsyncEvent>(); + /// Fired when a channel is destroyed. + /// + /// + /// This event is fired when a generic channel has been destroyed. The event handler must return a + /// and accept a as its parameter. + /// + /// + /// The destroyed channel is passed into the event handler parameter. The given channel type may + /// include, but not limited to, Private Channels (DM, Group), Guild Channels (Text, Voice, Category); + /// see the derived classes of for more details. + /// + /// + /// + /// + /// + public event Func ChannelDestroyed { + add { _channelDestroyedEvent.Add(value); } + remove { _channelDestroyedEvent.Remove(value); } + } + internal readonly AsyncEvent> _channelDestroyedEvent = new AsyncEvent>(); + /// Fired when a channel is updated. + /// + /// + /// This event is fired when a generic channel has been destroyed. The event handler must return a + /// and accept 2 as its parameters. + /// + /// + /// The original (prior to update) channel is passed into the first , while + /// the updated channel is passed into the second. The given channel type may include, but not limited + /// to, Private Channels (DM, Group), Guild Channels (Text, Voice, Category); see the derived classes of + /// for more details. + /// + /// + /// + /// + /// + public event Func ChannelUpdated { + add { _channelUpdatedEvent.Add(value); } + remove { _channelUpdatedEvent.Remove(value); } + } + internal readonly AsyncEvent> _channelUpdatedEvent = new AsyncEvent>(); + + //Messages + /// Fired when a message is received. + /// + /// + /// This event is fired when a message is received. The event handler must return a + /// and accept a as its parameter. + /// + /// + /// The message that is sent to the client is passed into the event handler parameter as + /// . This message may be a system message (i.e. + /// ) or a user message (i.e. . See the + /// derived classes of for more details. + /// + /// + /// + /// The example below checks if the newly received message contains the target user. + /// + /// + public event Func MessageReceived { + add { _messageReceivedEvent.Add(value); } + remove { _messageReceivedEvent.Remove(value); } + } + internal readonly AsyncEvent> _messageReceivedEvent = new AsyncEvent>(); + /// Fired when a message is deleted. + /// + /// + /// This event is fired when a message is deleted. The event handler must return a + /// and accept a and + /// as its parameters. + /// + /// + /// + /// It is not possible to retrieve the message via + /// ; the message cannot be retrieved by Discord + /// after the message has been deleted. + /// + /// If caching is enabled via , the + /// entity will contain the deleted message; otherwise, in event + /// that the message cannot be retrieved, the snowflake ID of the message is preserved in the + /// . + /// + /// + /// The source channel of the removed message will be passed into the + /// parameter. + /// + /// + /// + /// + /// + public event Func, ISocketMessageChannel, Task> MessageDeleted { + add { _messageDeletedEvent.Add(value); } + remove { _messageDeletedEvent.Remove(value); } + } + internal readonly AsyncEvent, ISocketMessageChannel, Task>> _messageDeletedEvent = new AsyncEvent, ISocketMessageChannel, Task>>(); + /// Fired when multiple messages are bulk deleted. + /// + /// + /// The event will not be fired for individual messages contained in this event. + /// + /// + /// This event is fired when multiple messages are bulk deleted. The event handler must return a + /// and accept an and + /// as its parameters. + /// + /// + /// + /// It is not possible to retrieve the message via + /// ; the message cannot be retrieved by Discord + /// after the message has been deleted. + /// + /// If caching is enabled via , the + /// entity will contain the deleted message; otherwise, in event + /// that the message cannot be retrieved, the snowflake ID of the message is preserved in the + /// . + /// + /// + /// The source channel of the removed message will be passed into the + /// parameter. + /// + /// + public event Func>, ISocketMessageChannel, Task> MessagesBulkDeleted + { + add { _messagesBulkDeletedEvent.Add(value); } + remove { _messagesBulkDeletedEvent.Remove(value); } + } + internal readonly AsyncEvent>, ISocketMessageChannel, Task>> _messagesBulkDeletedEvent = new AsyncEvent>, ISocketMessageChannel, Task>>(); + /// Fired when a message is updated. + /// + /// + /// This event is fired when a message is updated. The event handler must return a + /// and accept a , , + /// and as its parameters. + /// + /// + /// If caching is enabled via , the + /// entity will contain the original message; otherwise, in event + /// that the message cannot be retrieved, the snowflake ID of the message is preserved in the + /// . + /// + /// + /// The updated message will be passed into the parameter. + /// + /// + /// The source channel of the updated message will be passed into the + /// parameter. + /// + /// + public event Func, SocketMessage, ISocketMessageChannel, Task> MessageUpdated { + add { _messageUpdatedEvent.Add(value); } + remove { _messageUpdatedEvent.Remove(value); } + } + internal readonly AsyncEvent, SocketMessage, ISocketMessageChannel, Task>> _messageUpdatedEvent = new AsyncEvent, SocketMessage, ISocketMessageChannel, Task>>(); + /// Fired when a reaction is added to a message. + /// + /// + /// This event is fired when a reaction is added to a user message. The event handler must return a + /// and accept a , an + /// , and a as its parameter. + /// + /// + /// If caching is enabled via , the + /// entity will contain the original message; otherwise, in event + /// that the message cannot be retrieved, the snowflake ID of the message is preserved in the + /// . + /// + /// + /// The source channel of the reaction addition will be passed into the + /// parameter. + /// + /// + /// The reaction that was added will be passed into the parameter. + /// + /// + /// When fetching the reaction from this event, a user may not be provided under + /// . Please see the documentation of the property for more + /// information. + /// + /// + /// + /// + /// + public event Func, ISocketMessageChannel, SocketReaction, Task> ReactionAdded { + add { _reactionAddedEvent.Add(value); } + remove { _reactionAddedEvent.Remove(value); } + } + internal readonly AsyncEvent, ISocketMessageChannel, SocketReaction, Task>> _reactionAddedEvent = new AsyncEvent, ISocketMessageChannel, SocketReaction, Task>>(); + /// Fired when a reaction is removed from a message. + public event Func, ISocketMessageChannel, SocketReaction, Task> ReactionRemoved { + add { _reactionRemovedEvent.Add(value); } + remove { _reactionRemovedEvent.Remove(value); } + } + internal readonly AsyncEvent, ISocketMessageChannel, SocketReaction, Task>> _reactionRemovedEvent = new AsyncEvent, ISocketMessageChannel, SocketReaction, Task>>(); + /// Fired when all reactions to a message are cleared. + public event Func, ISocketMessageChannel, Task> ReactionsCleared { + add { _reactionsClearedEvent.Add(value); } + remove { _reactionsClearedEvent.Remove(value); } + } + internal readonly AsyncEvent, ISocketMessageChannel, Task>> _reactionsClearedEvent = new AsyncEvent, ISocketMessageChannel, Task>>(); + + //Roles + /// Fired when a role is created. + public event Func RoleCreated { + add { _roleCreatedEvent.Add(value); } + remove { _roleCreatedEvent.Remove(value); } + } + internal readonly AsyncEvent> _roleCreatedEvent = new AsyncEvent>(); + /// Fired when a role is deleted. + public event Func RoleDeleted { + add { _roleDeletedEvent.Add(value); } + remove { _roleDeletedEvent.Remove(value); } + } + internal readonly AsyncEvent> _roleDeletedEvent = new AsyncEvent>(); + /// Fired when a role is updated. + public event Func RoleUpdated { + add { _roleUpdatedEvent.Add(value); } + remove { _roleUpdatedEvent.Remove(value); } + } + internal readonly AsyncEvent> _roleUpdatedEvent = new AsyncEvent>(); + + //Guilds + /// Fired when the connected account joins a guild. + public event Func JoinedGuild { + add { _joinedGuildEvent.Add(value); } + remove { _joinedGuildEvent.Remove(value); } + } + internal readonly AsyncEvent> _joinedGuildEvent = new AsyncEvent>(); + /// Fired when the connected account leaves a guild. + public event Func LeftGuild { + add { _leftGuildEvent.Add(value); } + remove { _leftGuildEvent.Remove(value); } + } + internal readonly AsyncEvent> _leftGuildEvent = new AsyncEvent>(); + /// Fired when a guild becomes available. + public event Func GuildAvailable { + add { _guildAvailableEvent.Add(value); } + remove { _guildAvailableEvent.Remove(value); } + } + internal readonly AsyncEvent> _guildAvailableEvent = new AsyncEvent>(); + /// Fired when a guild becomes unavailable. + public event Func GuildUnavailable { + add { _guildUnavailableEvent.Add(value); } + remove { _guildUnavailableEvent.Remove(value); } + } + internal readonly AsyncEvent> _guildUnavailableEvent = new AsyncEvent>(); + /// Fired when offline guild members are downloaded. + public event Func GuildMembersDownloaded { + add { _guildMembersDownloadedEvent.Add(value); } + remove { _guildMembersDownloadedEvent.Remove(value); } + } + internal readonly AsyncEvent> _guildMembersDownloadedEvent = new AsyncEvent>(); + /// Fired when a guild is updated. + public event Func GuildUpdated { + add { _guildUpdatedEvent.Add(value); } + remove { _guildUpdatedEvent.Remove(value); } + } + internal readonly AsyncEvent> _guildUpdatedEvent = new AsyncEvent>(); + + //Users + /// Fired when a user joins a guild. + public event Func UserJoined { + add { _userJoinedEvent.Add(value); } + remove { _userJoinedEvent.Remove(value); } + } + internal readonly AsyncEvent> _userJoinedEvent = new AsyncEvent>(); + /// Fired when a user leaves a guild. + public event Func UserLeft { + add { _userLeftEvent.Add(value); } + remove { _userLeftEvent.Remove(value); } + } + internal readonly AsyncEvent> _userLeftEvent = new AsyncEvent>(); + /// Fired when a user is banned from a guild. + public event Func UserBanned { + add { _userBannedEvent.Add(value); } + remove { _userBannedEvent.Remove(value); } + } + internal readonly AsyncEvent> _userBannedEvent = new AsyncEvent>(); + /// Fired when a user is unbanned from a guild. + public event Func UserUnbanned { + add { _userUnbannedEvent.Add(value); } + remove { _userUnbannedEvent.Remove(value); } + } + internal readonly AsyncEvent> _userUnbannedEvent = new AsyncEvent>(); + /// Fired when a user is updated. + public event Func UserUpdated { + add { _userUpdatedEvent.Add(value); } + remove { _userUpdatedEvent.Remove(value); } + } + internal readonly AsyncEvent> _userUpdatedEvent = new AsyncEvent>(); + /// Fired when a guild member is updated, or a member presence is updated. + public event Func GuildMemberUpdated { + add { _guildMemberUpdatedEvent.Add(value); } + remove { _guildMemberUpdatedEvent.Remove(value); } + } + internal readonly AsyncEvent> _guildMemberUpdatedEvent = new AsyncEvent>(); + /// Fired when a user joins, leaves, or moves voice channels. + public event Func UserVoiceStateUpdated { + add { _userVoiceStateUpdatedEvent.Add(value); } + remove { _userVoiceStateUpdatedEvent.Remove(value); } + } + internal readonly AsyncEvent> _userVoiceStateUpdatedEvent = new AsyncEvent>(); + /// Fired when the bot connects to a Discord voice server. + public event Func VoiceServerUpdated + { + add { _voiceServerUpdatedEvent.Add(value); } + remove { _voiceServerUpdatedEvent.Remove(value); } + } + internal readonly AsyncEvent> _voiceServerUpdatedEvent = new AsyncEvent>(); + /// Fired when the connected account is updated. + public event Func CurrentUserUpdated { + add { _selfUpdatedEvent.Add(value); } + remove { _selfUpdatedEvent.Remove(value); } + } + internal readonly AsyncEvent> _selfUpdatedEvent = new AsyncEvent>(); + /// Fired when a user starts typing. + public event Func UserIsTyping { + add { _userIsTypingEvent.Add(value); } + remove { _userIsTypingEvent.Remove(value); } + } + internal readonly AsyncEvent> _userIsTypingEvent = new AsyncEvent>(); + /// Fired when a user joins a group channel. + public event Func RecipientAdded { + add { _recipientAddedEvent.Add(value); } + remove { _recipientAddedEvent.Remove(value); } + } + internal readonly AsyncEvent> _recipientAddedEvent = new AsyncEvent>(); + /// Fired when a user is removed from a group channel. + public event Func RecipientRemoved { + add { _recipientRemovedEvent.Add(value); } + remove { _recipientRemovedEvent.Remove(value); } + } + internal readonly AsyncEvent> _recipientRemovedEvent = new AsyncEvent>(); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/BaseSocketClient.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/BaseSocketClient.cs new file mode 100644 index 0000000..548bb75 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/BaseSocketClient.cs @@ -0,0 +1,305 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Discord.API; +using Discord.Rest; + +namespace Discord.WebSocket +{ + /// + /// Represents the base of a WebSocket-based Discord client. + /// + public abstract partial class BaseSocketClient : BaseDiscordClient, IDiscordClient + { + protected readonly DiscordSocketConfig BaseConfig; + + /// + /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. + /// + /// + /// An that represents the round-trip latency to the WebSocket server. Please + /// note that this value does not represent a "true" latency for operations such as sending a message. + /// + public abstract int Latency { get; protected set; } + /// + /// Gets the status for the logged-in user. + /// + /// + /// A status object that represents the user's online presence status. + /// + public abstract UserStatus Status { get; protected set; } + /// + /// Gets the activity for the logged-in user. + /// + /// + /// An activity object that represents the user's current activity. + /// + public abstract IActivity Activity { get; protected set; } + + /// + /// Provides access to a REST-only client with a shared state from this client. + /// + public abstract DiscordSocketRestClient Rest { get; } + + internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; + + /// + /// Gets the current logged-in user. + /// + public new SocketSelfUser CurrentUser { get => base.CurrentUser as SocketSelfUser; protected set => base.CurrentUser = value; } + /// + /// Gets a collection of guilds that the user is currently in. + /// + /// + /// A read-only collection of guilds that the current user is in. + /// + public abstract IReadOnlyCollection Guilds { get; } + /// + /// Gets a collection of private channels opened in this session. + /// + /// + /// This method will retrieve all private channels (including direct-message, group channel and such) that + /// are currently opened in this session. + /// + /// This method will not return previously opened private channels outside of the current session! If + /// you have just started the client, this may return an empty collection. + /// + /// + /// + /// A read-only collection of private channels that the user currently partakes in. + /// + public abstract IReadOnlyCollection PrivateChannels { get; } + /// + /// Gets a collection of available voice regions. + /// + /// + /// A read-only collection of voice regions that the user has access to. + /// + public abstract IReadOnlyCollection VoiceRegions { get; } + + internal BaseSocketClient(DiscordSocketConfig config, DiscordRestApiClient client) + : base(config, client) => BaseConfig = config; + private static DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config) + => new DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent, + rateLimitPrecision: config.RateLimitPrecision, + useSystemClock: config.UseSystemClock); + + /// + /// Gets a Discord application information for the logged-in user. + /// + /// + /// This method reflects your application information you submitted when creating a Discord application via + /// the Developer Portal. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the application + /// information. + /// + public abstract Task GetApplicationInfoAsync(RequestOptions options = null); + /// + /// Gets a generic user. + /// + /// The user snowflake ID. + /// + /// This method gets the user present in the WebSocket cache with the given condition. + /// + /// Sometimes a user may return null due to Discord not sending offline users in large guilds + /// (i.e. guild with 100+ members) actively. To download users on startup and to see more information + /// about this subject, see . + /// + /// + /// This method does not attempt to fetch users that the logged-in user does not have access to (i.e. + /// users who don't share mutual guild(s) with the current user). If you wish to get a user that you do + /// not have access to, consider using the REST implementation of + /// . + /// + /// + /// + /// A generic WebSocket-based user; null when the user cannot be found. + /// + public abstract SocketUser GetUser(ulong id); + + /// + /// Gets a user. + /// + /// + /// This method gets the user present in the WebSocket cache with the given condition. + /// + /// Sometimes a user may return null due to Discord not sending offline users in large guilds + /// (i.e. guild with 100+ members) actively. To download users on startup and to see more information + /// about this subject, see . + /// + /// + /// This method does not attempt to fetch users that the logged-in user does not have access to (i.e. + /// users who don't share mutual guild(s) with the current user). If you wish to get a user that you do + /// not have access to, consider using the REST implementation of + /// . + /// + /// + /// The name of the user. + /// The discriminator value of the user. + /// + /// A generic WebSocket-based user; null when the user cannot be found. + /// + public abstract SocketUser GetUser(string username, string discriminator); + /// + /// Gets a channel. + /// + /// The snowflake identifier of the channel (e.g. `381889909113225237`). + /// + /// A generic WebSocket-based channel object (voice, text, category, etc.) associated with the identifier; + /// null when the channel cannot be found. + /// + public abstract SocketChannel GetChannel(ulong id); + /// + /// Gets a guild. + /// + /// The guild snowflake identifier. + /// + /// A WebSocket-based guild associated with the snowflake identifier; null when the guild cannot be + /// found. + /// + public abstract SocketGuild GetGuild(ulong id); + /// + /// Gets a voice region. + /// + /// The identifier of the voice region (e.g. eu-central ). + /// + /// A REST-based voice region associated with the identifier; null if the voice region is not + /// found. + /// + public abstract RestVoiceRegion GetVoiceRegion(string id); + /// + public abstract Task StartAsync(); + /// + public abstract Task StopAsync(); + /// + /// Sets the current status of the user (e.g. Online, Do not Disturb). + /// + /// The new status to be set. + /// + /// A task that represents the asynchronous set operation. + /// + public abstract Task SetStatusAsync(UserStatus status); + /// + /// Sets the game of the user. + /// + /// The name of the game. + /// If streaming, the URL of the stream. Must be a valid Twitch URL. + /// The type of the game. + /// + /// A task that represents the asynchronous set operation. + /// + public abstract Task SetGameAsync(string name, string streamUrl = null, ActivityType type = ActivityType.Playing); + /// + /// Sets the of the logged-in user. + /// + /// + /// This method sets the of the user. + /// + /// Discord will only accept setting of name and the type of activity. + /// + /// + /// Rich Presence cannot be set via this method or client. Rich Presence is strictly limited to RPC + /// clients only. + /// + /// + /// The activity to be set. + /// + /// A task that represents the asynchronous set operation. + /// + public abstract Task SetActivityAsync(IActivity activity); + /// + /// Attempts to download users into the user cache for the selected guilds. + /// + /// The guilds to download the members from. + /// + /// A task that represents the asynchronous download operation. + /// + public abstract Task DownloadUsersAsync(IEnumerable guilds); + + /// + /// Creates a guild for the logged-in user who is in less than 10 active guilds. + /// + /// + /// This method creates a new guild on behalf of the logged-in user. + /// + /// Due to Discord's limitation, this method will only work for users that are in less than 10 guilds. + /// + /// + /// The name of the new guild. + /// The voice region to create the guild with. + /// The icon of the guild. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the created guild. + /// + public Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null, RequestOptions options = null) + => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon, options ?? RequestOptions.Default); + /// + /// Gets the connections that the user has set up. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of connections. + /// + public Task> GetConnectionsAsync(RequestOptions options = null) + => ClientHelper.GetConnectionsAsync(this, options ?? RequestOptions.Default); + /// + /// Gets an invite. + /// + /// The invitation identifier. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the invite information. + /// + public Task GetInviteAsync(string inviteId, RequestOptions options = null) + => ClientHelper.GetInviteAsync(this, inviteId, options ?? RequestOptions.Default); + + // IDiscordClient + /// + async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) + => await GetApplicationInfoAsync(options).ConfigureAwait(false); + + /// + Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetChannel(id)); + /// + Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode, RequestOptions options) + => Task.FromResult>(PrivateChannels); + + /// + async Task> IDiscordClient.GetConnectionsAsync(RequestOptions options) + => await GetConnectionsAsync(options).ConfigureAwait(false); + + /// + async Task IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options) + => await GetInviteAsync(inviteId, options).ConfigureAwait(false); + + /// + Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetGuild(id)); + /// + Task> IDiscordClient.GetGuildsAsync(CacheMode mode, RequestOptions options) + => Task.FromResult>(Guilds); + + /// + async Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) + => await CreateGuildAsync(name, region, jpegIcon, options).ConfigureAwait(false); + + /// + Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetUser(id)); + /// + Task IDiscordClient.GetUserAsync(string username, string discriminator, RequestOptions options) + => Task.FromResult(GetUser(username, discriminator)); + + /// + Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) + => Task.FromResult(GetVoiceRegion(id)); + /// + Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) + => Task.FromResult>(VoiceRegions); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/ClientState.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/ClientState.cs new file mode 100644 index 0000000..dad185d --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/ClientState.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace Discord.WebSocket +{ + internal class ClientState + { + 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 _dmChannels; + private readonly ConcurrentDictionary _guilds; + private readonly ConcurrentDictionary _users; + private readonly ConcurrentHashSet _groupChannels; + + 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(); + internal IReadOnlyCollection Users => _users.ToReadOnlyCollection(); + + internal IReadOnlyCollection PrivateChannels => + _dmChannels.Select(x => x.Value as ISocketPrivateChannel).Concat( + _groupChannels.Select(x => GetChannel(x) as ISocketPrivateChannel)) + .ToReadOnlyCollection(() => _dmChannels.Count + _groupChannels.Count); + + public ClientState(int guildCount, int dmChannelCount) + { + double estimatedChannelCount = guildCount * AverageChannelsPerGuild + dmChannelCount; + double estimatedUsersCount = guildCount * AverageUsersPerGuild; + _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 SocketChannel GetChannel(ulong id) + { + if (_channels.TryGetValue(id, out SocketChannel channel)) + return channel; + return null; + } + internal SocketDMChannel GetDMChannel(ulong userId) + { + if (_dmChannels.TryGetValue(userId, out SocketDMChannel channel)) + return channel; + return null; + } + internal void AddChannel(SocketChannel channel) + { + _channels[channel.Id] = channel; + + switch (channel) + { + case SocketDMChannel dmChannel: + _dmChannels[dmChannel.Recipient.Id] = dmChannel; + break; + case SocketGroupChannel groupChannel: + _groupChannels.TryAdd(groupChannel.Id); + break; + } + } + internal SocketChannel RemoveChannel(ulong id) + { + if (_channels.TryRemove(id, out SocketChannel channel)) + { + switch (channel) + { + case SocketDMChannel dmChannel: + _dmChannels.TryRemove(dmChannel.Recipient.Id, out _); + break; + case SocketGroupChannel _: + _groupChannels.TryRemove(id); + break; + } + return channel; + } + return null; + } + + internal SocketGuild GetGuild(ulong id) + { + if (_guilds.TryGetValue(id, out SocketGuild guild)) + return guild; + return null; + } + internal void AddGuild(SocketGuild guild) + { + _guilds[guild.Id] = guild; + } + internal SocketGuild RemoveGuild(ulong id) + { + if (_guilds.TryRemove(id, out SocketGuild guild)) + return guild; + return null; + } + + internal SocketGlobalUser GetUser(ulong id) + { + if (_users.TryGetValue(id, out SocketGlobalUser user)) + return user; + return null; + } + internal SocketGlobalUser GetOrAddUser(ulong id, Func userFactory) + { + return _users.GetOrAdd(id, userFactory); + } + internal SocketGlobalUser RemoveUser(ulong id) + { + if (_users.TryRemove(id, out SocketGlobalUser user)) + return user; + return null; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Commands/ShardedCommandContext.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Commands/ShardedCommandContext.cs new file mode 100644 index 0000000..f970c32 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Commands/ShardedCommandContext.cs @@ -0,0 +1,25 @@ +using Discord.WebSocket; + +namespace Discord.Commands +{ + /// The sharded variant of , which may contain the client, user, guild, channel, and message. + public class ShardedCommandContext : SocketCommandContext, ICommandContext + { + /// Gets the that the command is executed with. + public new DiscordShardedClient Client { get; } + + public ShardedCommandContext(DiscordShardedClient client, SocketUserMessage msg) + : base(client.GetShard(GetShardId(client, (msg.Channel as SocketGuildChannel)?.Guild)), msg) + { + Client = client; + } + + /// Gets the shard ID of the command context. + private static int GetShardId(DiscordShardedClient client, IGuild guild) + => guild == null ? 0 : client.GetShardIdFor(guild); + + //ICommandContext + /// + IDiscordClient ICommandContext.Client => Client; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Commands/SocketCommandContext.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Commands/SocketCommandContext.cs new file mode 100644 index 0000000..f4d5179 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Commands/SocketCommandContext.cs @@ -0,0 +1,62 @@ +using Discord.WebSocket; + +namespace Discord.Commands +{ + /// + /// Represents a WebSocket-based context of a command. This may include the client, guild, channel, user, and message. + /// + public class SocketCommandContext : ICommandContext + { + /// + /// Gets the that the command is executed with. + /// + public DiscordSocketClient Client { get; } + /// + /// Gets the that the command is executed in. + /// + public SocketGuild Guild { get; } + /// + /// Gets the that the command is executed in. + /// + public ISocketMessageChannel Channel { get; } + /// + /// Gets the who executed the command. + /// + public SocketUser User { get; } + /// + /// Gets the that the command is interpreted from. + /// + public SocketUserMessage Message { get; } + + /// + /// Indicates whether the channel that the command is executed in is a private channel. + /// + public bool IsPrivate => Channel is IPrivateChannel; + + /// + /// Initializes a new class with the provided client and message. + /// + /// The underlying client. + /// The underlying message. + public SocketCommandContext(DiscordSocketClient client, SocketUserMessage msg) + { + Client = client; + Guild = (msg.Channel as SocketGuildChannel)?.Guild; + Channel = msg.Channel; + User = msg.Author; + Message = msg; + } + + //ICommandContext + /// + IDiscordClient ICommandContext.Client => Client; + /// + IGuild ICommandContext.Guild => Guild; + /// + IMessageChannel ICommandContext.Channel => Channel; + /// + IUser ICommandContext.User => User; + /// + IUserMessage ICommandContext.Message => Message; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/ConnectionManager.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/ConnectionManager.cs new file mode 100644 index 0000000..8c9c743 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/ConnectionManager.cs @@ -0,0 +1,233 @@ +using Discord.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; +using Discord.Net; + +namespace Discord +{ + internal class ConnectionManager : IDisposable + { + 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>(); + + private readonly SemaphoreSlim _stateLock; + private readonly Logger _logger; + private readonly int _connectionTimeout; + private readonly Func _onConnecting; + private readonly Func _onDisconnecting; + + private TaskCompletionSource _connectionPromise, _readyPromise; + private CancellationTokenSource _combinedCancelToken, _reconnectCancelToken, _connectionCancelToken; + private Task _task; + + private bool _isDisposed; + + public ConnectionState State { get; private set; } + public CancellationToken CancelToken { get; private set; } + + internal ConnectionManager(SemaphoreSlim stateLock, Logger logger, int connectionTimeout, + Func onConnecting, Func onDisconnecting, Action> clientDisconnectHandler) + { + _stateLock = stateLock; + _logger = logger; + _connectionTimeout = connectionTimeout; + _onConnecting = onConnecting; + _onDisconnecting = onDisconnecting; + + clientDisconnectHandler(ex => + { + if (ex != null) + { + var ex2 = ex as WebSocketClosedException; + if (ex2?.CloseCode == 4006) + CriticalError(new Exception("WebSocket session expired", ex)); + else + Error(new Exception("WebSocket connection was closed", ex)); + } + else + Error(new Exception("WebSocket connection was closed")); + return Task.Delay(0); + }); + } + + public virtual async Task StartAsync() + { + await AcquireConnectionLock().ConfigureAwait(false); + var reconnectCancelToken = new CancellationTokenSource(); + _reconnectCancelToken?.Dispose(); + _reconnectCancelToken = reconnectCancelToken; + _task = Task.Run(async () => + { + try + { + Random jitter = new Random(); + int nextReconnectDelay = 1000; + while (!reconnectCancelToken.IsCancellationRequested) + { + try + { + await ConnectAsync(reconnectCancelToken).ConfigureAwait(false); + nextReconnectDelay = 1000; //Reset delay + await _connectionPromise.Task.ConfigureAwait(false); + } + catch (OperationCanceledException ex) + { + Cancel(); //In case this exception didn't come from another Error call + await DisconnectAsync(ex, !reconnectCancelToken.IsCancellationRequested).ConfigureAwait(false); + } + catch (Exception ex) + { + Error(ex); //In case this exception didn't come from another Error call + if (!reconnectCancelToken.IsCancellationRequested) + { + await _logger.WarningAsync(ex).ConfigureAwait(false); + await DisconnectAsync(ex, true).ConfigureAwait(false); + } + else + { + await _logger.ErrorAsync(ex).ConfigureAwait(false); + await DisconnectAsync(ex, false).ConfigureAwait(false); + } + } + + if (!reconnectCancelToken.IsCancellationRequested) + { + //Wait before reconnecting + await Task.Delay(nextReconnectDelay, reconnectCancelToken.Token).ConfigureAwait(false); + nextReconnectDelay = (nextReconnectDelay * 2) + jitter.Next(-250, 250); + if (nextReconnectDelay > 60000) + nextReconnectDelay = 60000; + } + } + } + finally { _stateLock.Release(); } + }); + } + public virtual Task StopAsync() + { + Cancel(); + return Task.CompletedTask; + } + + private async Task ConnectAsync(CancellationTokenSource reconnectCancelToken) + { + _connectionCancelToken?.Dispose(); + _combinedCancelToken?.Dispose(); + _connectionCancelToken = new CancellationTokenSource(); + _combinedCancelToken = CancellationTokenSource.CreateLinkedTokenSource(_connectionCancelToken.Token, reconnectCancelToken.Token); + CancelToken = _combinedCancelToken.Token; + + _connectionPromise = new TaskCompletionSource(); + State = ConnectionState.Connecting; + await _logger.InfoAsync("Connecting").ConfigureAwait(false); + + try + { + var readyPromise = new TaskCompletionSource(); + _readyPromise = readyPromise; + + //Abort connection on timeout + var cancelToken = CancelToken; + var _ = Task.Run(async () => + { + try + { + await Task.Delay(_connectionTimeout, cancelToken).ConfigureAwait(false); + readyPromise.TrySetException(new TimeoutException()); + } + catch (OperationCanceledException) { } + }); + + await _onConnecting().ConfigureAwait(false); + + await _logger.InfoAsync("Connected").ConfigureAwait(false); + State = ConnectionState.Connected; + await _logger.DebugAsync("Raising Event").ConfigureAwait(false); + await _connectedEvent.InvokeAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + Error(ex); + throw; + } + } + private async Task DisconnectAsync(Exception ex, bool isReconnecting) + { + if (State == ConnectionState.Disconnected) return; + State = ConnectionState.Disconnecting; + await _logger.InfoAsync("Disconnecting").ConfigureAwait(false); + + await _onDisconnecting(ex).ConfigureAwait(false); + + await _disconnectedEvent.InvokeAsync(ex, isReconnecting).ConfigureAwait(false); + State = ConnectionState.Disconnected; + await _logger.InfoAsync("Disconnected").ConfigureAwait(false); + } + + public async Task CompleteAsync() + { + await _readyPromise.TrySetResultAsync(true).ConfigureAwait(false); + } + public async Task WaitAsync() + { + await _readyPromise.Task.ConfigureAwait(false); + } + + public void Cancel() + { + _readyPromise?.TrySetCanceled(); + _connectionPromise?.TrySetCanceled(); + _reconnectCancelToken?.Cancel(); + _connectionCancelToken?.Cancel(); + } + public void Error(Exception ex) + { + _readyPromise.TrySetException(ex); + _connectionPromise.TrySetException(ex); + _connectionCancelToken?.Cancel(); + } + public void CriticalError(Exception ex) + { + _reconnectCancelToken?.Cancel(); + Error(ex); + } + public void Reconnect() + { + _readyPromise.TrySetCanceled(); + _connectionPromise.TrySetCanceled(); + _connectionCancelToken?.Cancel(); + } + private async Task AcquireConnectionLock() + { + while (true) + { + await StopAsync().ConfigureAwait(false); + if (await _stateLock.WaitAsync(0).ConfigureAwait(false)) + break; + } + } + + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + _combinedCancelToken?.Dispose(); + _reconnectCancelToken?.Dispose(); + _connectionCancelToken?.Dispose(); + } + + _isDisposed = true; + } + } + + public void Dispose() + { + Dispose(true); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj new file mode 100644 index 0000000..26a2490 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj @@ -0,0 +1,15 @@ + + + + Discord.Net.WebSocket + Discord.WebSocket + A core Discord.Net library containing the WebSocket client and models. + net461;netstandard2.0;netstandard2.1 + netstandard2.0;netstandard2.1 + true + + + + + + diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/DiscordShardedClient.Events.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/DiscordShardedClient.Events.cs new file mode 100644 index 0000000..c9e6796 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/DiscordShardedClient.Events.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.WebSocket +{ + public partial class DiscordShardedClient + { + //General + /// Fired when a shard is connected to the Discord gateway. + public event Func ShardConnected + { + add { _shardConnectedEvent.Add(value); } + remove { _shardConnectedEvent.Remove(value); } + } + private readonly AsyncEvent> _shardConnectedEvent = new AsyncEvent>(); + /// Fired when a shard is disconnected from the Discord gateway. + public event Func ShardDisconnected + { + add { _shardDisconnectedEvent.Add(value); } + remove { _shardDisconnectedEvent.Remove(value); } + } + private readonly AsyncEvent> _shardDisconnectedEvent = new AsyncEvent>(); + /// Fired when a guild data for a shard has finished downloading. + public event Func ShardReady + { + add { _shardReadyEvent.Add(value); } + remove { _shardReadyEvent.Remove(value); } + } + private readonly AsyncEvent> _shardReadyEvent = new AsyncEvent>(); + /// Fired when a shard receives a heartbeat from the Discord gateway. + public event Func ShardLatencyUpdated + { + add { _shardLatencyUpdatedEvent.Add(value); } + remove { _shardLatencyUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _shardLatencyUpdatedEvent = new AsyncEvent>(); + } +} \ No newline at end of file diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/DiscordShardedClient.cs new file mode 100644 index 0000000..0877abf --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -0,0 +1,403 @@ +using Discord.API; +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Threading; + +namespace Discord.WebSocket +{ + public partial class DiscordShardedClient : BaseSocketClient, IDiscordClient + { + private readonly DiscordSocketConfig _baseConfig; + private readonly SemaphoreSlim _connectionGroupLock; + private readonly Dictionary _shardIdsToIndex; + private readonly bool _automaticShards; + private int[] _shardIds; + private DiscordSocketClient[] _shards; + private int _totalShards; + + private bool _isDisposed; + + /// + public override int Latency { get => GetLatency(); protected set { } } + /// + public override UserStatus Status { get => _shards[0].Status; protected set { } } + /// + public override IActivity Activity { get => _shards[0].Activity; protected set { } } + + internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; + /// + public override IReadOnlyCollection Guilds => GetGuilds().ToReadOnlyCollection(GetGuildCount); + /// + public override IReadOnlyCollection PrivateChannels => GetPrivateChannels().ToReadOnlyCollection(GetPrivateChannelCount); + public IReadOnlyCollection Shards => _shards; + /// + public override IReadOnlyCollection VoiceRegions => _shards[0].VoiceRegions; + + /// + /// Provides access to a REST-only client with a shared state from this client. + /// + public override DiscordSocketRestClient Rest => _shards[0].Rest; + + /// Creates a new REST/WebSocket Discord client. + public DiscordShardedClient() : this(null, new DiscordSocketConfig()) { } + /// Creates a new REST/WebSocket Discord client. +#pragma warning disable IDISP004 + public DiscordShardedClient(DiscordSocketConfig config) : this(null, config, CreateApiClient(config)) { } +#pragma warning restore IDISP004 + /// Creates a new REST/WebSocket Discord client. + public DiscordShardedClient(int[] ids) : this(ids, new DiscordSocketConfig()) { } + /// Creates a new REST/WebSocket Discord client. +#pragma warning disable IDISP004 + public DiscordShardedClient(int[] ids, DiscordSocketConfig config) : this(ids, config, CreateApiClient(config)) { } +#pragma warning restore IDISP004 + private DiscordShardedClient(int[] ids, DiscordSocketConfig config, API.DiscordSocketApiClient client) + : base(config, client) + { + if (config.ShardId != null) + throw new ArgumentException($"{nameof(config.ShardId)} must not be set."); + if (ids != null && config.TotalShards == null) + throw new ArgumentException($"Custom ids are not supported when {nameof(config.TotalShards)} is not specified."); + + _shardIdsToIndex = new Dictionary(); + config.DisplayInitialLog = false; + _baseConfig = config; + _connectionGroupLock = new SemaphoreSlim(1, 1); + + if (config.TotalShards == null) + _automaticShards = true; + else + { + _totalShards = config.TotalShards.Value; + _shardIds = ids ?? Enumerable.Range(0, _totalShards).ToArray(); + _shards = new DiscordSocketClient[_shardIds.Length]; + for (int i = 0; i < _shardIds.Length; i++) + { + _shardIdsToIndex.Add(_shardIds[i], i); + var newConfig = config.Clone(); + newConfig.ShardId = _shardIds[i]; + _shards[i] = new DiscordSocketClient(newConfig, _connectionGroupLock, i != 0 ? _shards[0] : null); + RegisterEvents(_shards[i], i == 0); + } + } + } + private static API.DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config) + => new API.DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent, + rateLimitPrecision: config.RateLimitPrecision); + + internal override async Task OnLoginAsync(TokenType tokenType, string token) + { + if (_automaticShards) + { + var shardCount = await GetRecommendedShardCountAsync().ConfigureAwait(false); + _shardIds = Enumerable.Range(0, shardCount).ToArray(); + _totalShards = _shardIds.Length; + _shards = new DiscordSocketClient[_shardIds.Length]; + for (int i = 0; i < _shardIds.Length; i++) + { + _shardIdsToIndex.Add(_shardIds[i], i); + var newConfig = _baseConfig.Clone(); + newConfig.ShardId = _shardIds[i]; + newConfig.TotalShards = _totalShards; + _shards[i] = new DiscordSocketClient(newConfig, _connectionGroupLock, i != 0 ? _shards[0] : null); + RegisterEvents(_shards[i], i == 0); + } + } + + //Assume thread safe: already in a connection lock + for (int i = 0; i < _shards.Length; i++) + await _shards[i].LoginAsync(tokenType, token); + } + internal override async Task OnLogoutAsync() + { + //Assume thread safe: already in a connection lock + if (_shards != null) + { + for (int i = 0; i < _shards.Length; i++) + await _shards[i].LogoutAsync(); + } + + CurrentUser = null; + if (_automaticShards) + { + _shardIds = new int[0]; + _shardIdsToIndex.Clear(); + _totalShards = 0; + _shards = null; + } + } + + /// + public override async Task StartAsync() + => await Task.WhenAll(_shards.Select(x => x.StartAsync())).ConfigureAwait(false); + /// + public override async Task StopAsync() + => await Task.WhenAll(_shards.Select(x => x.StopAsync())).ConfigureAwait(false); + + public DiscordSocketClient GetShard(int id) + { + if (_shardIdsToIndex.TryGetValue(id, out id)) + return _shards[id]; + return null; + } + private int GetShardIdFor(ulong guildId) + => (int)((guildId >> 22) % (uint)_totalShards); + public int GetShardIdFor(IGuild guild) + => GetShardIdFor(guild?.Id ?? 0); + private DiscordSocketClient GetShardFor(ulong guildId) + => GetShard(GetShardIdFor(guildId)); + public DiscordSocketClient GetShardFor(IGuild guild) + => GetShardFor(guild?.Id ?? 0); + + /// + public override async Task GetApplicationInfoAsync(RequestOptions options = null) + => await _shards[0].GetApplicationInfoAsync(options).ConfigureAwait(false); + + /// + public override SocketGuild GetGuild(ulong id) + => GetShardFor(id).GetGuild(id); + + /// + public override SocketChannel GetChannel(ulong id) + { + for (int i = 0; i < _shards.Length; i++) + { + var channel = _shards[i].GetChannel(id); + if (channel != null) + return channel; + } + return null; + } + private IEnumerable GetPrivateChannels() + { + for (int i = 0; i < _shards.Length; i++) + { + foreach (var channel in _shards[i].PrivateChannels) + yield return channel; + } + } + private int GetPrivateChannelCount() + { + int result = 0; + for (int i = 0; i < _shards.Length; i++) + result += _shards[i].PrivateChannels.Count; + return result; + } + + private IEnumerable GetGuilds() + { + for (int i = 0; i < _shards.Length; i++) + { + foreach (var guild in _shards[i].Guilds) + yield return guild; + } + } + private int GetGuildCount() + { + int result = 0; + for (int i = 0; i < _shards.Length; i++) + result += _shards[i].Guilds.Count; + return result; + } + + /// + public override SocketUser GetUser(ulong id) + { + for (int i = 0; i < _shards.Length; i++) + { + var user = _shards[i].GetUser(id); + if (user != null) + return user; + } + return null; + } + /// + public override SocketUser GetUser(string username, string discriminator) + { + for (int i = 0; i < _shards.Length; i++) + { + var user = _shards[i].GetUser(username, discriminator); + if (user != null) + return user; + } + return null; + } + + /// + public override RestVoiceRegion GetVoiceRegion(string id) + => _shards[0].GetVoiceRegion(id); + + /// + /// is + public override async Task DownloadUsersAsync(IEnumerable guilds) + { + if (guilds == null) throw new ArgumentNullException(nameof(guilds)); + for (int i = 0; i < _shards.Length; i++) + { + int id = _shardIds[i]; + var arr = guilds.Where(x => GetShardIdFor(x) == id).ToArray(); + if (arr.Length > 0) + await _shards[i].DownloadUsersAsync(arr).ConfigureAwait(false); + } + } + + private int GetLatency() + { + int total = 0; + for (int i = 0; i < _shards.Length; i++) + total += _shards[i].Latency; + return (int)Math.Round(total / (double)_shards.Length); + } + + /// + public override async Task SetStatusAsync(UserStatus status) + { + for (int i = 0; i < _shards.Length; i++) + await _shards[i].SetStatusAsync(status).ConfigureAwait(false); + } + /// + public override async Task SetGameAsync(string name, string streamUrl = null, ActivityType type = ActivityType.Playing) + { + IActivity activity = null; + if (!string.IsNullOrEmpty(streamUrl)) + activity = new StreamingGame(name, streamUrl); + else if (!string.IsNullOrEmpty(name)) + activity = new Game(name, type); + await SetActivityAsync(activity).ConfigureAwait(false); + } + /// + public override async Task SetActivityAsync(IActivity activity) + { + for (int i = 0; i < _shards.Length; i++) + await _shards[i].SetActivityAsync(activity).ConfigureAwait(false); + } + + private void RegisterEvents(DiscordSocketClient client, bool isPrimary) + { + client.Log += (msg) => _logEvent.InvokeAsync(msg); + client.LoggedOut += () => + { + var state = LoginState; + if (state == LoginState.LoggedIn || state == LoginState.LoggingIn) + { + //Should only happen if token is changed + var _ = LogoutAsync(); //Signal the logout, fire and forget + } + return Task.Delay(0); + }; + if (isPrimary) + { + client.Ready += () => + { + CurrentUser = client.CurrentUser; + return Task.Delay(0); + }; + } + + client.Connected += () => _shardConnectedEvent.InvokeAsync(client); + client.Disconnected += (exception) => _shardDisconnectedEvent.InvokeAsync(exception, client); + client.Ready += () => _shardReadyEvent.InvokeAsync(client); + client.LatencyUpdated += (oldLatency, newLatency) => _shardLatencyUpdatedEvent.InvokeAsync(oldLatency, newLatency, client); + + client.ChannelCreated += (channel) => _channelCreatedEvent.InvokeAsync(channel); + client.ChannelDestroyed += (channel) => _channelDestroyedEvent.InvokeAsync(channel); + client.ChannelUpdated += (oldChannel, newChannel) => _channelUpdatedEvent.InvokeAsync(oldChannel, newChannel); + + client.MessageReceived += (msg) => _messageReceivedEvent.InvokeAsync(msg); + client.MessageDeleted += (cache, channel) => _messageDeletedEvent.InvokeAsync(cache, channel); + client.MessagesBulkDeleted += (cache, channel) => _messagesBulkDeletedEvent.InvokeAsync(cache, channel); + client.MessageUpdated += (oldMsg, newMsg, channel) => _messageUpdatedEvent.InvokeAsync(oldMsg, newMsg, channel); + client.ReactionAdded += (cache, channel, reaction) => _reactionAddedEvent.InvokeAsync(cache, channel, reaction); + client.ReactionRemoved += (cache, channel, reaction) => _reactionRemovedEvent.InvokeAsync(cache, channel, reaction); + client.ReactionsCleared += (cache, channel) => _reactionsClearedEvent.InvokeAsync(cache, channel); + + client.RoleCreated += (role) => _roleCreatedEvent.InvokeAsync(role); + client.RoleDeleted += (role) => _roleDeletedEvent.InvokeAsync(role); + client.RoleUpdated += (oldRole, newRole) => _roleUpdatedEvent.InvokeAsync(oldRole, newRole); + + client.JoinedGuild += (guild) => _joinedGuildEvent.InvokeAsync(guild); + client.LeftGuild += (guild) => _leftGuildEvent.InvokeAsync(guild); + client.GuildAvailable += (guild) => _guildAvailableEvent.InvokeAsync(guild); + client.GuildUnavailable += (guild) => _guildUnavailableEvent.InvokeAsync(guild); + client.GuildMembersDownloaded += (guild) => _guildMembersDownloadedEvent.InvokeAsync(guild); + client.GuildUpdated += (oldGuild, newGuild) => _guildUpdatedEvent.InvokeAsync(oldGuild, newGuild); + + client.UserJoined += (user) => _userJoinedEvent.InvokeAsync(user); + client.UserLeft += (user) => _userLeftEvent.InvokeAsync(user); + client.UserBanned += (user, guild) => _userBannedEvent.InvokeAsync(user, guild); + client.UserUnbanned += (user, guild) => _userUnbannedEvent.InvokeAsync(user, guild); + client.UserUpdated += (oldUser, newUser) => _userUpdatedEvent.InvokeAsync(oldUser, newUser); + client.GuildMemberUpdated += (oldUser, newUser) => _guildMemberUpdatedEvent.InvokeAsync(oldUser, newUser); + client.UserVoiceStateUpdated += (user, oldVoiceState, newVoiceState) => _userVoiceStateUpdatedEvent.InvokeAsync(user, oldVoiceState, newVoiceState); + client.VoiceServerUpdated += (server) => _voiceServerUpdatedEvent.InvokeAsync(server); + client.CurrentUserUpdated += (oldUser, newUser) => _selfUpdatedEvent.InvokeAsync(oldUser, newUser); + client.UserIsTyping += (oldUser, newUser) => _userIsTypingEvent.InvokeAsync(oldUser, newUser); + client.RecipientAdded += (user) => _recipientAddedEvent.InvokeAsync(user); + client.RecipientRemoved += (user) => _recipientRemovedEvent.InvokeAsync(user); + } + + //IDiscordClient + /// + async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) + => await GetApplicationInfoAsync().ConfigureAwait(false); + + /// + Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetChannel(id)); + /// + Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode, RequestOptions options) + => Task.FromResult>(PrivateChannels); + + /// + async Task> IDiscordClient.GetConnectionsAsync(RequestOptions options) + => await GetConnectionsAsync().ConfigureAwait(false); + + /// + async Task IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options) + => await GetInviteAsync(inviteId, options).ConfigureAwait(false); + + /// + Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetGuild(id)); + /// + Task> IDiscordClient.GetGuildsAsync(CacheMode mode, RequestOptions options) + => Task.FromResult>(Guilds); + /// + async Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) + => await CreateGuildAsync(name, region, jpegIcon).ConfigureAwait(false); + + /// + Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetUser(id)); + /// + Task IDiscordClient.GetUserAsync(string username, string discriminator, RequestOptions options) + => Task.FromResult(GetUser(username, discriminator)); + + /// + Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) + => Task.FromResult>(VoiceRegions); + /// + Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) + => Task.FromResult(GetVoiceRegion(id)); + + internal override void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + foreach (var client in _shards) + client?.Dispose(); + _connectionGroupLock?.Dispose(); + } + + _isDisposed = true; + } + + base.Dispose(disposing); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/DiscordSocketApiClient.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/DiscordSocketApiClient.cs new file mode 100644 index 0000000..9313f07 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/DiscordSocketApiClient.cs @@ -0,0 +1,288 @@ +#pragma warning disable CS1591 +using Discord.API.Gateway; +using Discord.Net.Queue; +using Discord.Net.Rest; +using Discord.Net.WebSockets; +using Discord.WebSocket; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class DiscordSocketApiClient : DiscordRestApiClient + { + public event Func SentGatewayMessage { add { _sentGatewayMessageEvent.Add(value); } remove { _sentGatewayMessageEvent.Remove(value); } } + private readonly AsyncEvent> _sentGatewayMessageEvent = new AsyncEvent>(); + public event Func ReceivedGatewayEvent { add { _receivedGatewayEvent.Add(value); } remove { _receivedGatewayEvent.Remove(value); } } + private readonly AsyncEvent> _receivedGatewayEvent = new AsyncEvent>(); + + public event Func Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } + private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); + + private readonly bool _isExplicitUrl; + private CancellationTokenSource _connectCancelToken; + private string _gatewayUrl; + + //Store our decompression streams for zlib shared state + private MemoryStream _compressed; + private DeflateStream _decompressor; + + internal IWebSocketClient WebSocketClient { get; } + + public ConnectionState ConnectionState { get; private set; } + + public DiscordSocketApiClient(RestClientProvider restClientProvider, WebSocketProvider webSocketProvider, string userAgent, + string url = null, RetryMode defaultRetryMode = RetryMode.AlwaysRetry, JsonSerializer serializer = null, + RateLimitPrecision rateLimitPrecision = RateLimitPrecision.Second, + bool useSystemClock = true) + : base(restClientProvider, userAgent, defaultRetryMode, serializer, rateLimitPrecision, useSystemClock) + { + _gatewayUrl = url; + if (url != null) + _isExplicitUrl = true; + WebSocketClient = webSocketProvider(); + //WebSocketClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .NET Framework 4.6+) + + WebSocketClient.BinaryMessage += async (data, index, count) => + { + using (var decompressed = new MemoryStream()) + { + if (data[0] == 0x78) + { + //Strip the zlib header + _compressed.Write(data, index + 2, count - 2); + _compressed.SetLength(count - 2); + } + else + { + _compressed.Write(data, index, count); + _compressed.SetLength(count); + } + + //Reset positions so we don't run out of memory + _compressed.Position = 0; + _decompressor.CopyTo(decompressed); + _compressed.Position = 0; + decompressed.Position = 0; + + using (var reader = new StreamReader(decompressed)) + using (var jsonReader = new JsonTextReader(reader)) + { + var msg = _serializer.Deserialize(jsonReader); + if (msg != null) + await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); + } + } + }; + WebSocketClient.TextMessage += async text => + { + using (var reader = new StringReader(text)) + using (var jsonReader = new JsonTextReader(reader)) + { + var msg = _serializer.Deserialize(jsonReader); + if (msg != null) + await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); + } + }; + WebSocketClient.Closed += async ex => + { + await DisconnectAsync().ConfigureAwait(false); + await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); + }; + } + + internal override void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + _connectCancelToken?.Dispose(); + (WebSocketClient as IDisposable)?.Dispose(); + _decompressor?.Dispose(); + _compressed?.Dispose(); + } + _isDisposed = true; + } + + base.Dispose(disposing); + } + + public async Task ConnectAsync() + { + await _stateLock.WaitAsync().ConfigureAwait(false); + try + { + await ConnectInternalAsync().ConfigureAwait(false); + } + finally { _stateLock.Release(); } + } + /// The client must be logged in before connecting. + /// This client is not configured with WebSocket support. + internal override async Task ConnectInternalAsync() + { + if (LoginState != LoginState.LoggedIn) + throw new InvalidOperationException("The client must be logged in before connecting."); + if (WebSocketClient == null) + throw new NotSupportedException("This client is not configured with WebSocket support."); + + //Re-create streams to reset the zlib state + _compressed?.Dispose(); + _decompressor?.Dispose(); + _compressed = new MemoryStream(); + _decompressor = new DeflateStream(_compressed, CompressionMode.Decompress); + + ConnectionState = ConnectionState.Connecting; + try + { + _connectCancelToken?.Dispose(); + _connectCancelToken = new CancellationTokenSource(); + if (WebSocketClient != null) + WebSocketClient.SetCancelToken(_connectCancelToken.Token); + + if (!_isExplicitUrl) + { + var gatewayResponse = await GetGatewayAsync().ConfigureAwait(false); + _gatewayUrl = $"{gatewayResponse.Url}?v={DiscordConfig.APIVersion}&encoding={DiscordSocketConfig.GatewayEncoding}&compress=zlib-stream"; + } + await WebSocketClient.ConnectAsync(_gatewayUrl).ConfigureAwait(false); + + ConnectionState = ConnectionState.Connected; + } + catch + { + if (!_isExplicitUrl) + _gatewayUrl = null; //Uncache in case the gateway url changed + await DisconnectInternalAsync().ConfigureAwait(false); + throw; + } + } + + public async Task DisconnectAsync() + { + await _stateLock.WaitAsync().ConfigureAwait(false); + try + { + await DisconnectInternalAsync().ConfigureAwait(false); + } + finally { _stateLock.Release(); } + } + public async Task DisconnectAsync(Exception ex) + { + await _stateLock.WaitAsync().ConfigureAwait(false); + try + { + await DisconnectInternalAsync().ConfigureAwait(false); + } + finally { _stateLock.Release(); } + } + /// This client is not configured with WebSocket support. + internal override async Task DisconnectInternalAsync() + { + if (WebSocketClient == null) + throw new NotSupportedException("This client is not configured with WebSocket support."); + + if (ConnectionState == ConnectionState.Disconnected) return; + ConnectionState = ConnectionState.Disconnecting; + + try { _connectCancelToken?.Cancel(false); } + catch { } + + await WebSocketClient.DisconnectAsync().ConfigureAwait(false); + + ConnectionState = ConnectionState.Disconnected; + } + + //Core + 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 SocketFrame { Operation = (int)opCode, Payload = payload }; + if (payload != null) + bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); + await RequestQueue.SendAsync(new WebSocketRequest(WebSocketClient, null, bytes, true, options)).ConfigureAwait(false); + await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false); + } + + public async Task SendIdentifyAsync(int largeThreshold = 100, int shardID = 0, int totalShards = 1, bool guildSubscriptions = true, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + var props = new Dictionary + { + ["$device"] = "Discord.Net" + }; + var msg = new IdentifyParams() + { + Token = AuthToken, + Properties = props, + LargeThreshold = largeThreshold, + GuildSubscriptions = guildSubscriptions + }; + if (totalShards > 1) + msg.ShardingParams = new int[] { shardID, totalShards }; + + await SendGatewayAsync(GatewayOpCode.Identify, msg, options: options).ConfigureAwait(false); + } + public async Task SendResumeAsync(string sessionId, int lastSeq, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + var msg = new ResumeParams() + { + Token = AuthToken, + SessionId = sessionId, + Sequence = lastSeq + }; + await SendGatewayAsync(GatewayOpCode.Resume, msg, options: options).ConfigureAwait(false); + } + public async Task SendHeartbeatAsync(int lastSeq, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + await SendGatewayAsync(GatewayOpCode.Heartbeat, lastSeq, options: options).ConfigureAwait(false); + } + public async Task SendStatusUpdateAsync(UserStatus status, bool isAFK, long? since, Game game, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + var args = new StatusUpdateParams + { + 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, + ChannelId = channelId, + SelfDeaf = selfDeaf, + SelfMute = selfMute + }; + await SendGatewayAsync(GatewayOpCode.VoiceStateUpdate, payload, options: options).ConfigureAwait(false); + } + 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/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/DiscordSocketClient.Events.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/DiscordSocketClient.Events.cs new file mode 100644 index 0000000..51dea5f --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/DiscordSocketClient.Events.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading.Tasks; +using Discord.API; + +namespace Discord.WebSocket +{ + public partial class DiscordSocketClient + { + //General + /// Fired when connected to the Discord gateway. + public event Func Connected + { + add { _connectedEvent.Add(value); } + remove { _connectedEvent.Remove(value); } + } + private readonly AsyncEvent> _connectedEvent = new AsyncEvent>(); + /// Fired when disconnected to the Discord gateway. + public event Func Disconnected + { + add { _disconnectedEvent.Add(value); } + remove { _disconnectedEvent.Remove(value); } + } + private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); + /// Fired when guild data has finished downloading. + public event Func Ready + { + add { _readyEvent.Add(value); } + remove { _readyEvent.Remove(value); } + } + private readonly AsyncEvent> _readyEvent = new AsyncEvent>(); + /// Fired when a heartbeat is received from the Discord gateway. + public event Func LatencyUpdated + { + add { _latencyUpdatedEvent.Add(value); } + remove { _latencyUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _latencyUpdatedEvent = new AsyncEvent>(); + + internal DiscordSocketClient(DiscordSocketConfig config, DiscordRestApiClient client) : base(config, client) + { + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/DiscordSocketClient.cs new file mode 100644 index 0000000..ed142d0 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -0,0 +1,2002 @@ +using Discord.API; +using Discord.API.Gateway; +using Discord.Logging; +using Discord.Net.Converters; +using Discord.Net.Udp; +using Discord.Net.WebSockets; +using Discord.Rest; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +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; +using GameModel = Discord.API.Game; + +namespace Discord.WebSocket +{ + /// + /// Represents a WebSocket-based Discord client. + /// + public partial class DiscordSocketClient : BaseSocketClient, IDiscordClient + { + private readonly ConcurrentQueue _largeGuilds; + private readonly JsonSerializer _serializer; + private readonly SemaphoreSlim _connectionGroupLock; + private readonly DiscordSocketClient _parentClient; + private readonly ConcurrentQueue _heartbeatTimes; + private readonly ConnectionManager _connection; + private readonly Logger _gatewayLogger; + private readonly SemaphoreSlim _stateLock; + + private string _sessionId; + private int _lastSeq; + private ImmutableDictionary _voiceRegions; + private Task _heartbeatTask, _guildDownloadTask; + private int _unavailableGuildCount; + private long _lastGuildAvailableTime, _lastMessageTime; + private int _nextAudioId; + private DateTimeOffset? _statusSince; + private RestApplication _applicationInfo; + private bool _isDisposed; + private bool _guildSubscriptions; + + /// + /// Provides access to a REST-only client with a shared state from this client. + /// + public override DiscordSocketRestClient Rest { get; } + /// Gets the shard of of this client. + public int ShardId { get; } + /// Gets the current connection state of this client. + public ConnectionState ConnectionState => _connection.State; + /// + public override int Latency { get; protected set; } + /// + public override UserStatus Status { get; protected set; } = UserStatus.Online; + /// + public override IActivity Activity { get; protected set; } + + //From DiscordSocketConfig + internal int TotalShards { get; private set; } + internal int MessageCacheSize { get; private set; } + internal int LargeThreshold { get; private set; } + internal ClientState State { get; private set; } + internal UdpSocketProvider UdpSocketProvider { get; private set; } + internal WebSocketProvider WebSocketProvider { get; private set; } + internal bool AlwaysDownloadUsers { get; private set; } + internal int? HandlerTimeout { get; private set; } + internal bool? ExclusiveBulkDelete { get; private set; } + + internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; + /// + public override IReadOnlyCollection Guilds => State.Guilds; + /// + public override IReadOnlyCollection PrivateChannels => State.PrivateChannels; + /// + /// Gets a collection of direct message channels opened in this session. + /// + /// + /// This method returns a collection of currently opened direct message channels. + /// + /// This method will not return previously opened DM channels outside of the current session! If you + /// have just started the client, this may return an empty collection. + /// + /// + /// + /// A collection of DM channels that have been opened in this session. + /// + public IReadOnlyCollection DMChannels + => State.PrivateChannels.OfType().ToImmutableArray(); + /// + /// Gets a collection of group channels opened in this session. + /// + /// + /// This method returns a collection of currently opened group channels. + /// + /// This method will not return previously opened group channels outside of the current session! If you + /// have just started the client, this may return an empty collection. + /// + /// + /// + /// A collection of group channels that have been opened in this session. + /// + public IReadOnlyCollection GroupChannels + => State.PrivateChannels.OfType().ToImmutableArray(); + /// + public override IReadOnlyCollection VoiceRegions => _voiceRegions.ToReadOnlyCollection(); + + /// + /// Initializes a new REST/WebSocket-based Discord client. + /// + public DiscordSocketClient() : this(new DiscordSocketConfig()) { } + /// + /// Initializes a new REST/WebSocket-based Discord client with the provided configuration. + /// + /// The configuration to be used with the client. +#pragma warning disable IDISP004 + public DiscordSocketClient(DiscordSocketConfig config) : this(config, CreateApiClient(config), null, null) { } + internal DiscordSocketClient(DiscordSocketConfig config, SemaphoreSlim groupLock, DiscordSocketClient parentClient) : this(config, CreateApiClient(config), groupLock, parentClient) { } +#pragma warning restore IDISP004 + private DiscordSocketClient(DiscordSocketConfig config, API.DiscordSocketApiClient client, SemaphoreSlim groupLock, DiscordSocketClient parentClient) + : base(config, client) + { + ShardId = config.ShardId ?? 0; + TotalShards = config.TotalShards ?? 1; + MessageCacheSize = config.MessageCacheSize; + LargeThreshold = config.LargeThreshold; + UdpSocketProvider = config.UdpSocketProvider; + WebSocketProvider = config.WebSocketProvider; + AlwaysDownloadUsers = config.AlwaysDownloadUsers; + HandlerTimeout = config.HandlerTimeout; + ExclusiveBulkDelete = config.ExclusiveBulkDelete; + State = new ClientState(0, 0); + Rest = new DiscordSocketRestClient(config, ApiClient); + _heartbeatTimes = new ConcurrentQueue(); + _guildSubscriptions = config.GuildSubscriptions; + + _stateLock = new SemaphoreSlim(1, 1); + _gatewayLogger = LogManager.CreateLogger(ShardId == 0 && TotalShards == 1 ? "Gateway" : $"Shard #{ShardId}"); + _connection = new ConnectionManager(_stateLock, _gatewayLogger, config.ConnectionTimeout, + OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x); + _connection.Connected += () => TimedInvokeAsync(_connectedEvent, nameof(Connected)); + _connection.Disconnected += (ex, recon) => TimedInvokeAsync(_disconnectedEvent, nameof(Disconnected), ex); + + _nextAudioId = 1; + _connectionGroupLock = groupLock; + _parentClient = parentClient; + + _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; + _serializer.Error += (s, e) => + { + _gatewayLogger.WarningAsync("Serializer Error", e.ErrorContext.Error).GetAwaiter().GetResult(); + e.ErrorContext.Handled = true; + }; + + ApiClient.SentGatewayMessage += async opCode => await _gatewayLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false); + ApiClient.ReceivedGatewayEvent += ProcessMessageAsync; + + LeftGuild += async g => await _gatewayLogger.InfoAsync($"Left {g.Name}").ConfigureAwait(false); + JoinedGuild += async g => await _gatewayLogger.InfoAsync($"Joined {g.Name}").ConfigureAwait(false); + GuildAvailable += async g => await _gatewayLogger.VerboseAsync($"Connected to {g.Name}").ConfigureAwait(false); + GuildUnavailable += async g => await _gatewayLogger.VerboseAsync($"Disconnected from {g.Name}").ConfigureAwait(false); + LatencyUpdated += async (old, val) => await _gatewayLogger.DebugAsync($"Latency = {val} ms").ConfigureAwait(false); + + GuildAvailable += g => + { + if (ConnectionState == ConnectionState.Connected && AlwaysDownloadUsers && !g.HasAllMembers) + { + var _ = g.DownloadUsersAsync(); + } + return Task.Delay(0); + }; + + _voiceRegions = ImmutableDictionary.Create(); + _largeGuilds = new ConcurrentQueue(); + } + private static API.DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config) + => new API.DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent, config.GatewayHost, + rateLimitPrecision: config.RateLimitPrecision); + /// + internal override void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + StopAsync().GetAwaiter().GetResult(); + ApiClient?.Dispose(); + _stateLock?.Dispose(); + } + _isDisposed = true; + } + + base.Dispose(disposing); + } + + /// + internal override async Task OnLoginAsync(TokenType tokenType, string token) + { + if (_parentClient == null) + { + var voiceRegions = await ApiClient.GetVoiceRegionsAsync(new RequestOptions { IgnoreState = true, RetryMode = RetryMode.AlwaysRetry }).ConfigureAwait(false); + _voiceRegions = voiceRegions.Select(x => RestVoiceRegion.Create(this, x)).ToImmutableDictionary(x => x.Id); + } + else + _voiceRegions = _parentClient._voiceRegions; + await Rest.OnLoginAsync(tokenType, token); + } + /// + internal override async Task OnLogoutAsync() + { + await StopAsync().ConfigureAwait(false); + _applicationInfo = null; + _voiceRegions = ImmutableDictionary.Create(); + await Rest.OnLogoutAsync(); + } + + /// + public override async Task StartAsync() + => await _connection.StartAsync().ConfigureAwait(false); + /// + public override async Task StopAsync() + => await _connection.StopAsync().ConfigureAwait(false); + + private async Task OnConnectingAsync() + { + if (_connectionGroupLock != null) + await _connectionGroupLock.WaitAsync(_connection.CancelToken).ConfigureAwait(false); + try + { + await _gatewayLogger.DebugAsync("Connecting ApiClient").ConfigureAwait(false); + await ApiClient.ConnectAsync().ConfigureAwait(false); + + if (_sessionId != null) + { + await _gatewayLogger.DebugAsync("Resuming").ConfigureAwait(false); + await ApiClient.SendResumeAsync(_sessionId, _lastSeq).ConfigureAwait(false); + } + else + { + await _gatewayLogger.DebugAsync("Identifying").ConfigureAwait(false); + await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards, guildSubscriptions: _guildSubscriptions).ConfigureAwait(false); + } + + //Wait for READY + await _connection.WaitAsync().ConfigureAwait(false); + + await _gatewayLogger.DebugAsync("Sending Status").ConfigureAwait(false); + await SendStatusAsync().ConfigureAwait(false); + } + finally + { + if (_connectionGroupLock != null) + { + await Task.Delay(5000).ConfigureAwait(false); + _connectionGroupLock.Release(); + } + } + } + private async Task OnDisconnectingAsync(Exception ex) + { + + await _gatewayLogger.DebugAsync("Disconnecting ApiClient").ConfigureAwait(false); + await ApiClient.DisconnectAsync().ConfigureAwait(false); + + //Wait for tasks to complete + await _gatewayLogger.DebugAsync("Waiting for heartbeater").ConfigureAwait(false); + var heartbeatTask = _heartbeatTask; + if (heartbeatTask != null) + await heartbeatTask.ConfigureAwait(false); + _heartbeatTask = null; + + while (_heartbeatTimes.TryDequeue(out _)) { } + _lastMessageTime = 0; + + 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("Clearing large guild queue").ConfigureAwait(false); + while (_largeGuilds.TryDequeue(out _)) { } + + //Raise virtual GUILD_UNAVAILABLEs + await _gatewayLogger.DebugAsync("Raising virtual GuildUnavailables").ConfigureAwait(false); + foreach (var guild in State.Guilds) + { + if (guild.IsAvailable) + await GuildUnavailableAsync(guild).ConfigureAwait(false); + } + } + + /// + public override async Task GetApplicationInfoAsync(RequestOptions options = null) + => _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this, options ?? RequestOptions.Default).ConfigureAwait(false)); + + /// + public override SocketGuild GetGuild(ulong id) + => State.GetGuild(id); + + /// + public override SocketChannel GetChannel(ulong id) + => State.GetChannel(id); + + /// + public override SocketUser GetUser(ulong id) + => State.GetUser(id); + /// + public override SocketUser GetUser(string username, string discriminator) + => State.Users.FirstOrDefault(x => x.Discriminator == discriminator && x.Username == username); + internal SocketGlobalUser GetOrCreateUser(ClientState state, Discord.API.User model) + { + return state.GetOrAddUser(model.Id, x => + { + var user = SocketGlobalUser.Create(this, state, model); + user.GlobalUser.AddRef(); + return user; + }); + } + internal SocketGlobalUser GetOrCreateSelfUser(ClientState state, Discord.API.User model) + { + return state.GetOrAddUser(model.Id, x => + { + var user = SocketGlobalUser.Create(this, state, model); + user.GlobalUser.AddRef(); + user.Presence = new SocketPresence(UserStatus.Online, null, null); + return user; + }); + } + internal void RemoveUser(ulong id) + => State.RemoveUser(id); + + /// + public override RestVoiceRegion GetVoiceRegion(string id) + { + if (_voiceRegions.TryGetValue(id, out RestVoiceRegion region)) + return region; + return null; + } + + /// + public override async Task DownloadUsersAsync(IEnumerable guilds) + { + if (ConnectionState == ConnectionState.Connected) + { + //Race condition leads to guilds being requested twice, probably okay + await ProcessUserDownloadsAsync(guilds.Select(x => GetGuild(x.Id)).Where(x => x != null)).ConfigureAwait(false); + } + } + private async Task ProcessUserDownloadsAsync(IEnumerable guilds) + { + var cachedGuilds = guilds.ToImmutableArray(); + + const short batchSize = 50; + ulong[] batchIds = new ulong[Math.Min(batchSize, cachedGuilds.Length)]; + Task[] batchTasks = new Task[batchIds.Length]; + int batchCount = (cachedGuilds.Length + (batchSize - 1)) / batchSize; + + for (int i = 0, k = 0; i < batchCount; i++) + { + bool isLast = i == batchCount - 1; + int count = isLast ? (batchIds.Length - (batchCount - 1) * batchSize) : batchSize; + + for (int j = 0; j < count; j++, k++) + { + var guild = cachedGuilds[k]; + batchIds[j] = guild.Id; + batchTasks[j] = guild.DownloaderPromise; + } + + await ApiClient.SendRequestMembersAsync(batchIds).ConfigureAwait(false); + + if (isLast && batchCount > 1) + await Task.WhenAll(batchTasks.Take(count)).ConfigureAwait(false); + else + await Task.WhenAll(batchTasks).ConfigureAwait(false); + } + } + + /// + /// + /// The following example sets the status of the current user to Do Not Disturb. + /// + /// await client.SetStatusAsync(UserStatus.DoNotDisturb); + /// + /// + public override async Task SetStatusAsync(UserStatus status) + { + Status = status; + if (status == UserStatus.AFK) + _statusSince = DateTimeOffset.UtcNow; + else + _statusSince = null; + await SendStatusAsync().ConfigureAwait(false); + } + /// + /// + /// + /// The following example sets the activity of the current user to the specified game name. + /// + /// await client.SetGameAsync("A Strange Game"); + /// + /// + /// + /// The following example sets the activity of the current user to a streaming status. + /// + /// await client.SetGameAsync("Great Stream 10/10", "https://twitch.tv/MyAmazingStream1337", ActivityType.Streaming); + /// + /// + /// + public override async Task SetGameAsync(string name, string streamUrl = null, ActivityType type = ActivityType.Playing) + { + if (!string.IsNullOrEmpty(streamUrl)) + Activity = new StreamingGame(name, streamUrl); + else if (!string.IsNullOrEmpty(name)) + Activity = new Game(name, type); + else + Activity = null; + await SendStatusAsync().ConfigureAwait(false); + } + /// + public override async Task SetActivityAsync(IActivity activity) + { + Activity = activity; + await SendStatusAsync().ConfigureAwait(false); + } + + private async Task SendStatusAsync() + { + if (CurrentUser == null) + return; + var status = Status; + var statusSince = _statusSince; + CurrentUser.Presence = new SocketPresence(status, Activity, null); + + var gameModel = new GameModel(); + // Discord only accepts rich presence over RPC, don't even bother building a payload + if (Activity is RichGame) + throw new NotSupportedException("Outgoing Rich Presences are not supported via WebSocket."); + + if (Activity != null) + { + gameModel.Name = Activity.Name; + gameModel.Type = Activity.Type; + if (Activity is StreamingGame streamGame) + gameModel.StreamUrl = streamGame.Url; + } + + await ApiClient.SendStatusUpdateAsync( + status, + status == UserStatus.AFK, + statusSince != null ? _statusSince.Value.ToUnixTimeMilliseconds() : (long?)null, + gameModel).ConfigureAwait(false); + } + + private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string type, object payload) + { + if (seq != null) + _lastSeq = seq.Value; + _lastMessageTime = Environment.TickCount; + + try + { + switch (opCode) + { + case GatewayOpCode.Hello: + { + await _gatewayLogger.DebugAsync("Received Hello").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); + + _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _connection.CancelToken); + } + break; + case GatewayOpCode.Heartbeat: + { + await _gatewayLogger.DebugAsync("Received Heartbeat").ConfigureAwait(false); + + await ApiClient.SendHeartbeatAsync(_lastSeq).ConfigureAwait(false); + } + break; + case GatewayOpCode.HeartbeatAck: + { + await _gatewayLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false); + + if (_heartbeatTimes.TryDequeue(out long time)) + { + int latency = (int)(Environment.TickCount - time); + int before = Latency; + Latency = latency; + + await TimedInvokeAsync(_latencyUpdatedEvent, nameof(LatencyUpdated), before, latency).ConfigureAwait(false); + } + } + break; + case GatewayOpCode.InvalidSession: + { + await _gatewayLogger.DebugAsync("Received InvalidSession").ConfigureAwait(false); + await _gatewayLogger.WarningAsync("Failed to resume previous session").ConfigureAwait(false); + + _sessionId = null; + _lastSeq = 0; + + await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards).ConfigureAwait(false); + } + break; + case GatewayOpCode.Reconnect: + { + await _gatewayLogger.DebugAsync("Received Reconnect").ConfigureAwait(false); + _connection.Error(new Exception("Server requested a reconnect")); + } + break; + case GatewayOpCode.Dispatch: + switch (type) + { + //Connection + case "READY": + { + try + { + await _gatewayLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var state = new ClientState(data.Guilds.Length, data.PrivateChannels.Length); + + var currentUser = SocketSelfUser.Create(this, state, data.User); + ApiClient.CurrentUserId = currentUser.Id; + int unavailableGuilds = 0; + for (int i = 0; i < data.Guilds.Length; i++) + { + var model = data.Guilds[i]; + var guild = AddGuild(model, state); + if (!guild.IsAvailable) + unavailableGuilds++; + else + await GuildAvailableAsync(guild).ConfigureAwait(false); + } + for (int i = 0; i < data.PrivateChannels.Length; i++) + AddPrivateChannel(data.PrivateChannels[i], state); + + _sessionId = data.SessionId; + _unavailableGuildCount = unavailableGuilds; + CurrentUser = currentUser; + State = state; + } + catch (Exception ex) + { + _connection.CriticalError(new Exception("Processing READY failed", ex)); + return; + } + + _lastGuildAvailableTime = Environment.TickCount; + _guildDownloadTask = WaitForGuildsAsync(_connection.CancelToken, _gatewayLogger) + .ContinueWith(async x => + { + if (x.IsFaulted) + { + _connection.Error(x.Exception); + return; + } + else if (_connection.CancelToken.IsCancellationRequested) + return; + + await TimedInvokeAsync(_readyEvent, nameof(Ready)).ConfigureAwait(false); + await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false); + }); + _ = _connection.CompleteAsync(); + } + break; + case "RESUMED": + { + await _gatewayLogger.DebugAsync("Received Dispatch (RESUMED)").ConfigureAwait(false); + + _ = _connection.CompleteAsync(); + + //Notify the client that these guilds are available again + foreach (var guild in State.Guilds) + { + if (guild.IsAvailable) + await GuildAvailableAsync(guild).ConfigureAwait(false); + } + + await _gatewayLogger.InfoAsync("Resumed previous session").ConfigureAwait(false); + } + break; + + //Guilds + case "GUILD_CREATE": + { + var data = (payload as JToken).ToObject(_serializer); + + if (data.Unavailable == false) + { + type = "GUILD_AVAILABLE"; + _lastGuildAvailableTime = Environment.TickCount; + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_AVAILABLE)").ConfigureAwait(false); + + var guild = State.GetGuild(data.Id); + if (guild != null) + { + guild.Update(State, data); + + if (_unavailableGuildCount != 0) + _unavailableGuildCount--; + await GuildAvailableAsync(guild).ConfigureAwait(false); + + if (guild.DownloadedMemberCount >= guild.MemberCount && !guild.DownloaderPromise.IsCompleted) + { + guild.CompleteDownloadUsers(); + await TimedInvokeAsync(_guildMembersDownloadedEvent, nameof(GuildMembersDownloaded), guild).ConfigureAwait(false); + } + } + else + { + await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); + return; + } + } + else + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_CREATE)").ConfigureAwait(false); + + var guild = AddGuild(data, State); + if (guild != null) + { + await TimedInvokeAsync(_joinedGuildEvent, nameof(JoinedGuild), guild).ConfigureAwait(false); + } + else + { + await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); + return; + } + } + } + break; + case "GUILD_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.Id); + if (guild != null) + { + var before = guild.Clone(); + guild.Update(State, data); + await TimedInvokeAsync(_guildUpdatedEvent, nameof(GuildUpdated), before, guild).ConfigureAwait(false); + } + else + { + await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); + return; + } + } + break; + case "GUILD_EMOJIS_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_EMOJIS_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); + if (guild != null) + { + var before = guild.Clone(); + guild.Update(State, data); + await TimedInvokeAsync(_guildUpdatedEvent, nameof(GuildUpdated), before, guild).ConfigureAwait(false); + } + else + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + } + break; + case "GUILD_SYNC": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_SYNC)").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.Id); + if (guild != null) + { + var before = guild.Clone(); + guild.Update(State, data); + //This is treated as an extension of GUILD_AVAILABLE + _unavailableGuildCount--; + _lastGuildAvailableTime = Environment.TickCount; + await GuildAvailableAsync(guild).ConfigureAwait(false); + await TimedInvokeAsync(_guildUpdatedEvent, nameof(GuildUpdated), before, guild).ConfigureAwait(false); + } + else + { + await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); + return; + } + } + break; + case "GUILD_DELETE": + { + var data = (payload as JToken).ToObject(_serializer); + if (data.Unavailable == true) + { + type = "GUILD_UNAVAILABLE"; + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_UNAVAILABLE)").ConfigureAwait(false); + + var guild = State.GetGuild(data.Id); + if (guild != null) + { + await GuildUnavailableAsync(guild).ConfigureAwait(false); + _unavailableGuildCount++; + } + else + { + await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); + return; + } + } + else + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_DELETE)").ConfigureAwait(false); + + var guild = RemoveGuild(data.Id); + if (guild != null) + { + await GuildUnavailableAsync(guild).ConfigureAwait(false); + await TimedInvokeAsync(_leftGuildEvent, nameof(LeftGuild), guild).ConfigureAwait(false); + (guild as IDisposable).Dispose(); + } + else + { + await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); + return; + } + } + } + break; + + //Channels + case "CHANNEL_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_CREATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + SocketChannel channel = null; + if (data.GuildId.IsSpecified) + { + var guild = State.GetGuild(data.GuildId.Value); + if (guild != null) + { + channel = guild.AddChannel(State, data); + + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + } + else + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } + } + else + { + channel = State.GetChannel(data.Id); + if (channel != null) + return; //Discord may send duplicate CHANNEL_CREATEs for DMs + channel = AddPrivateChannel(data, State) as SocketChannel; + } + + if (channel != null) + await TimedInvokeAsync(_channelCreatedEvent, nameof(ChannelCreated), channel).ConfigureAwait(false); + } + break; + case "CHANNEL_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = State.GetChannel(data.Id); + if (channel != null) + { + var before = channel.Clone(); + channel.Update(State, data); + + var guild = (channel as SocketGuildChannel)?.Guild; + if (!(guild?.IsSynced ?? true)) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + await TimedInvokeAsync(_channelUpdatedEvent, nameof(ChannelUpdated), before, channel).ConfigureAwait(false); + } + else + { + await UnknownChannelAsync(type, data.Id).ConfigureAwait(false); + return; + } + } + break; + case "CHANNEL_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_DELETE)").ConfigureAwait(false); + + SocketChannel channel = null; + var data = (payload as JToken).ToObject(_serializer); + if (data.GuildId.IsSpecified) + { + var guild = State.GetGuild(data.GuildId.Value); + if (guild != null) + { + channel = guild.RemoveChannel(State, data.Id); + + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + } + else + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } + } + else + channel = RemovePrivateChannel(data.Id) as SocketChannel; + + if (channel != null) + await TimedInvokeAsync(_channelDestroyedEvent, nameof(ChannelDestroyed), channel).ConfigureAwait(false); + else + { + await UnknownChannelAsync(type, data.Id, data.GuildId.GetValueOrDefault(0)).ConfigureAwait(false); + return; + } + } + break; + + //Members + case "GUILD_MEMBER_ADD": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_ADD)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); + if (guild != null) + { + var user = guild.AddOrUpdateUser(data); + guild.MemberCount++; + + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + await TimedInvokeAsync(_userJoinedEvent, nameof(UserJoined), user).ConfigureAwait(false); + } + else + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + } + break; + case "GUILD_MEMBER_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); + if (guild != null) + { + var user = guild.GetUser(data.User.Id); + + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + if (user != null) + { + var before = user.Clone(); + user.Update(State, data); + await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), before, user).ConfigureAwait(false); + } + else + { + if (!guild.HasAllMembers) + await IncompleteGuildUserAsync(type, data.User.Id, data.GuildId).ConfigureAwait(false); + else + await UnknownGuildUserAsync(type, data.User.Id, data.GuildId).ConfigureAwait(false); + return; + } + } + else + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + } + break; + case "GUILD_MEMBER_REMOVE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_REMOVE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); + if (guild != null) + { + var user = guild.RemoveUser(data.User.Id); + guild.MemberCount--; + + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + if (user != null) + await TimedInvokeAsync(_userLeftEvent, nameof(UserLeft), user).ConfigureAwait(false); + else + { + if (!guild.HasAllMembers) + await IncompleteGuildUserAsync(type, data.User.Id, data.GuildId).ConfigureAwait(false); + else + await UnknownGuildUserAsync(type, data.User.Id, data.GuildId).ConfigureAwait(false); + return; + } + } + else + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + } + break; + case "GUILD_MEMBERS_CHUNK": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBERS_CHUNK)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); + if (guild != null) + { + foreach (var memberModel in data.Members) + guild.AddOrUpdateUser(memberModel); + + if (guild.DownloadedMemberCount >= guild.MemberCount && !guild.DownloaderPromise.IsCompleted) + { + guild.CompleteDownloadUsers(); + await TimedInvokeAsync(_guildMembersDownloadedEvent, nameof(GuildMembersDownloaded), guild).ConfigureAwait(false); + } + } + else + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + } + break; + case "CHANNEL_RECIPIENT_ADD": + { + await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_RECIPIENT_ADD)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + if (State.GetChannel(data.ChannelId) is SocketGroupChannel channel) + { + var user = channel.GetOrAddUser(data.User); + await TimedInvokeAsync(_recipientAddedEvent, nameof(RecipientAdded), user).ConfigureAwait(false); + } + else + { + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + return; + } + } + break; + case "CHANNEL_RECIPIENT_REMOVE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_RECIPIENT_REMOVE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + if (State.GetChannel(data.ChannelId) is SocketGroupChannel channel) + { + var user = channel.RemoveUser(data.User.Id); + if (user != null) + await TimedInvokeAsync(_recipientRemovedEvent, nameof(RecipientRemoved), user).ConfigureAwait(false); + else + { + await UnknownChannelUserAsync(type, data.User.Id, data.ChannelId).ConfigureAwait(false); + return; + } + } + else + { + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + return; + } + } + break; + + //Roles + case "GUILD_ROLE_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_CREATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); + if (guild != null) + { + var role = guild.AddRole(data.Role); + + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + await TimedInvokeAsync(_roleCreatedEvent, nameof(RoleCreated), role).ConfigureAwait(false); + } + else + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + } + break; + case "GUILD_ROLE_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + 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(State, data.Role); + + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + await TimedInvokeAsync(_roleUpdatedEvent, nameof(RoleUpdated), before, role).ConfigureAwait(false); + } + else + { + await UnknownRoleAsync(type, data.Role.Id, guild.Id).ConfigureAwait(false); + return; + } + } + else + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + } + break; + case "GUILD_ROLE_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_DELETE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); + if (guild != null) + { + var role = guild.RemoveRole(data.RoleId); + if (role != null) + { + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + await TimedInvokeAsync(_roleDeletedEvent, nameof(RoleDeleted), role).ConfigureAwait(false); + } + else + { + await UnknownRoleAsync(type, data.RoleId, guild.Id).ConfigureAwait(false); + return; + } + } + else + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + } + break; + + //Bans + case "GUILD_BAN_ADD": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_BAN_ADD)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); + if (guild != null) + { + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + SocketUser user = guild.GetUser(data.User.Id); + if (user == null) + user = SocketUnknownUser.Create(this, State, data.User); + await TimedInvokeAsync(_userBannedEvent, nameof(UserBanned), user, guild).ConfigureAwait(false); + } + else + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + } + break; + case "GUILD_BAN_REMOVE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_BAN_REMOVE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); + if (guild != null) + { + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + SocketUser user = State.GetUser(data.User.Id); + if (user == null) + user = SocketUnknownUser.Create(this, State, data.User); + await TimedInvokeAsync(_userUnbannedEvent, nameof(UserUnbanned), user, guild).ConfigureAwait(false); + } + else + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + } + break; + + //Messages + case "MESSAGE_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_CREATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) + { + var guild = (channel as SocketGuildChannel)?.Guild; + if (guild != null && !guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + SocketUser author; + if (guild != null) + { + if (data.WebhookId.IsSpecified) + author = SocketWebhookUser.Create(guild, State, data.Author.Value, data.WebhookId.Value); + else + author = guild.GetUser(data.Author.Value.Id); + } + else + author = (channel as SocketChannel).GetUser(data.Author.Value.Id); + + if (author == null) + { + if (guild != null) + { + if (data.Member.IsSpecified) // member isn't always included, but use it when we can + { + data.Member.Value.User = data.Author.Value; + author = guild.AddOrUpdateUser(data.Member.Value); + } + else + author = guild.AddOrUpdateUser(data.Author.Value); // user has no guild-specific data + } + else if (channel is SocketGroupChannel) + author = (channel as SocketGroupChannel).GetOrAddUser(data.Author.Value); + else + { + await UnknownChannelUserAsync(type, data.Author.Value.Id, channel.Id).ConfigureAwait(false); + return; + } + } + + var msg = SocketMessage.Create(this, State, author, channel, data); + SocketChannelHelper.AddMessage(channel, this, msg); + await TimedInvokeAsync(_messageReceivedEvent, nameof(MessageReceived), msg).ConfigureAwait(false); + } + else + { + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + return; + } + } + break; + case "MESSAGE_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) + { + var guild = (channel as SocketGuildChannel)?.Guild; + if (guild != null && !guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + SocketMessage before = null, after = null; + SocketMessage cachedMsg = channel.GetCachedMessage(data.Id); + bool isCached = cachedMsg != null; + if (isCached) + { + before = cachedMsg.Clone(); + cachedMsg.Update(State, data); + after = cachedMsg; + } + else + { + //Edited message isnt in cache, create a detached one + SocketUser author; + if (data.Author.IsSpecified) + { + if (guild != null) + author = guild.GetUser(data.Author.Value.Id); + else + author = (channel as SocketChannel).GetUser(data.Author.Value.Id); + if (author == null) + author = SocketUnknownUser.Create(this, State, data.Author.Value); + } + else + // Message author wasn't specified in the payload, so create a completely anonymous unknown user + author = new SocketUnknownUser(this, id: 0); + + after = SocketMessage.Create(this, State, author, channel, data); + } + var cacheableBefore = new Cacheable(before, data.Id, isCached, async () => await channel.GetMessageAsync(data.Id).ConfigureAwait(false)); + + await TimedInvokeAsync(_messageUpdatedEvent, nameof(MessageUpdated), cacheableBefore, after, channel).ConfigureAwait(false); + } + else + { + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + return; + } + } + break; + case "MESSAGE_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) + { + var guild = (channel as SocketGuildChannel)?.Guild; + if (!(guild?.IsSynced ?? true)) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + var msg = SocketChannelHelper.RemoveMessage(channel, this, data.Id); + bool isCached = msg != null; + var cacheable = new Cacheable(msg, data.Id, isCached, async () => await channel.GetMessageAsync(data.Id).ConfigureAwait(false)); + + await TimedInvokeAsync(_messageDeletedEvent, nameof(MessageDeleted), cacheable, channel).ConfigureAwait(false); + } + else + { + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + return; + } + } + break; + case "MESSAGE_REACTION_ADD": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_ADD)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) + { + var cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; + bool isCached = cachedMsg != null; + var user = await channel.GetUserAsync(data.UserId, CacheMode.CacheOnly).ConfigureAwait(false); + + var optionalMsg = !isCached + ? Optional.Create() + : Optional.Create(cachedMsg); + + var optionalUser = user is null + ? Optional.Create() + : Optional.Create(user); + + var reaction = SocketReaction.Create(data, channel, optionalMsg, optionalUser); + var cacheable = new Cacheable(cachedMsg, data.MessageId, isCached, async () => await channel.GetMessageAsync(data.MessageId).ConfigureAwait(false) as IUserMessage); + + cachedMsg?.AddReaction(reaction); + + await TimedInvokeAsync(_reactionAddedEvent, nameof(ReactionAdded), cacheable, channel, reaction).ConfigureAwait(false); + } + else + { + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + return; + } + } + break; + case "MESSAGE_REACTION_REMOVE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) + { + var cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; + bool isCached = cachedMsg != null; + var user = await channel.GetUserAsync(data.UserId, CacheMode.CacheOnly).ConfigureAwait(false); + + var optionalMsg = !isCached + ? Optional.Create() + : Optional.Create(cachedMsg); + + var optionalUser = user is null + ? Optional.Create() + : Optional.Create(user); + + var reaction = SocketReaction.Create(data, channel, optionalMsg, optionalUser); + var cacheable = new Cacheable(cachedMsg, data.MessageId, isCached, async () => await channel.GetMessageAsync(data.MessageId).ConfigureAwait(false) as IUserMessage); + + cachedMsg?.RemoveReaction(reaction); + + await TimedInvokeAsync(_reactionRemovedEvent, nameof(ReactionRemoved), cacheable, channel, reaction).ConfigureAwait(false); + } + else + { + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + return; + } + } + break; + case "MESSAGE_REACTION_REMOVE_ALL": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE_ALL)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) + { + var cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; + bool isCached = cachedMsg != null; + var cacheable = new Cacheable(cachedMsg, data.MessageId, isCached, async () => (await channel.GetMessageAsync(data.MessageId).ConfigureAwait(false)) as IUserMessage); + + cachedMsg?.ClearReactions(); + + await TimedInvokeAsync(_reactionsClearedEvent, nameof(ReactionsCleared), cacheable, channel).ConfigureAwait(false); + } + else + { + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + return; + } + } + break; + case "MESSAGE_DELETE_BULK": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE_BULK)").ConfigureAwait(false); + + if (!ExclusiveBulkDelete.HasValue) + { + await _gatewayLogger.WarningAsync("A bulk delete event has been received, but the event handling behavior has not been set. " + + "To suppress this message, set the ExclusiveBulkDelete configuration property. " + + "This message will appear only once."); + ExclusiveBulkDelete = false; + } + + var data = (payload as JToken).ToObject(_serializer); + if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) + { + var guild = (channel as SocketGuildChannel)?.Guild; + if (!(guild?.IsSynced ?? true)) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + var cacheableList = new List>(data.Ids.Length); + foreach (ulong id in data.Ids) + { + var msg = SocketChannelHelper.RemoveMessage(channel, this, id); + bool isCached = msg != null; + var cacheable = new Cacheable(msg, id, isCached, async () => await channel.GetMessageAsync(id).ConfigureAwait(false)); + cacheableList.Add(cacheable); + + if (!ExclusiveBulkDelete ?? false) // this shouldn't happen, but we'll play it safe anyways + await TimedInvokeAsync(_messageDeletedEvent, nameof(MessageDeleted), cacheable, channel).ConfigureAwait(false); + } + + await TimedInvokeAsync(_messagesBulkDeletedEvent, nameof(MessagesBulkDeleted), cacheableList, channel).ConfigureAwait(false); + } + else + { + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + return; + } + } + break; + + //Statuses + case "PRESENCE_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (PRESENCE_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + if (data.GuildId.IsSpecified) + { + var guild = State.GetGuild(data.GuildId.Value); + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + var user = guild.GetUser(data.User.Id); + if (user == null) + { + if (data.Status == UserStatus.Offline) + { + return; + } + user = guild.AddOrUpdateUser(data); + } + else + { + var globalBefore = user.GlobalUser.Clone(); + if (user.GlobalUser.Update(State, data.User)) + { + //Global data was updated, trigger UserUpdated + await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false); + } + } + + var before = user.Clone(); + user.Update(State, data, true); + await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), before, user).ConfigureAwait(false); + } + else + { + var globalUser = State.GetUser(data.User.Id); + if (globalUser == null) + { + await UnknownGlobalUserAsync(type, data.User.Id).ConfigureAwait(false); + return; + } + + var before = globalUser.Clone(); + globalUser.Update(State, data.User); + globalUser.Update(State, data); + await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), before, globalUser).ConfigureAwait(false); + } + } + break; + case "TYPING_START": + { + await _gatewayLogger.DebugAsync("Received Dispatch (TYPING_START)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) + { + var guild = (channel as SocketGuildChannel)?.Guild; + if (!(guild?.IsSynced ?? true)) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + var user = (channel as SocketChannel).GetUser(data.UserId); + if (user == null) + { + if (guild != null) + user = guild.AddOrUpdateUser(data.Member); + } + if (user != null) + await TimedInvokeAsync(_userIsTypingEvent, nameof(UserIsTyping), user, channel).ConfigureAwait(false); + } + } + break; + + //Users + case "USER_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (USER_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + if (data.Id == CurrentUser.Id) + { + var before = CurrentUser.Clone(); + CurrentUser.Update(State, data); + await TimedInvokeAsync(_selfUpdatedEvent, nameof(CurrentUserUpdated), before, CurrentUser).ConfigureAwait(false); + } + else + { + await _gatewayLogger.WarningAsync("Received USER_UPDATE for wrong user.").ConfigureAwait(false); + return; + } + } + break; + + //Voice + case "VOICE_STATE_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_STATE_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + SocketUser user; + SocketVoiceState before, after; + if (data.GuildId != null) + { + var guild = State.GetGuild(data.GuildId.Value); + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } + else if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + if (data.ChannelId != null) + { + before = guild.GetVoiceState(data.UserId)?.Clone() ?? SocketVoiceState.Default; + after = await guild.AddOrUpdateVoiceStateAsync(State, data).ConfigureAwait(false); + /*if (data.UserId == CurrentUser.Id) + { + var _ = guild.FinishJoinAudioChannel().ConfigureAwait(false); + }*/ + } + else + { + before = await guild.RemoveVoiceStateAsync(data.UserId).ConfigureAwait(false) ?? SocketVoiceState.Default; + after = SocketVoiceState.Create(null, data); + } + + // per g250k, this should always be sent, but apparently not always + user = guild.GetUser(data.UserId) + ?? (data.Member.IsSpecified ? guild.AddOrUpdateUser(data.Member.Value) : null); + if (user == null) + { + await UnknownGuildUserAsync(type, data.UserId, guild.Id).ConfigureAwait(false); + return; + } + } + else + { + var groupChannel = State.GetChannel(data.ChannelId.Value) as SocketGroupChannel; + if (groupChannel == null) + { + await UnknownChannelAsync(type, data.ChannelId.Value).ConfigureAwait(false); + return; + } + if (data.ChannelId != null) + { + before = groupChannel.GetVoiceState(data.UserId)?.Clone() ?? SocketVoiceState.Default; + after = groupChannel.AddOrUpdateVoiceState(State, data); + } + else + { + before = groupChannel.RemoveVoiceState(data.UserId) ?? SocketVoiceState.Default; + after = SocketVoiceState.Create(null, data); + } + user = groupChannel.GetUser(data.UserId); + if (user == null) + { + await UnknownChannelUserAsync(type, data.UserId, groupChannel.Id).ConfigureAwait(false); + return; + } + } + + await TimedInvokeAsync(_userVoiceStateUpdatedEvent, nameof(UserVoiceStateUpdated), user, before, after).ConfigureAwait(false); + } + break; + case "VOICE_SERVER_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_SERVER_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); + var isCached = guild != null; + var cachedGuild = new Cacheable(guild, data.GuildId, isCached, + () => Task.FromResult(State.GetGuild(data.GuildId) as IGuild)); + + var voiceServer = new SocketVoiceServer(cachedGuild, data.Endpoint, data.Token); + await TimedInvokeAsync(_voiceServerUpdatedEvent, nameof(UserVoiceStateUpdated), voiceServer).ConfigureAwait(false); + + if (isCached) + { + var endpoint = data.Endpoint; + + //Only strip out the port if the endpoint contains it + var portBegin = endpoint.LastIndexOf(':'); + if (portBegin > 0) + endpoint = endpoint.Substring(0, portBegin); + + var _ = guild.FinishConnectAudio(endpoint, data.Token).ConfigureAwait(false); + } + else + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + } + + } + break; + + //Ignored (User only) + case "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)").ConfigureAwait(false); + break; + case "GUILD_INTEGRATIONS_UPDATE": + await _gatewayLogger.DebugAsync("Ignored Dispatch (GUILD_INTEGRATIONS_UPDATE)").ConfigureAwait(false); + break; + case "MESSAGE_ACK": + await _gatewayLogger.DebugAsync("Ignored Dispatch (MESSAGE_ACK)").ConfigureAwait(false); + break; + case "PRESENCES_REPLACE": + await _gatewayLogger.DebugAsync("Ignored Dispatch (PRESENCES_REPLACE)").ConfigureAwait(false); + break; + case "USER_SETTINGS_UPDATE": + await _gatewayLogger.DebugAsync("Ignored Dispatch (USER_SETTINGS_UPDATE)").ConfigureAwait(false); + break; + case "WEBHOOKS_UPDATE": + await _gatewayLogger.DebugAsync("Ignored Dispatch (WEBHOOKS_UPDATE)").ConfigureAwait(false); + break; + + //Others + default: + await _gatewayLogger.WarningAsync($"Unknown Dispatch ({type})").ConfigureAwait(false); + break; + } + break; + default: + await _gatewayLogger.WarningAsync($"Unknown OpCode ({opCode})").ConfigureAwait(false); + break; + } + } + catch (Exception ex) + { + await _gatewayLogger.ErrorAsync($"Error handling {opCode}{(type != null ? $" ({type})" : "")}", ex).ConfigureAwait(false); + } + } + + private async Task RunHeartbeatAsync(int intervalMillis, CancellationToken cancelToken) + { + try + { + await _gatewayLogger.DebugAsync("Heartbeat Started").ConfigureAwait(false); + while (!cancelToken.IsCancellationRequested) + { + int now = Environment.TickCount; + + //Did server respond to our last heartbeat, or are we still receiving messages (long load?) + if (_heartbeatTimes.Count != 0 && (now - _lastMessageTime) > intervalMillis) + { + if (ConnectionState == ConnectionState.Connected && (_guildDownloadTask?.IsCompleted ?? true)) + { + _connection.Error(new Exception("Server missed last heartbeat")); + return; + } + } + + _heartbeatTimes.Enqueue(now); + try + { + await ApiClient.SendHeartbeatAsync(_lastSeq).ConfigureAwait(false); + } + catch (Exception ex) + { + await _gatewayLogger.WarningAsync("Heartbeat Errored", ex).ConfigureAwait(false); + } + + await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); + } + await _gatewayLogger.DebugAsync("Heartbeat Stopped").ConfigureAwait(false); + } + catch (OperationCanceledException) + { + await _gatewayLogger.DebugAsync("Heartbeat Stopped").ConfigureAwait(false); + } + catch (Exception ex) + { + await _gatewayLogger.ErrorAsync("Heartbeat Errored", ex).ConfigureAwait(false); + } + } + /*public async Task WaitForGuildsAsync() + { + var downloadTask = _guildDownloadTask; + if (downloadTask != null) + await _guildDownloadTask.ConfigureAwait(false); + }*/ + private async Task WaitForGuildsAsync(CancellationToken cancelToken, Logger logger) + { + //Wait for GUILD_AVAILABLEs + try + { + await logger.DebugAsync("GuildDownloader Started").ConfigureAwait(false); + while ((_unavailableGuildCount != 0) && (Environment.TickCount - _lastGuildAvailableTime < 2000)) + await Task.Delay(500, cancelToken).ConfigureAwait(false); + await logger.DebugAsync("GuildDownloader Stopped").ConfigureAwait(false); + } + catch (OperationCanceledException) + { + await logger.DebugAsync("GuildDownloader Stopped").ConfigureAwait(false); + } + catch (Exception ex) + { + await logger.ErrorAsync("GuildDownloader Errored", ex).ConfigureAwait(false); + } + } + private async Task SyncGuildsAsync() + { + 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 _ in guild.Channels) + State.RemoveChannel(id); + foreach (var user in guild.Users) + user.GlobalUser.RemoveRef(this); + } + return guild; + } + + /// Unexpected channel type is created. + internal ISocketPrivateChannel AddPrivateChannel(API.Channel model, ClientState state) + { + var channel = SocketChannel.CreatePrivate(this, state, model); + state.AddChannel(channel as SocketChannel); + if (channel is SocketDMChannel dm) + dm.Recipient.GlobalUser.DMChannel = dm; + + return channel; + } + internal ISocketPrivateChannel RemovePrivateChannel(ulong id) + { + var channel = State.RemoveChannel(id) as ISocketPrivateChannel; + if (channel != null) + { + if (channel is SocketDMChannel dmChannel) + dmChannel.Recipient.GlobalUser.DMChannel = null; + + foreach (var recipient in channel.Recipients) + recipient.GlobalUser.RemoveRef(this); + } + return channel; + } + + private async Task GuildAvailableAsync(SocketGuild guild) + { + if (!guild.IsConnected) + { + guild.IsConnected = true; + await TimedInvokeAsync(_guildAvailableEvent, nameof(GuildAvailable), guild).ConfigureAwait(false); + } + } + private async Task GuildUnavailableAsync(SocketGuild guild) + { + if (guild.IsConnected) + { + guild.IsConnected = false; + await TimedInvokeAsync(_guildUnavailableEvent, nameof(GuildUnavailable), guild).ConfigureAwait(false); + } + } + + private async Task TimedInvokeAsync(AsyncEvent> eventHandler, string name) + { + if (eventHandler.HasSubscribers) + { + if (HandlerTimeout.HasValue) + await TimeoutWrap(name, eventHandler.InvokeAsync).ConfigureAwait(false); + else + await eventHandler.InvokeAsync().ConfigureAwait(false); + } + } + private async Task TimedInvokeAsync(AsyncEvent> eventHandler, string name, T arg) + { + if (eventHandler.HasSubscribers) + { + if (HandlerTimeout.HasValue) + await TimeoutWrap(name, () => eventHandler.InvokeAsync(arg)).ConfigureAwait(false); + else + await eventHandler.InvokeAsync(arg).ConfigureAwait(false); + } + } + private async Task TimedInvokeAsync(AsyncEvent> eventHandler, string name, T1 arg1, T2 arg2) + { + if (eventHandler.HasSubscribers) + { + if (HandlerTimeout.HasValue) + await TimeoutWrap(name, () => eventHandler.InvokeAsync(arg1, arg2)).ConfigureAwait(false); + else + await eventHandler.InvokeAsync(arg1, arg2).ConfigureAwait(false); + } + } + private async Task TimedInvokeAsync(AsyncEvent> eventHandler, string name, T1 arg1, T2 arg2, T3 arg3) + { + if (eventHandler.HasSubscribers) + { + if (HandlerTimeout.HasValue) + await TimeoutWrap(name, () => eventHandler.InvokeAsync(arg1, arg2, arg3)).ConfigureAwait(false); + else + await eventHandler.InvokeAsync(arg1, arg2, arg3).ConfigureAwait(false); + } + } + private async Task TimedInvokeAsync(AsyncEvent> eventHandler, string name, T1 arg1, T2 arg2, T3 arg3, T4 arg4) + { + if (eventHandler.HasSubscribers) + { + if (HandlerTimeout.HasValue) + await TimeoutWrap(name, () => eventHandler.InvokeAsync(arg1, arg2, arg3, arg4)).ConfigureAwait(false); + else + await eventHandler.InvokeAsync(arg1, arg2, arg3, arg4).ConfigureAwait(false); + } + } + private async Task TimedInvokeAsync(AsyncEvent> eventHandler, string name, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) + { + if (eventHandler.HasSubscribers) + { + if (HandlerTimeout.HasValue) + await TimeoutWrap(name, () => eventHandler.InvokeAsync(arg1, arg2, arg3, arg4, arg5)).ConfigureAwait(false); + else + await eventHandler.InvokeAsync(arg1, arg2, arg3, arg4, arg5).ConfigureAwait(false); + } + } + private async Task TimeoutWrap(string name, Func action) + { + try + { + var timeoutTask = Task.Delay(HandlerTimeout.Value); + var handlersTask = action(); + if (await Task.WhenAny(timeoutTask, handlersTask).ConfigureAwait(false) == timeoutTask) + { + await _gatewayLogger.WarningAsync($"A {name} handler is blocking the gateway task.").ConfigureAwait(false); + } + await handlersTask.ConfigureAwait(false); //Ensure the handler completes + } + catch (Exception ex) + { + await _gatewayLogger.WarningAsync($"A {name} handler has thrown an unhandled exception.", ex).ConfigureAwait(false); + } + } + + private async Task UnknownGlobalUserAsync(string evnt, ulong userId) + { + string details = $"{evnt} User={userId}"; + await _gatewayLogger.WarningAsync($"Unknown User ({details}).").ConfigureAwait(false); + } + private async Task UnknownChannelUserAsync(string evnt, ulong userId, ulong channelId) + { + string details = $"{evnt} User={userId} Channel={channelId}"; + await _gatewayLogger.WarningAsync($"Unknown User ({details}).").ConfigureAwait(false); + } + private async Task UnknownGuildUserAsync(string evnt, ulong userId, ulong guildId) + { + string details = $"{evnt} User={userId} Guild={guildId}"; + await _gatewayLogger.WarningAsync($"Unknown User ({details}).").ConfigureAwait(false); + } + private async Task IncompleteGuildUserAsync(string evnt, ulong userId, ulong guildId) + { + string details = $"{evnt} User={userId} Guild={guildId}"; + await _gatewayLogger.DebugAsync($"User has not been downloaded ({details}).").ConfigureAwait(false); + } + private async Task UnknownChannelAsync(string evnt, ulong channelId) + { + string details = $"{evnt} Channel={channelId}"; + await _gatewayLogger.WarningAsync($"Unknown Channel ({details}).").ConfigureAwait(false); + } + private async Task UnknownChannelAsync(string evnt, ulong channelId, ulong guildId) + { + if (guildId == 0) + { + await UnknownChannelAsync(evnt, channelId).ConfigureAwait(false); + return; + } + string details = $"{evnt} Channel={channelId} Guild={guildId}"; + await _gatewayLogger.WarningAsync($"Unknown Channel ({details}).").ConfigureAwait(false); + } + private async Task UnknownRoleAsync(string evnt, ulong roleId, ulong guildId) + { + string details = $"{evnt} Role={roleId} Guild={guildId}"; + await _gatewayLogger.WarningAsync($"Unknown Role ({details}).").ConfigureAwait(false); + } + private async Task UnknownGuildAsync(string evnt, ulong guildId) + { + string details = $"{evnt} Guild={guildId}"; + await _gatewayLogger.WarningAsync($"Unknown Guild ({details}).").ConfigureAwait(false); + } + private async Task UnsyncedGuildAsync(string evnt, ulong guildId) + { + string details = $"{evnt} Guild={guildId}"; + await _gatewayLogger.DebugAsync($"Unsynced Guild ({details}).").ConfigureAwait(false); + } + + internal int GetAudioId() => _nextAudioId++; + + //IDiscordClient + /// + async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) + => await GetApplicationInfoAsync().ConfigureAwait(false); + + /// + Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetChannel(id)); + /// + Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode, RequestOptions options) + => Task.FromResult>(PrivateChannels); + /// + Task> IDiscordClient.GetDMChannelsAsync(CacheMode mode, RequestOptions options) + => Task.FromResult>(DMChannels); + /// + Task> IDiscordClient.GetGroupChannelsAsync(CacheMode mode, RequestOptions options) + => Task.FromResult>(GroupChannels); + + /// + async Task> IDiscordClient.GetConnectionsAsync(RequestOptions options) + => await GetConnectionsAsync().ConfigureAwait(false); + + /// + async Task IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options) + => await GetInviteAsync(inviteId, options).ConfigureAwait(false); + + /// + Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetGuild(id)); + /// + Task> IDiscordClient.GetGuildsAsync(CacheMode mode, RequestOptions options) + => Task.FromResult>(Guilds); + /// + async Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) + => await CreateGuildAsync(name, region, jpegIcon).ConfigureAwait(false); + + /// + Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetUser(id)); + /// + Task IDiscordClient.GetUserAsync(string username, string discriminator, RequestOptions options) + => Task.FromResult(GetUser(username, discriminator)); + + /// + Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) + => Task.FromResult>(VoiceRegions); + /// + Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) + => Task.FromResult(GetVoiceRegion(id)); + + /// + async Task IDiscordClient.StartAsync() + => await StartAsync().ConfigureAwait(false); + /// + async Task IDiscordClient.StopAsync() + => await StopAsync().ConfigureAwait(false); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/DiscordSocketConfig.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/DiscordSocketConfig.cs new file mode 100644 index 0000000..98ab0ef --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/DiscordSocketConfig.cs @@ -0,0 +1,138 @@ +using Discord.Net.Udp; +using Discord.Net.WebSockets; +using Discord.Rest; + +namespace Discord.WebSocket +{ + /// + /// Represents a configuration class for . + /// + /// + /// This configuration, based on , helps determine several key configurations the + /// socket client depend on. For instance, shards and connection timeout. + /// + /// + /// The following config enables the message cache and configures the client to always download user upon guild + /// availability. + /// + /// var config = new DiscordSocketConfig + /// { + /// AlwaysDownloadUsers = true, + /// MessageCacheSize = 100 + /// }; + /// var client = new DiscordSocketClient(config); + /// + /// + public class DiscordSocketConfig : DiscordRestConfig + { + /// + /// Returns the encoding gateway should use. + /// + public const string GatewayEncoding = "json"; + + /// + /// Gets or sets the WebSocket host to connect to. If null, the client will use the + /// /gateway endpoint. + /// + public string GatewayHost { get; set; } = null; + + /// + /// Gets or sets the time, in milliseconds, to wait for a connection to complete before aborting. + /// + public int ConnectionTimeout { get; set; } = 30000; + + /// + /// Gets or sets the ID for this shard. Must be less than . + /// + public int? ShardId { get; set; } = null; + /// + /// Gets or sets the total number of shards for this application. + /// + public int? TotalShards { get; set; } = null; + + /// + /// Gets or sets the number of messages per channel that should be kept in cache. Setting this to zero + /// disables the message cache entirely. + /// + public int MessageCacheSize { get; set; } = 0; + /// + /// Gets or sets the max number of users a guild may have for offline users to be included in the READY + /// packet. Max is 250. + /// + public int LargeThreshold { get; set; } = 250; + + /// + /// Gets or sets the provider used to generate new WebSocket connections. + /// + public WebSocketProvider WebSocketProvider { get; set; } + /// + /// Gets or sets the provider used to generate new UDP sockets. + /// + public UdpSocketProvider UdpSocketProvider { get; set; } + + /// + /// Gets or sets whether or not all users should be downloaded as guilds come available. + /// + /// + /// + /// By default, the Discord gateway will only send offline members if a guild has less than a certain number + /// of members (determined by in this library). This behaviour is why + /// sometimes a user may be missing from the WebSocket cache for collections such as + /// . + /// + /// + /// This property ensures that whenever a guild becomes available (determined by + /// ), incomplete user chunks will be + /// downloaded to the WebSocket cache. + /// + /// + /// For more information, please see + /// Request Guild Members + /// on the official Discord API documentation. + /// + /// + /// Please note that it can be difficult to fill the cache completely on large guilds depending on the + /// traffic. If you are using the command system, the default user TypeReader may fail to find the user + /// due to this issue. This may be resolved at v3 of the library. Until then, you may want to consider + /// overriding the TypeReader and use + /// + /// or + /// as a backup. + /// + /// + public bool AlwaysDownloadUsers { get; set; } = false; + /// + /// Gets or sets the timeout for event handlers, in milliseconds, after which a warning will be logged. Null + /// disables this check. + /// + public int? HandlerTimeout { get; set; } = 3000; + + /// + /// Gets or sets the behavior for on bulk deletes. + /// + /// If true, the event will not be raised for bulk deletes, and + /// only the will be raised. + /// + /// If false, both events will be raised. + /// + /// If unset, both events will be raised, but a warning will be raised the first time a bulk delete event is received. + /// + public bool? ExclusiveBulkDelete { get; set; } = null; + + /// + /// Gets or sets enabling dispatching of guild subscription events e.g. presence and typing events. + /// + public bool GuildSubscriptions { get; set; } = true; + + /// + /// Initializes a default configuration. + /// + public DiscordSocketConfig() + { + WebSocketProvider = DefaultWebSocketProvider.Instance; + UdpSocketProvider = DefaultUdpSocketProvider.Instance; + } + + internal DiscordSocketConfig Clone() => MemberwiseClone() as DiscordSocketConfig; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/DiscordSocketRestClient.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/DiscordSocketRestClient.cs new file mode 100644 index 0000000..5107629 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/DiscordSocketRestClient.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading.Tasks; +using Discord.Rest; + +namespace Discord.WebSocket +{ + public class DiscordSocketRestClient : DiscordRestClient + { + internal DiscordSocketRestClient(DiscordRestConfig config, API.DiscordRestApiClient api) : base(config, api) { } + + public new Task LoginAsync(TokenType tokenType, string token, bool validateToken = true) + => throw new NotSupportedException("The Socket REST wrapper cannot be used to log in or out."); + internal override Task LoginInternalAsync(TokenType tokenType, string token, bool validateToken) + => throw new NotSupportedException("The Socket REST wrapper cannot be used to log in or out."); + public new Task LogoutAsync() + => throw new NotSupportedException("The Socket REST wrapper cannot be used to log in or out."); + internal override Task LogoutInternalAsync() + => throw new NotSupportedException("The Socket REST wrapper cannot be used to log in or out."); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/DiscordVoiceApiClient.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/DiscordVoiceApiClient.cs new file mode 100644 index 0000000..f78145d --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/DiscordVoiceApiClient.cs @@ -0,0 +1,273 @@ +#pragma warning disable CS1591 +using Discord.API; +using Discord.API.Voice; +using Discord.Net.Converters; +using Discord.Net.Udp; +using Discord.Net.WebSockets; +using Newtonsoft.Json; +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Audio +{ + internal class DiscordVoiceAPIClient : IDisposable + { + public const int MaxBitrate = 128 * 1024; + public const string Mode = "xsalsa20_poly1305"; + + public event Func SentRequest { add { _sentRequestEvent.Add(value); } remove { _sentRequestEvent.Remove(value); } } + private readonly AsyncEvent> _sentRequestEvent = new AsyncEvent>(); + public event Func SentGatewayMessage { add { _sentGatewayMessageEvent.Add(value); } remove { _sentGatewayMessageEvent.Remove(value); } } + private readonly AsyncEvent> _sentGatewayMessageEvent = new AsyncEvent>(); + public event Func SentDiscovery { add { _sentDiscoveryEvent.Add(value); } remove { _sentDiscoveryEvent.Remove(value); } } + private readonly AsyncEvent> _sentDiscoveryEvent = new AsyncEvent>(); + public event Func SentData { add { _sentDataEvent.Add(value); } remove { _sentDataEvent.Remove(value); } } + private readonly AsyncEvent> _sentDataEvent = new AsyncEvent>(); + + public event Func ReceivedEvent { add { _receivedEvent.Add(value); } remove { _receivedEvent.Remove(value); } } + private readonly AsyncEvent> _receivedEvent = new AsyncEvent>(); + public event Func ReceivedPacket { add { _receivedPacketEvent.Add(value); } remove { _receivedPacketEvent.Remove(value); } } + private readonly AsyncEvent> _receivedPacketEvent = new AsyncEvent>(); + public event Func Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } + private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); + + private readonly JsonSerializer _serializer; + private readonly SemaphoreSlim _connectionLock; + private readonly IUdpSocket _udp; + private CancellationTokenSource _connectCancelToken; + private bool _isDisposed; + private ulong _nextKeepalive; + + public ulong GuildId { get; } + internal IWebSocketClient WebSocketClient { get; } + public ConnectionState ConnectionState { get; private set; } + + public ushort UdpPort => _udp.Port; + + internal DiscordVoiceAPIClient(ulong guildId, WebSocketProvider webSocketProvider, UdpSocketProvider udpSocketProvider, JsonSerializer serializer = null) + { + GuildId = guildId; + _connectionLock = new SemaphoreSlim(1, 1); + _udp = udpSocketProvider(); + _udp.ReceivedDatagram += async (data, index, count) => + { + if (index != 0 || count != data.Length) + { + var newData = new byte[count]; + Buffer.BlockCopy(data, index, newData, 0, count); + data = newData; + } + await _receivedPacketEvent.InvokeAsync(data).ConfigureAwait(false); + }; + + WebSocketClient = webSocketProvider(); + //_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); //(Causes issues in .Net 4.6+) + WebSocketClient.BinaryMessage += async (data, index, count) => + { + using (var compressed = new MemoryStream(data, index + 2, count - 2)) + using (var decompressed = new MemoryStream()) + { + using (var zlib = new DeflateStream(compressed, CompressionMode.Decompress)) + zlib.CopyTo(decompressed); + decompressed.Position = 0; + using (var reader = new StreamReader(decompressed)) + { + var msg = JsonConvert.DeserializeObject(reader.ReadToEnd()); + await _receivedEvent.InvokeAsync((VoiceOpCode)msg.Operation, msg.Payload).ConfigureAwait(false); + } + } + }; + WebSocketClient.TextMessage += async text => + { + var msg = JsonConvert.DeserializeObject(text); + await _receivedEvent.InvokeAsync((VoiceOpCode)msg.Operation, msg.Payload).ConfigureAwait(false); + }; + WebSocketClient.Closed += async ex => + { + await DisconnectAsync().ConfigureAwait(false); + await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); + }; + + _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; + } + private void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + _connectCancelToken?.Dispose(); + _udp?.Dispose(); + WebSocketClient?.Dispose(); + _connectionLock?.Dispose(); + } + _isDisposed = true; + } + } + public void Dispose() => Dispose(true); + + public async Task SendAsync(VoiceOpCode opCode, object payload, RequestOptions options = null) + { + byte[] bytes = null; + 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).ConfigureAwait(false); + } + public async Task SendAsync(byte[] data, int offset, int bytes) + { + await _udp.SendAsync(data, offset, bytes).ConfigureAwait(false); + await _sentDataEvent.InvokeAsync(bytes).ConfigureAwait(false); + } + + //WebSocket + public async Task SendHeartbeatAsync(RequestOptions options = null) + { + await SendAsync(VoiceOpCode.Heartbeat, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), options: options).ConfigureAwait(false); + } + public async Task SendIdentityAsync(ulong userId, string sessionId, string token) + { + await SendAsync(VoiceOpCode.Identify, new IdentifyParams + { + GuildId = GuildId, + UserId = userId, + SessionId = sessionId, + Token = token + }).ConfigureAwait(false); + } + public async Task SendSelectProtocol(string externalIp, int externalPort) + { + await SendAsync(VoiceOpCode.SelectProtocol, new SelectProtocolParams + { + Protocol = "udp", + Data = new UdpProtocolInfo + { + Address = externalIp, + Port = externalPort, + Mode = Mode + } + }).ConfigureAwait(false); + } + public async Task SendSetSpeaking(bool value) + { + await SendAsync(VoiceOpCode.Speaking, new SpeakingParams + { + IsSpeaking = value, + Delay = 0 + }).ConfigureAwait(false); + } + + public async Task ConnectAsync(string url) + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await ConnectInternalAsync(url).ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + private async Task ConnectInternalAsync(string url) + { + ConnectionState = ConnectionState.Connecting; + try + { + _connectCancelToken?.Dispose(); + _connectCancelToken = new CancellationTokenSource(); + var cancelToken = _connectCancelToken.Token; + + WebSocketClient.SetCancelToken(cancelToken); + await WebSocketClient.ConnectAsync(url).ConfigureAwait(false); + + _udp.SetCancelToken(cancelToken); + await _udp.StartAsync().ConfigureAwait(false); + + ConnectionState = ConnectionState.Connected; + } + catch + { + await DisconnectInternalAsync().ConfigureAwait(false); + throw; + } + } + + public async Task DisconnectAsync() + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await DisconnectInternalAsync().ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + private async Task DisconnectInternalAsync() + { + if (ConnectionState == ConnectionState.Disconnected) return; + ConnectionState = ConnectionState.Disconnecting; + + try { _connectCancelToken?.Cancel(false); } + catch { } + + //Wait for tasks to complete + await _udp.StopAsync().ConfigureAwait(false); + await WebSocketClient.DisconnectAsync().ConfigureAwait(false); + + ConnectionState = ConnectionState.Disconnected; + } + + //Udp + public async Task SendDiscoveryAsync(uint ssrc) + { + var packet = new byte[70]; + packet[0] = (byte)(ssrc >> 24); + packet[1] = (byte)(ssrc >> 16); + packet[2] = (byte)(ssrc >> 8); + packet[3] = (byte)(ssrc >> 0); + await SendAsync(packet, 0, 70).ConfigureAwait(false); + await _sentDiscoveryEvent.InvokeAsync().ConfigureAwait(false); + } + public async Task SendKeepaliveAsync() + { + var value = _nextKeepalive++; + var packet = new byte[8]; + packet[0] = (byte)(value >> 0); + packet[1] = (byte)(value >> 8); + packet[2] = (byte)(value >> 16); + packet[3] = (byte)(value >> 24); + packet[4] = (byte)(value >> 32); + packet[5] = (byte)(value >> 40); + packet[6] = (byte)(value >> 48); + packet[7] = (byte)(value >> 56); + await SendAsync(packet, 0, 8).ConfigureAwait(false); + return value; + } + + public void SetUdpEndpoint(string ip, int port) + { + _udp.SetDestination(ip, port); + } + + //Helpers + private static double ToMilliseconds(Stopwatch stopwatch) => Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); + private string SerializeJson(object value) + { + var sb = new StringBuilder(256); + using (TextWriter text = new StringWriter(sb, CultureInfo.InvariantCulture)) + using (JsonWriter writer = new JsonTextWriter(text)) + _serializer.Serialize(writer, value); + return sb.ToString(); + } + private T DeserializeJson(Stream jsonStream) + { + using (TextReader text = new StreamReader(jsonStream)) + using (JsonReader reader = new JsonTextReader(text)) + return _serializer.Deserialize(reader); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs new file mode 100644 index 0000000..3cb978a --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs @@ -0,0 +1,9 @@ +namespace Discord.WebSocket +{ + /// + /// Represents a generic WebSocket-based audio channel. + /// + public interface ISocketAudioChannel : IAudioChannel + { + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs new file mode 100644 index 0000000..b88d010 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs @@ -0,0 +1,177 @@ +using Discord.Rest; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace Discord.WebSocket +{ + /// + /// Represents a generic WebSocket-based channel that can send and receive messages. + /// + public interface ISocketMessageChannel : IMessageChannel + { + /// + /// Gets all messages in this channel's cache. + /// + /// + /// A read-only collection of WebSocket-based messages. + /// + IReadOnlyCollection CachedMessages { get; } + + /// + /// Sends a message to this message channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The message to be sent. + /// Determines whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + new Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); + /// + /// Sends a file to this message channel with an optional caption. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The file path of the file. + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// Whether the message attachment should be hidden as a spoiler. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false); + /// + /// Sends a file to this message channel with an optional caption. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The of the file to be sent. + /// The name of the attachment. + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// Whether the message attachment should be hidden as a spoiler. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false); + + /// + /// Gets a cached message from this channel. + /// + /// + /// + /// This method requires the use of cache, which is not enabled by default; if caching is not enabled, + /// this method will always return null. Please refer to + /// for more details. + /// + /// + /// This method retrieves the message from the local WebSocket cache and does not send any additional + /// request to Discord. This message may be a message that has been deleted. + /// + /// + /// The snowflake identifier of the message. + /// + /// A WebSocket-based message object; null if it does not exist in the cache or if caching is not + /// enabled. + /// + SocketMessage GetCachedMessage(ulong id); + /// + /// Gets the last N cached messages from this message channel. + /// + /// + /// + /// This method requires the use of cache, which is not enabled by default; if caching is not enabled, + /// this method will always return an empty collection. Please refer to + /// for more details. + /// + /// + /// This method retrieves the message(s) from the local WebSocket cache and does not send any additional + /// request to Discord. This read-only collection may include messages that have been deleted. The + /// maximum number of messages that can be retrieved from this method depends on the + /// set. + /// + /// + /// The number of messages to get. + /// + /// A read-only collection of WebSocket-based messages. + /// + IReadOnlyCollection GetCachedMessages(int limit = DiscordConfig.MaxMessagesPerBatch); + + /// + /// Gets the last N cached messages starting from a certain message in this message channel. + /// + /// + /// + /// This method requires the use of cache, which is not enabled by default; if caching is not enabled, + /// this method will always return an empty collection. Please refer to + /// for more details. + /// + /// + /// This method retrieves the message(s) from the local WebSocket cache and does not send any additional + /// request to Discord. This read-only collection may include messages that have been deleted. The + /// maximum number of messages that can be retrieved from this method depends on the + /// set. + /// + /// + /// The message ID to start the fetching from. + /// The direction of which the message should be gotten from. + /// The number of messages to get. + /// + /// A read-only collection of WebSocket-based messages. + /// + IReadOnlyCollection GetCachedMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch); + /// + /// Gets the last N cached messages starting from a certain message in this message channel. + /// + /// + /// + /// This method requires the use of cache, which is not enabled by default; if caching is not enabled, + /// this method will always return an empty collection. Please refer to + /// for more details. + /// + /// + /// This method retrieves the message(s) from the local WebSocket cache and does not send any additional + /// request to Discord. This read-only collection may include messages that have been deleted. The + /// maximum number of messages that can be retrieved from this method depends on the + /// set. + /// + /// + /// The message to start the fetching from. + /// The direction of which the message should be gotten from. + /// The number of messages to get. + /// + /// A read-only collection of WebSocket-based messages. + /// + IReadOnlyCollection GetCachedMessages(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch); + /// + /// Gets a read-only collection of pinned messages in this channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation for retrieving pinned messages in this channel. + /// The task result contains a read-only collection of messages found in the pinned messages. + /// + new Task> GetPinnedMessagesAsync(RequestOptions options = null); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/ISocketPrivateChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/ISocketPrivateChannel.cs new file mode 100644 index 0000000..08da223 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/ISocketPrivateChannel.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace Discord.WebSocket +{ + /// + /// Represents a generic WebSocket-based channel that is private to select recipients. + /// + public interface ISocketPrivateChannel : IPrivateChannel + { + new IReadOnlyCollection Recipients { get; } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs new file mode 100644 index 0000000..b90c197 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs @@ -0,0 +1,79 @@ +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 +{ + /// + /// Represents a WebSocket-based category channel. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketCategoryChannel : SocketGuildChannel, ICategoryChannel + { + /// + public override IReadOnlyCollection Users + => Guild.Users.Where(x => Permissions.GetValue( + Permissions.ResolveChannel(Guild, x, this, Permissions.ResolveGuild(Guild, x)), + ChannelPermission.ViewChannel)).ToImmutableArray(); + + /// + /// Gets the child channels of this category. + /// + /// + /// A read-only collection of whose + /// matches the snowflake identifier of this category + /// channel. + /// + public IReadOnlyCollection Channels + => Guild.Channels.Where(x => x is INestedChannel nestedChannel && nestedChannel.CategoryId == Id).ToImmutableArray(); + + internal SocketCategoryChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) + : base(discord, id, guild) + { + } + internal new static SocketCategoryChannel Create(SocketGuild guild, ClientState state, Model model) + { + var entity = new SocketCategoryChannel(guild.Discord, model.Id, guild); + entity.Update(state, model); + return entity; + } + + //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.ViewChannel)) + return user; + } + return null; + } + + private string DebuggerDisplay => $"{Name} ({Id}, Category)"; + internal new SocketCategoryChannel Clone() => MemberwiseClone() as SocketCategoryChannel; + + // IGuildChannel + /// + 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(); + /// + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetUser(id)); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs new file mode 100644 index 0000000..13c0c9b --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord.WebSocket +{ + /// + /// Represents a WebSocket-based channel. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public abstract class SocketChannel : SocketEntity, IChannel + { + /// + /// Gets when the channel is created. + /// + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + /// + /// Gets a collection of users from the WebSocket cache. + /// + public IReadOnlyCollection Users => GetUsersInternal(); + + internal SocketChannel(DiscordSocketClient discord, ulong id) + : base(discord, id) + { + } + + /// Unexpected channel type is created. + 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 + /// + /// Gets a generic user from this channel. + /// + /// The snowflake identifier of the user. + /// + /// A generic WebSocket-based user associated with the snowflake identifier. + /// + public SocketUser GetUser(ulong id) => GetUserInternal(id); + internal abstract SocketUser GetUserInternal(ulong id); + internal abstract IReadOnlyCollection GetUsersInternal(); + + private string DebuggerDisplay => $"Unknown ({Id}, Channel)"; + 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/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs new file mode 100644 index 0000000..e6339b6 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs @@ -0,0 +1,85 @@ +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(ISocketMessageChannel channel, DiscordSocketClient discord, MessageCache messages, + ulong? fromMessageId, Direction dir, int limit, CacheMode mode, 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 + ulong? minId = cachedMessages.Count > 0 ? cachedMessages.Min(x => x.Id) : fromMessageId; + var downloadedMessages = ChannelHelper.GetMessagesAsync(channel, discord, minId, dir, limit, 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, 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(); + } + /// Unexpected type. + public static void AddMessage(ISocketMessageChannel channel, DiscordSocketClient discord, + SocketMessage msg) + { + switch (channel) + { + case SocketDMChannel dmChannel: dmChannel.AddMessage(msg); break; + case SocketGroupChannel groupChannel: groupChannel.AddMessage(msg); break; + case SocketTextChannel textChannel: textChannel.AddMessage(msg); break; + default: throw new NotSupportedException($"Unexpected {nameof(ISocketMessageChannel)} type."); + } + } + /// Unexpected type. + public static SocketMessage RemoveMessage(ISocketMessageChannel channel, DiscordSocketClient discord, + ulong id) + { + switch (channel) + { + case SocketDMChannel dmChannel: return dmChannel.RemoveMessage(id); + case SocketGroupChannel groupChannel: return groupChannel.RemoveMessage(id); + case SocketTextChannel textChannel: return textChannel.RemoveMessage(id); + default: throw new NotSupportedException($"Unexpected {nameof(ISocketMessageChannel)} type."); + } + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs new file mode 100644 index 0000000..838fb8e --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -0,0 +1,252 @@ +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 +{ + /// + /// Represents a WebSocket-based direct-message channel. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketDMChannel : SocketChannel, IDMChannel, ISocketPrivateChannel, ISocketMessageChannel + { + private readonly MessageCache _messages; + + /// + /// Gets the recipient of the channel. + /// + public SocketUser Recipient { get; } + + /// + public IReadOnlyCollection CachedMessages => _messages?.Messages ?? ImmutableArray.Create(); + + /// + /// Gets a collection that is the current logged-in user and the recipient. + /// + public new IReadOnlyCollection Users => ImmutableArray.Create(Discord.CurrentUser, Recipient); + + internal SocketDMChannel(DiscordSocketClient discord, ulong id, SocketGlobalUser recipient) + : base(discord, id) + { + Recipient = recipient; + recipient.GlobalUser.AddRef(); + if (Discord.MessageCacheSize > 0) + _messages = new MessageCache(Discord); + } + 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); + /// + /// Gets the message associated with the given . + /// + /// TThe ID of the message. + /// The options to be used when sending the request. + /// + /// The message gotten from either the cache or the download, or null if none is found. + /// + public async Task GetMessageAsync(ulong id, RequestOptions options = null) + { + IMessage msg = _messages?.Get(id); + if (msg == null) + msg = await ChannelHelper.GetMessageAsync(this, Discord, id, options).ConfigureAwait(false); + return msg; + } + + /// + /// Gets the last N messages from this message channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The numbers of message to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// + public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, CacheMode.AllowDownload, options); + /// + /// Gets a collection of messages in this channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The ID of the starting message to get the messages from. + /// The direction of the messages to be gotten from. + /// The numbers of message to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// + public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, CacheMode.AllowDownload, options); + /// + /// Gets a collection of messages in this channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The starting message to get the messages from. + /// The direction of the messages to be gotten from. + /// The numbers of message to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// + 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, 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, options); + + /// + /// Message content is too long, length must be less or equal to . + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); + + /// + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler); + /// + /// Message content is too long, length must be less or equal to . + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler); + /// + public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) + => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); + /// + public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) + => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, 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 + /// + /// Gets a user in this channel from the provided . + /// + /// The snowflake identifier of the user. + /// + /// A object that is a recipient of this channel; otherwise null. + /// + public new SocketUser GetUser(ulong id) + { + if (id == Recipient.Id) + return Recipient; + else if (id == Discord.CurrentUser.Id) + return Discord.CurrentUser; + else + return null; + } + + /// + /// Returns the recipient user. + /// + 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, options); + /// + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, mode, options); + /// + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, mode, options); + /// + async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) + => await GetPinnedMessagesAsync(options).ConfigureAwait(false); + /// + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); + /// + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); + /// + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) + => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + + //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/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs new file mode 100644 index 0000000..26fcbe8 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -0,0 +1,319 @@ +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.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 +{ + /// + /// Represents a WebSocket-based private group channel. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketGroupChannel : SocketChannel, IGroupChannel, ISocketPrivateChannel, ISocketMessageChannel, ISocketAudioChannel + { + private readonly MessageCache _messages; + private readonly ConcurrentDictionary _voiceStates; + + private string _iconId; + private ConcurrentDictionary _users; + + /// + 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); + _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); + + /// Voice is not yet supported for group channels. + public Task ConnectAsync() + { + throw new NotSupportedException("Voice is not yet supported for group channels."); + } + + //Messages + /// + public SocketMessage GetCachedMessage(ulong id) + => _messages?.Get(id); + /// + /// Gets a message from this message channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The snowflake identifier of the message. + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous get operation for retrieving the message. The task result contains + /// the retrieved message; null if no message is found with the specified identifier. + /// + public async Task GetMessageAsync(ulong id, RequestOptions options = null) + { + IMessage msg = _messages?.Get(id); + if (msg == null) + msg = await ChannelHelper.GetMessageAsync(this, Discord, id, options).ConfigureAwait(false); + return msg; + } + + /// + /// Gets the last N messages from this message channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The numbers of message to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// + public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, CacheMode.AllowDownload, options); + /// + /// Gets a collection of messages in this channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The ID of the starting message to get the messages from. + /// The direction of the messages to be gotten from. + /// The numbers of message to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// + public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, CacheMode.AllowDownload, options); + /// + /// Gets a collection of messages in this channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The starting message to get the messages from. + /// The direction of the messages to be gotten from. + /// The numbers of message to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// + 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, 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, options); + + /// + /// Message content is too long, length must be less or equal to . + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); + + /// + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler); + /// + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler); + + /// + public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) + => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); + /// + public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) + => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, 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 + /// + /// Gets a user from this group. + /// + /// The snowflake identifier of the user. + /// + /// A WebSocket-based group user associated with the snowflake identifier. + /// + public new SocketGroupUser GetUser(ulong id) + { + if (_users.TryGetValue(id, out SocketGroupUser user)) + return user; + return null; + } + internal SocketGroupUser GetOrAddUser(UserModel model) + { + if (_users.TryGetValue(model.Id, out SocketGroupUser user)) + return user; + else + { + var privateUser = SocketGroupUser.Create(this, Discord.State, model); + privateUser.GlobalUser.AddRef(); + _users[privateUser.Id] = privateUser; + return privateUser; + } + } + internal SocketGroupUser RemoveUser(ulong id) + { + if (_users.TryRemove(id, out SocketGroupUser user)) + { + user.GlobalUser.RemoveRef(Discord); + return user; + } + 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) + { + if (_voiceStates.TryGetValue(id, out SocketVoiceState voiceState)) + return voiceState; + return null; + } + internal SocketVoiceState? RemoveVoiceState(ulong id) + { + if (_voiceStates.TryRemove(id, out SocketVoiceState voiceState)) + return voiceState; + return null; + } + + /// + /// Returns the name of the group. + /// + 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, options); + /// + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, mode, options); + /// + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, mode, options); + /// + async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) + => await GetPinnedMessagesAsync(options).ConfigureAwait(false); + + /// + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); + /// + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); + /// + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) + => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + + //IAudioChannel + /// + /// Connecting to a group channel is not supported. + Task IAudioChannel.ConnectAsync(bool selfDeaf, bool selfMute, bool external) { throw new NotSupportedException(); } + Task IAudioChannel.DisconnectAsync() { throw new NotSupportedException(); } + + //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/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs new file mode 100644 index 0000000..c65f3be --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -0,0 +1,246 @@ +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 +{ + /// + /// Represents a WebSocket-based guild channel. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketGuildChannel : SocketChannel, IGuildChannel + { + private ImmutableArray _overwrites; + + /// + /// Gets the guild associated with this channel. + /// + /// + /// A guild object that this channel belongs to. + /// + public SocketGuild Guild { get; } + /// + public string Name { get; private set; } + /// + public int Position { get; private set; } + + /// + public virtual IReadOnlyCollection PermissionOverwrites => _overwrites; + /// + /// Gets a collection of users that are able to view the channel. + /// + /// + /// A read-only collection of users that can access the channel (i.e. the users seen in the user list). + /// + public new virtual IReadOnlyCollection Users => ImmutableArray.Create(); + + 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.News: + return SocketNewsChannel.Create(guild, state, model); + case ChannelType.Text: + return SocketTextChannel.Create(guild, state, model); + case ChannelType.Voice: + return SocketVoiceChannel.Create(guild, state, model); + case ChannelType.Category: + return SocketCategoryChannel.Create(guild, state, model); + default: + return new SocketGuildChannel(guild.Discord, model.Id, guild); + } + } + /// + 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(overwrites[i].ToEntity()); + _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); + + /// + /// Gets the permission overwrite for a specific user. + /// + /// The user to get the overwrite from. + /// + /// An overwrite object for the targeted user; null if none is set. + /// + public virtual OverwritePermissions? GetPermissionOverwrite(IUser user) + { + for (int i = 0; i < _overwrites.Length; i++) + { + if (_overwrites[i].TargetId == user.Id) + return _overwrites[i].Permissions; + } + return null; + } + /// + /// Gets the permission overwrite for a specific role. + /// + /// The role to get the overwrite from. + /// + /// An overwrite object for the targeted role; null if none is set. + /// + public virtual OverwritePermissions? GetPermissionOverwrite(IRole role) + { + for (int i = 0; i < _overwrites.Length; i++) + { + if (_overwrites[i].TargetId == role.Id) + return _overwrites[i].Permissions; + } + return null; + } + + /// + /// Adds or updates the permission overwrite for the given user. + /// + /// The user to add the overwrite to. + /// The overwrite to add to the user. + /// The options to be used when sending the request. + /// + /// A task representing the asynchronous permission operation for adding the specified permissions to the channel. + /// + public virtual async Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null) + { + await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, user, permissions, options).ConfigureAwait(false); + _overwrites = _overwrites.Add(new Overwrite(user.Id, PermissionTarget.User, new OverwritePermissions(permissions.AllowValue, permissions.DenyValue))); + } + + /// + /// Adds or updates the permission overwrite for the given role. + /// + /// The role to add the overwrite to. + /// The overwrite to add to the role. + /// The options to be used when sending the request. + /// + /// A task representing the asynchronous permission operation for adding the specified permissions to the channel. + /// + public virtual async Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) + { + await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, role, permissions, options).ConfigureAwait(false); + _overwrites = _overwrites.Add(new Overwrite(role.Id, PermissionTarget.Role, new OverwritePermissions(permissions.AllowValue, permissions.DenyValue))); + } + /// + /// Removes the permission overwrite for the given user, if one exists. + /// + /// The user to remove the overwrite from. + /// The options to be used when sending the request. + /// + /// A task representing the asynchronous operation for removing the specified permissions from the channel. + /// + public virtual 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; + } + } + } + /// + /// Removes the permission overwrite for the given role, if one exists. + /// + /// The role to remove the overwrite from. + /// The options to be used when sending the request. + /// + /// A task representing the asynchronous operation for removing the specified permissions from the channel. + /// + public virtual 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 new virtual SocketGuildUser GetUser(ulong id) => null; + + /// + /// Gets the name of the channel. + /// + /// + /// A string that resolves to . + /// + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id}, Guild)"; + internal new SocketGuildChannel Clone() => MemberwiseClone() as SocketGuildChannel; + + //SocketChannel + /// + internal override IReadOnlyCollection GetUsersInternal() => Users; + /// + internal override SocketUser GetUserInternal(ulong id) => GetUser(id); + + //IGuildChannel + /// + IGuild IGuildChannel.Guild => Guild; + /// + ulong IGuildChannel.GuildId => Guild.Id; + + /// + 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(); //Overridden in Text/Voice + /// + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetUser(id)); //Overridden in Text/Voice + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs new file mode 100644 index 0000000..815a99c --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord.WebSocket +{ + /// + /// Represents a WebSocket-based news channel in a guild that has the same properties as a . + /// + /// + /// + /// The property is not supported for news channels. + /// + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketNewsChannel : SocketTextChannel + { + internal SocketNewsChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) + :base(discord, id, guild) + { + } + internal new static SocketNewsChannel Create(SocketGuild guild, ClientState state, Model model) + { + var entity = new SocketNewsChannel(guild.Discord, model.Id, guild); + entity.Update(state, model); + return entity; + } + /// + /// + /// + /// This property is not supported by this type. Attempting to use this property will result in a . + /// + /// + public override int SlowModeInterval + => throw new NotSupportedException("News channels do not support Slow Mode."); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs new file mode 100644 index 0000000..ca7ca11 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -0,0 +1,319 @@ +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 +{ + /// + /// Represents a WebSocket-based channel in a guild that can send and receive messages. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketTextChannel : SocketGuildChannel, ITextChannel, ISocketMessageChannel + { + private readonly MessageCache _messages; + + /// + public string Topic { get; private set; } + /// + public virtual int SlowModeInterval { get; private set; } + /// + public ulong? CategoryId { get; private set; } + /// + /// Gets the parent (category) of this channel in the guild's channel list. + /// + /// + /// An representing the parent of this channel; null if none is set. + /// + public ICategoryChannel Category + => CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null; + /// + public virtual Task SyncPermissionsAsync(RequestOptions options = null) + => ChannelHelper.SyncPermissionsAsync(this, Discord, options); + + private bool _nsfw; + /// + public bool IsNsfw => _nsfw; + + /// + 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.ViewChannel)).ToImmutableArray(); + + internal SocketTextChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) + : base(discord, id, guild) + { + if (Discord.MessageCacheSize > 0) + _messages = new MessageCache(Discord); + } + 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); + CategoryId = model.CategoryId; + Topic = model.Topic.Value; + SlowModeInterval = model.SlowMode.GetValueOrDefault(); // some guilds haven't been patched to include this yet? + _nsfw = model.Nsfw.GetValueOrDefault(); + } + + /// + public Task ModifyAsync(Action func, RequestOptions options = null) + => ChannelHelper.ModifyAsync(this, Discord, func, options); + + //Messages + /// + public SocketMessage GetCachedMessage(ulong id) + => _messages?.Get(id); + /// + /// Gets a message from this message channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The snowflake identifier of the message. + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous get operation for retrieving the message. The task result contains + /// the retrieved message; null if no message is found with the specified identifier. + /// + public async Task GetMessageAsync(ulong id, RequestOptions options = null) + { + IMessage msg = _messages?.Get(id); + if (msg == null) + msg = await ChannelHelper.GetMessageAsync(this, Discord, id, options).ConfigureAwait(false); + return msg; + } + + /// + /// Gets the last N messages from this message channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The numbers of message to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// + public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, CacheMode.AllowDownload, options); + /// + /// Gets a collection of messages in this channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The ID of the starting message to get the messages from. + /// The direction of the messages to be gotten from. + /// The numbers of message to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// + public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, CacheMode.AllowDownload, options); + /// + /// Gets a collection of messages in this channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The starting message to get the messages from. + /// The direction of the messages to be gotten from. + /// The numbers of message to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// + 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, 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, options); + + /// + /// Message content is too long, length must be less or equal to . + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); + + /// + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler); + + /// + /// Message content is too long, length must be less or equal to . + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler); + + /// + public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + /// + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); + + /// + public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) + => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); + /// + public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) + => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, 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.ViewChannel)) + return user; + } + return null; + } + + //Webhooks + /// + /// Creates a webhook in this text channel. + /// + /// The name of the webhook. + /// The avatar of the webhook. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// webhook. + /// + public Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) + => ChannelHelper.CreateWebhookAsync(this, Discord, name, avatar, options); + /// + /// Gets a webhook available in this text channel. + /// + /// The identifier of the webhook. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a webhook associated + /// with the identifier; null if the webhook is not found. + /// + public Task GetWebhookAsync(ulong id, RequestOptions options = null) + => ChannelHelper.GetWebhookAsync(this, Discord, id, options); + /// + /// Gets the webhooks available in this text channel. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of webhooks that is available in this channel. + /// + public Task> GetWebhooksAsync(RequestOptions options = null) + => ChannelHelper.GetWebhooksAsync(this, Discord, options); + + //Invites + /// + public async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); + /// + public async Task> GetInvitesAsync(RequestOptions options = null) + => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); + + private string DebuggerDisplay => $"{Name} ({Id}, Text)"; + internal new SocketTextChannel Clone() => MemberwiseClone() as SocketTextChannel; + + //ITextChannel + /// + async Task ITextChannel.CreateWebhookAsync(string name, Stream avatar, RequestOptions options) + => await CreateWebhookAsync(name, avatar, options).ConfigureAwait(false); + /// + async Task ITextChannel.GetWebhookAsync(ulong id, RequestOptions options) + => await GetWebhookAsync(id, options).ConfigureAwait(false); + /// + async Task> ITextChannel.GetWebhooksAsync(RequestOptions options) + => await GetWebhooksAsync(options).ConfigureAwait(false); + + //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, options); + /// + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, mode, options); + /// + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, mode, options); + /// + async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) + => await GetPinnedMessagesAsync(options).ConfigureAwait(false); + + /// + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); + /// + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); + /// + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) + => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + + // INestedChannel + /// + Task INestedChannel.GetCategoryAsync(CacheMode mode, RequestOptions options) + => Task.FromResult(Category); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs new file mode 100644 index 0000000..9fff6c2 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs @@ -0,0 +1,107 @@ +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 +{ + /// + /// Represents a WebSocket-based voice channel in a guild. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketVoiceChannel : SocketGuildChannel, IVoiceChannel, ISocketAudioChannel + { + /// + public int Bitrate { get; private set; } + /// + public int? UserLimit { get; private set; } + /// + public ulong? CategoryId { get; private set; } + /// + /// Gets the parent (category) channel of this channel. + /// + /// + /// A category channel representing the parent of this channel; null if none is set. + /// + public ICategoryChannel Category + => CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null; + /// + public Task SyncPermissionsAsync(RequestOptions options = null) + => ChannelHelper.SyncPermissionsAsync(this, Discord, options); + + /// + 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); + CategoryId = model.CategoryId; + Bitrate = model.Bitrate.Value; + UserLimit = model.UserLimit.Value != 0 ? model.UserLimit.Value : (int?)null; + } + + /// + public Task ModifyAsync(Action func, RequestOptions options = null) + => ChannelHelper.ModifyAsync(this, Discord, func, options); + + /// + public async Task ConnectAsync(bool selfDeaf = false, bool selfMute = false, bool external = false) + { + return await Guild.ConnectAudioAsync(Id, selfDeaf, selfMute, external).ConfigureAwait(false); + } + + /// + public async Task DisconnectAsync() + => await Guild.DisconnectAudioAsync(); + + /// + public override SocketGuildUser GetUser(ulong id) + { + var user = Guild.GetUser(id); + if (user?.VoiceChannel?.Id == Id) + return user; + return null; + } + + //Invites + /// + public async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); + /// + public async Task> GetInvitesAsync(RequestOptions options = null) + => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); + + private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; + internal new SocketVoiceChannel Clone() => MemberwiseClone() as SocketVoiceChannel; + + //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(); + + // INestedChannel + /// + Task INestedChannel.GetCategoryAsync(CacheMode mode, RequestOptions options) + => Task.FromResult(Category); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs new file mode 100644 index 0000000..da9a316 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -0,0 +1,1205 @@ +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.Globalization; +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 UserModel = Discord.API.User; +using VoiceStateModel = Discord.API.VoiceState; + +namespace Discord.WebSocket +{ + /// + /// Represents a WebSocket-based guild object. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketGuild : SocketEntity, IGuild, IDisposable + { +#pragma warning disable IDISP002, IDISP006 + 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 _emotes; + private ImmutableArray _features; + private AudioClient _audioClient; +#pragma warning restore IDISP002, IDISP006 + + /// + 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 ExplicitContentFilterLevel ExplicitContentFilter { get; private set; } + /// + /// Gets the number of members. + /// + /// + /// This property retrieves the number of members returned by Discord. + /// + /// + /// Due to how this property is returned by Discord instead of relying on the WebSocket cache, the + /// number here is the most accurate in terms of counting the number of users within this guild. + /// + /// + /// Use this instead of enumerating the count of the + /// collection, as you may see discrepancy + /// between that and this property. + /// + /// + /// + public int MemberCount { get; internal set; } + /// Gets the number of members downloaded to the local guild cache. + public int DownloadedMemberCount { get; private set; } + internal bool IsAvailable { get; private set; } + /// Indicates whether the client is connected to this guild. + public bool IsConnected { get; internal set; } + /// + public ulong? ApplicationId { get; internal set; } + + internal ulong? AFKChannelId { get; private set; } + internal ulong? EmbedChannelId { get; private set; } + internal ulong? SystemChannelId { get; private set; } + /// + public ulong OwnerId { get; private set; } + /// Gets the user that owns this guild. + public SocketGuildUser Owner => GetUser(OwnerId); + /// + public string VoiceRegionId { get; private set; } + /// + public string IconId { get; private set; } + /// + public string SplashId { get; private set; } + /// + public PremiumTier PremiumTier { get; private set; } + /// + public string BannerId { get; private set; } + /// + public string VanityURLCode { get; private set; } + /// + public SystemChannelMessageDeny SystemChannelFlags { get; private set; } + /// + public string Description { get; private set; } + /// + public int PremiumSubscriptionCount { get; private set; } + /// + public string PreferredLocale { get; private set; } + + /// + public CultureInfo PreferredCulture { get; private set; } + + /// + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + /// + public string IconUrl => CDN.GetGuildIconUrl(Id, IconId); + /// + public string SplashUrl => CDN.GetGuildSplashUrl(Id, SplashId); + /// + public string BannerUrl => CDN.GetGuildBannerUrl(Id, BannerId); + /// Indicates whether the client has all the members downloaded to the local guild cache. + public bool HasAllMembers => MemberCount == DownloadedMemberCount;// _downloaderPromise.Task.IsCompleted; + /// Indicates whether the guild cache is synced to this guild. + public bool IsSynced => _syncPromise.Task.IsCompleted; + public Task SyncPromise => _syncPromise.Task; + public Task DownloaderPromise => _downloaderPromise.Task; + /// + /// Gets the associated with this guild. + /// + public IAudioClient AudioClient => _audioClient; + /// + /// Gets the default channel in this guild. + /// + /// + /// This property retrieves the first viewable text channel for this guild. + /// + /// This channel does not guarantee the user can send message to it, as it only looks for the first viewable + /// text channel. + /// + /// + /// + /// A representing the first viewable channel that the user has access to. + /// + public SocketTextChannel DefaultChannel => TextChannels + .Where(c => CurrentUser.GetPermissions(c).ViewChannel) + .OrderBy(c => c.Position) + .FirstOrDefault(); + /// + /// Gets the AFK voice channel in this guild. + /// + /// + /// A that the AFK users will be moved to after they have idled for too + /// long; null if none is set. + /// + public SocketVoiceChannel AFKChannel + { + get + { + var id = AFKChannelId; + return id.HasValue ? GetVoiceChannel(id.Value) : null; + } + } + /// + /// Gets the embed channel (i.e. the channel set in the guild's widget settings) in this guild. + /// + /// + /// A channel set within the server's widget settings; null if none is set. + /// + public SocketGuildChannel EmbedChannel + { + get + { + var id = EmbedChannelId; + return id.HasValue ? GetChannel(id.Value) : null; + } + } + /// + /// Gets the system channel where randomized welcome messages are sent in this guild. + /// + /// + /// A text channel where randomized welcome messages will be sent to; null if none is set. + /// + public SocketTextChannel SystemChannel + { + get + { + var id = SystemChannelId; + return id.HasValue ? GetTextChannel(id.Value) : null; + } + } + /// + /// Gets a collection of all text channels in this guild. + /// + /// + /// A read-only collection of message channels found within this guild. + /// + public IReadOnlyCollection TextChannels + => Channels.OfType().ToImmutableArray(); + /// + /// Gets a collection of all voice channels in this guild. + /// + /// + /// A read-only collection of voice channels found within this guild. + /// + public IReadOnlyCollection VoiceChannels + => Channels.OfType().ToImmutableArray(); + /// + /// Gets a collection of all category channels in this guild. + /// + /// + /// A read-only collection of category channels found within this guild. + /// + public IReadOnlyCollection CategoryChannels + => Channels.OfType().ToImmutableArray(); + /// + /// Gets the current logged-in user. + /// + public SocketGuildUser CurrentUser => _members.TryGetValue(Discord.CurrentUser.Id, out SocketGuildUser member) ? member : null; + /// + /// Gets the built-in role containing all users in this guild. + /// + /// + /// A role object that represents an @everyone role in this guild. + /// + public SocketRole EveryoneRole => GetRole(Id); + /// + /// Gets a collection of all channels in this guild. + /// + /// + /// A read-only collection of generic channels found within this guild. + /// + 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 Emotes => _emotes; + /// + public IReadOnlyCollection Features => _features; + /// + /// Gets a collection of users in this guild. + /// + /// + /// This property retrieves all users found within this guild. + /// + /// + /// This property may not always return all the members for large guilds (i.e. guilds containing + /// 100+ users). If you are simply looking to get the number of users present in this guild, + /// consider using instead. + /// + /// + /// Otherwise, you may need to enable to fetch + /// the full user list upon startup, or use to manually download + /// the users. + /// + /// + /// + /// + /// A collection of guild users found within this guild. + /// + public IReadOnlyCollection Users => _members.ToReadOnlyCollection(); + /// + /// Gets a collection of all roles in this guild. + /// + /// + /// A read-only collection of roles found within this guild. + /// + public IReadOnlyCollection Roles => _roles.ToReadOnlyCollection(); + + internal SocketGuild(DiscordSocketClient client, ulong id) + : base(client, id) + { + _audioLock = new SemaphoreSlim(1, 1); + _emotes = 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) + { + IsAvailable = !(model.Unavailable ?? false); + if (!IsAvailable) + { + 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();*/ + _syncPromise = new TaskCompletionSource(); + _downloaderPromise = new TaskCompletionSource(); + 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++) + { + if (members.TryGetValue(model.Presences[i].User.Id, out SocketGuildUser member)) + member.Update(state, model.Presences[i], true); + } + } + _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(); + var _ = _syncPromise.TrySetResultAsync(true); + /*if (!model.Large) + _ = _downloaderPromise.TrySetResultAsync(true);*/ + } + internal void Update(ClientState state, Model model) + { + AFKChannelId = model.AFKChannelId; + EmbedChannelId = model.EmbedChannelId; + SystemChannelId = model.SystemChannelId; + 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; + ExplicitContentFilter = model.ExplicitContentFilter; + ApplicationId = model.ApplicationId; + PremiumTier = model.PremiumTier; + VanityURLCode = model.VanityURLCode; + BannerId = model.Banner; + SystemChannelFlags = model.SystemChannelFlags; + Description = model.Description; + PremiumSubscriptionCount = model.PremiumSubscriptionCount.GetValueOrDefault(); + PreferredLocale = model.PreferredLocale; + PreferredCulture = new CultureInfo(PreferredLocale); + + if (model.Emojis != null) + { + var emojis = ImmutableArray.CreateBuilder(model.Emojis.Length); + for (int i = 0; i < model.Emojis.Length; i++) + emojis.Add(model.Emojis[i].ToEntity()); + _emotes = emojis.ToImmutable(); + } + else + _emotes = 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++) + { + if (members.TryGetValue(model.Presences[i].User.Id, out SocketGuildUser member)) + member.Update(state, model.Presences[i], true); + } + } + _members = members; + + var _ = _syncPromise.TrySetResultAsync(true); + /*if (!model.Large) + _ = _downloaderPromise.TrySetResultAsync(true);*/ + } + + internal void Update(ClientState state, EmojiUpdateModel model) + { + var emotes = ImmutableArray.CreateBuilder(model.Emojis.Length); + for (int i = 0; i < model.Emojis.Length; i++) + emotes.Add(model.Emojis[i].ToEntity()); + _emotes = emotes.ToImmutable(); + } + + //General + /// + public Task DeleteAsync(RequestOptions options = null) + => GuildHelper.DeleteAsync(this, Discord, options); + + /// + /// is null. + public Task ModifyAsync(Action func, RequestOptions options = null) + => GuildHelper.ModifyAsync(this, Discord, func, options); + + /// + /// is null. + public Task ModifyEmbedAsync(Action func, RequestOptions options = null) + => GuildHelper.ModifyEmbedAsync(this, Discord, func, options); + /// + public Task ReorderChannelsAsync(IEnumerable args, RequestOptions options = null) + => GuildHelper.ReorderChannelsAsync(this, Discord, args, options); + /// + public Task ReorderRolesAsync(IEnumerable args, RequestOptions options = null) + => GuildHelper.ReorderRolesAsync(this, Discord, args, options); + + /// + public Task LeaveAsync(RequestOptions options = null) + => GuildHelper.LeaveAsync(this, Discord, options); + + //Bans + /// + /// Gets a collection of all users banned in this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// ban objects that this guild currently possesses, with each object containing the user banned and reason + /// behind the ban. + /// + public Task> GetBansAsync(RequestOptions options = null) + => GuildHelper.GetBansAsync(this, Discord, options); + /// + /// Gets a ban object for a banned user. + /// + /// The banned user. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a ban object, which + /// contains the user information and the reason for the ban; null if the ban entry cannot be found. + /// + public Task GetBanAsync(IUser user, RequestOptions options = null) + => GuildHelper.GetBanAsync(this, Discord, user.Id, options); + /// + /// Gets a ban object for a banned user. + /// + /// The snowflake identifier for the banned user. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a ban object, which + /// contains the user information and the reason for the ban; null if the ban entry cannot be found. + /// + public Task GetBanAsync(ulong userId, RequestOptions options = null) + => GuildHelper.GetBanAsync(this, Discord, userId, options); + + /// + public Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null) + => GuildHelper.AddBanAsync(this, Discord, user.Id, pruneDays, reason, options); + /// + public Task AddBanAsync(ulong userId, int pruneDays = 0, string reason = null, RequestOptions options = null) + => GuildHelper.AddBanAsync(this, Discord, userId, pruneDays, reason, 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 + /// + /// Gets a channel in this guild. + /// + /// The snowflake identifier for the channel. + /// + /// A generic channel associated with the specified ; null if none is found. + /// + public SocketGuildChannel GetChannel(ulong id) + { + var channel = Discord.State.GetChannel(id) as SocketGuildChannel; + if (channel?.Guild.Id == Id) + return channel; + return null; + } + /// + /// Gets a text channel in this guild. + /// + /// The snowflake identifier for the text channel. + /// + /// A text channel associated with the specified ; null if none is found. + /// + public SocketTextChannel GetTextChannel(ulong id) + => GetChannel(id) as SocketTextChannel; + /// + /// Gets a voice channel in this guild. + /// + /// The snowflake identifier for the voice channel. + /// + /// A voice channel associated with the specified ; null if none is found. + /// + public SocketVoiceChannel GetVoiceChannel(ulong id) + => GetChannel(id) as SocketVoiceChannel; + /// + /// Gets a category channel in this guild. + /// + /// The snowflake identifier for the category channel. + /// + /// A category channel associated with the specified ; null if none is found. + /// + public SocketCategoryChannel GetCategoryChannel(ulong id) + => GetChannel(id) as SocketCategoryChannel; + + /// + /// Creates a new text channel in this guild. + /// + /// + /// The following example creates a new text channel under an existing category named Wumpus with a set topic. + /// + /// var categories = await guild.GetCategoriesAsync(); + /// var targetCategory = categories.FirstOrDefault(x => x.Name == "wumpus"); + /// if (targetCategory == null) return; + /// await Context.Guild.CreateTextChannelAsync(name, x => + /// { + /// x.CategoryId = targetCategory.Id; + /// x.Topic = $"This channel was created at {DateTimeOffset.UtcNow} by {user}."; + /// }); + /// + /// + /// The new name for the text channel. + /// The delegate containing the properties to be applied to the channel upon its creation. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// text channel. + /// + public Task CreateTextChannelAsync(string name, Action func = null, RequestOptions options = null) + => GuildHelper.CreateTextChannelAsync(this, Discord, name, options, func); + /// + /// Creates a new voice channel in this guild. + /// + /// The new name for the voice channel. + /// The delegate containing the properties to be applied to the channel upon its creation. + /// The options to be used when sending the request. + /// is null. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// voice channel. + /// + public Task CreateVoiceChannelAsync(string name, Action func = null, RequestOptions options = null) + => GuildHelper.CreateVoiceChannelAsync(this, Discord, name, options, func); + /// + /// Creates a new channel category in this guild. + /// + /// The new name for the category. + /// The delegate containing the properties to be applied to the channel upon its creation. + /// The options to be used when sending the request. + /// is null. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// category channel. + /// + public Task CreateCategoryChannelAsync(string name, Action func = null, RequestOptions options = null) + => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options, func); + + 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; + } + + //Voice Regions + /// + /// Gets a collection of all the voice regions this guild can access. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// voice regions the guild can access. + /// + public Task> GetVoiceRegionsAsync(RequestOptions options = null) + => GuildHelper.GetVoiceRegionsAsync(this, Discord, 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 + /// + /// Gets a collection of all invites in this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// invite metadata, each representing information for an invite found within this guild. + /// + public Task> GetInvitesAsync(RequestOptions options = null) + => GuildHelper.GetInvitesAsync(this, Discord, options); + /// + /// Gets the vanity invite URL of this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the partial metadata of + /// the vanity invite found within this guild; null if none is found. + /// + public Task GetVanityInviteAsync(RequestOptions options = null) + => GuildHelper.GetVanityInviteAsync(this, Discord, options); + + //Roles + /// + /// Gets a role in this guild. + /// + /// The snowflake identifier for the role. + /// + /// A role that is associated with the specified ; null if none is found. + /// + public SocketRole GetRole(ulong id) + { + if (_roles.TryGetValue(id, out SocketRole 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, false, options); + /// + /// Creates a new role with the provided name. + /// + /// The new name for the role. + /// The guild permission that the role should possess. + /// The color of the role. + /// Whether the role is separated from others on the sidebar. + /// Whether the role can be mentioned. + /// The options to be used when sending the request. + /// is null. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// role. + /// + public Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), + bool isHoisted = false, bool isMentionable = false, RequestOptions options = null) + => GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, isMentionable, 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) + { + if (_roles.TryRemove(id, out SocketRole role)) + return role; + return null; + } + + //Users + /// + public Task AddGuildUserAsync(ulong id, string accessToken, Action func = null, RequestOptions options = null) + => GuildHelper.AddGuildUserAsync(this, Discord, id, accessToken, func, options); + + /// + /// Gets a user from this guild. + /// + /// + /// This method retrieves a user found within this guild. + /// + /// This may return null in the WebSocket implementation due to incomplete user collection in + /// large guilds. + /// + /// + /// The snowflake identifier of the user. + /// + /// A guild user associated with the specified ; null if none is found. + /// + public SocketGuildUser GetUser(ulong id) + { + if (_members.TryGetValue(id, out SocketGuildUser 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(UserModel model) + { + if (_members.TryGetValue(model.Id, out SocketGuildUser member)) + member.GlobalUser?.Update(Discord.State, model); + else + { + member = SocketGuildUser.Create(this, Discord.State, model); + member.GlobalUser.AddRef(); + _members[member.Id] = member; + DownloadedMemberCount++; + } + return member; + } + internal SocketGuildUser AddOrUpdateUser(MemberModel model) + { + if (_members.TryGetValue(model.User.Id, out SocketGuildUser member)) + member.Update(Discord.State, model); + else + { + member = SocketGuildUser.Create(this, Discord.State, model); + if (member == null) + throw new InvalidOperationException("SocketGuildUser.Create failed to produce a member"); // TODO 2.2rel: delete this + if (member.GlobalUser == null) + throw new InvalidOperationException("Member was created without global user"); // TODO 2.2rel: delete this + member.GlobalUser.AddRef(); + _members[member.Id] = member; + DownloadedMemberCount++; + } + if (member == null) + throw new InvalidOperationException("AddOrUpdateUser failed to produce a user"); // TODO 2.2rel: delete this + return member; + } + internal SocketGuildUser AddOrUpdateUser(PresenceModel model) + { + if (_members.TryGetValue(model.User.Id, out SocketGuildUser member)) + member.Update(Discord.State, model, false); + else + { + member = SocketGuildUser.Create(this, Discord.State, model); + member.GlobalUser.AddRef(); + _members[member.Id] = member; + DownloadedMemberCount++; + } + return member; + } + internal SocketGuildUser RemoveUser(ulong id) + { + if (_members.TryRemove(id, out SocketGuildUser member)) + { + DownloadedMemberCount--; + member.GlobalUser.RemoveRef(Discord); + return member; + } + return null; + } + + /// + public async Task DownloadUsersAsync() + { + await Discord.DownloadUsersAsync(new[] { this }).ConfigureAwait(false); + } + internal void CompleteDownloadUsers() + { + _downloaderPromise.TrySetResultAsync(true); + } + + //Audit logs + /// + /// Gets the specified number of audit log entries for this guild. + /// + /// The number of audit log entries to fetch. + /// The options to be used when sending the request. + /// The audit log entry ID to filter entries before. + /// The type of actions to filter. + /// The user ID to filter entries for. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of the requested audit log entries. + /// + public IAsyncEnumerable> GetAuditLogsAsync(int limit, RequestOptions options = null, ulong? beforeId = null, ulong? userId = null, ActionType? actionType = null) + => GuildHelper.GetAuditLogsAsync(this, Discord, beforeId, limit, options, userId: userId, actionType: actionType); + + //Webhooks + /// + /// Gets a webhook found within this guild. + /// + /// The identifier for the webhook. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the webhook with the + /// specified ; null if none is found. + /// + public Task GetWebhookAsync(ulong id, RequestOptions options = null) + => GuildHelper.GetWebhookAsync(this, Discord, id, options); + /// + /// Gets a collection of all webhook from this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of webhooks found within the guild. + /// + public Task> GetWebhooksAsync(RequestOptions options = null) + => GuildHelper.GetWebhooksAsync(this, Discord, options); + + //Emotes + /// + public Task GetEmoteAsync(ulong id, RequestOptions options = null) + => GuildHelper.GetEmoteAsync(this, Discord, id, options); + /// + public Task CreateEmoteAsync(string name, Image image, Optional> roles = default(Optional>), RequestOptions options = null) + => GuildHelper.CreateEmoteAsync(this, Discord, name, image, roles, options); + /// + /// is null. + public Task ModifyEmoteAsync(GuildEmote emote, Action func, RequestOptions options = null) + => GuildHelper.ModifyEmoteAsync(this, Discord, emote.Id, func, options); + /// + public Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null) + => GuildHelper.DeleteEmoteAsync(this, Discord, emote.Id, options); + + //Voice States + internal async Task AddOrUpdateVoiceStateAsync(ClientState state, VoiceStateModel model) + { + var voiceChannel = state.GetChannel(model.ChannelId.Value) as SocketVoiceChannel; + var before = GetVoiceState(model.UserId) ?? SocketVoiceState.Default; + var after = SocketVoiceState.Create(voiceChannel, model); + _voiceStates[model.UserId] = after; + + if (_audioClient != null && before.VoiceChannel?.Id != after.VoiceChannel?.Id) + { + if (model.UserId == CurrentUser.Id) + { + if (after.VoiceChannel != null && _audioClient.ChannelId != after.VoiceChannel?.Id) + { + _audioClient.ChannelId = after.VoiceChannel.Id; + await RepopulateAudioStreamsAsync().ConfigureAwait(false); + } + } + else + { + await _audioClient.RemoveInputStreamAsync(model.UserId).ConfigureAwait(false); //User changed channels, end their stream + if (CurrentUser.VoiceChannel != null && after.VoiceChannel?.Id == CurrentUser.VoiceChannel?.Id) + await _audioClient.CreateInputStreamAsync(model.UserId).ConfigureAwait(false); + } + } + + return after; + } + internal SocketVoiceState? GetVoiceState(ulong id) + { + if (_voiceStates.TryGetValue(id, out SocketVoiceState voiceState)) + return voiceState; + return null; + } + internal async Task RemoveVoiceStateAsync(ulong id) + { + if (_voiceStates.TryRemove(id, out SocketVoiceState voiceState)) + { + if (_audioClient != null) + await _audioClient.RemoveInputStreamAsync(id).ConfigureAwait(false); //User changed channels, end their stream + return voiceState; + } + return null; + } + + //Audio + internal AudioInStream GetAudioStream(ulong userId) + { + return _audioClient?.GetInputStream(userId); + } + internal async Task ConnectAudioAsync(ulong channelId, bool selfDeaf, bool selfMute, bool external) + { + TaskCompletionSource promise; + + await _audioLock.WaitAsync().ConfigureAwait(false); + try + { + await DisconnectAudioInternalAsync().ConfigureAwait(false); + promise = new TaskCompletionSource(); + _audioConnectPromise = promise; + + if (external) + { +#pragma warning disable IDISP001 + var _ = promise.TrySetResultAsync(null); + await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, channelId, selfDeaf, selfMute).ConfigureAwait(false); + return null; +#pragma warning restore IDISP001 + } + + if (_audioClient == null) + { + var audioClient = new AudioClient(this, Discord.GetAudioId(), channelId); + audioClient.Disconnected += async ex => + { + if (!promise.Task.IsCompleted) + { + try + { audioClient.Dispose(); } + catch { } + _audioClient = null; + if (ex != null) + await promise.TrySetExceptionAsync(ex); + else + await promise.TrySetCanceledAsync(); + return; + } + }; + audioClient.Connected += () => + { +#pragma warning disable IDISP001 + var _ = promise.TrySetResultAsync(_audioClient); +#pragma warning restore IDISP001 + return Task.Delay(0); + }; +#pragma warning disable IDISP003 + _audioClient = audioClient; +#pragma warning restore IDISP003 + } + + await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, channelId, selfDeaf, selfMute).ConfigureAwait(false); + } + catch + { + await DisconnectAudioInternalAsync().ConfigureAwait(false); + throw; + } + finally + { + _audioLock.Release(); + } + + try + { + var timeoutTask = Task.Delay(15000); + if (await Task.WhenAny(promise.Task, timeoutTask).ConfigureAwait(false) == timeoutTask) + throw new TimeoutException(); + return await promise.Task.ConfigureAwait(false); + } + catch + { + await DisconnectAudioAsync().ConfigureAwait(false); + throw; + } + } + + internal async Task DisconnectAudioAsync() + { + await _audioLock.WaitAsync().ConfigureAwait(false); + try + { + await DisconnectAudioInternalAsync().ConfigureAwait(false); + } + finally + { + _audioLock.Release(); + } + } + private async Task DisconnectAudioInternalAsync() + { + _audioConnectPromise?.TrySetCanceledAsync(); //Cancel any previous audio connection + _audioConnectPromise = null; + if (_audioClient != null) + await _audioClient.StopAsync().ConfigureAwait(false); + await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, null, false, false).ConfigureAwait(false); + _audioClient?.Dispose(); + _audioClient = null; + } + internal async Task FinishConnectAudio(string url, string token) + { + //TODO: Mem Leak: Disconnected/Connected handlers arent cleaned up + var voiceState = GetVoiceState(Discord.CurrentUser.Id).Value; + + await _audioLock.WaitAsync().ConfigureAwait(false); + try + { + if (_audioClient != null) + { + await RepopulateAudioStreamsAsync().ConfigureAwait(false); + await _audioClient.StartAsync(url, Discord.CurrentUser.Id, voiceState.VoiceSessionId, token).ConfigureAwait(false); + } + } + catch (OperationCanceledException) + { + await DisconnectAudioInternalAsync().ConfigureAwait(false); + } + catch (Exception e) + { + await _audioConnectPromise.SetExceptionAsync(e).ConfigureAwait(false); + await DisconnectAudioInternalAsync().ConfigureAwait(false); + } + finally + { + _audioLock.Release(); + } + } + + internal async Task RepopulateAudioStreamsAsync() + { + await _audioClient.ClearInputStreamsAsync().ConfigureAwait(false); //We changed channels, end all current streams + if (CurrentUser.VoiceChannel != null) + { + foreach (var pair in _voiceStates) + { + if (pair.Value.VoiceChannel?.Id == CurrentUser.VoiceChannel?.Id && pair.Key != CurrentUser.Id) + await _audioClient.CreateInputStreamAsync(pair.Key).ConfigureAwait(false); + } + } + } + + /// + /// Gets the name of the guild. + /// + /// + /// A string that resolves to . + /// + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id})"; + internal SocketGuild Clone() => MemberwiseClone() as SocketGuild; + + //IGuild + /// + ulong? IGuild.AFKChannelId => AFKChannelId; + /// + IAudioClient IGuild.AudioClient => null; + /// + bool IGuild.Available => true; + /// + ulong IGuild.DefaultChannelId => DefaultChannel?.Id ?? 0; + /// + ulong? IGuild.EmbedChannelId => EmbedChannelId; + /// + ulong? IGuild.SystemChannelId => SystemChannelId; + /// + IRole IGuild.EveryoneRole => EveryoneRole; + /// + IReadOnlyCollection IGuild.Roles => Roles; + + /// + async Task> IGuild.GetBansAsync(RequestOptions options) + => await GetBansAsync(options).ConfigureAwait(false); + /// + async Task IGuild.GetBanAsync(IUser user, RequestOptions options) + => await GetBanAsync(user, options).ConfigureAwait(false); + /// + async Task IGuild.GetBanAsync(ulong userId, RequestOptions options) + => await GetBanAsync(userId, 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)); + /// + Task> IGuild.GetTextChannelsAsync(CacheMode mode, RequestOptions options) + => Task.FromResult>(TextChannels); + /// + Task IGuild.GetTextChannelAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetTextChannel(id)); + /// + Task> IGuild.GetVoiceChannelsAsync(CacheMode mode, RequestOptions options) + => Task.FromResult>(VoiceChannels); + /// + Task> IGuild.GetCategoriesAsync(CacheMode mode , RequestOptions options) + => Task.FromResult>(CategoryChannels); + /// + Task IGuild.GetVoiceChannelAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetVoiceChannel(id)); + /// + Task IGuild.GetAFKChannelAsync(CacheMode mode, RequestOptions options) + => Task.FromResult(AFKChannel); + /// + Task IGuild.GetDefaultChannelAsync(CacheMode mode, RequestOptions options) + => Task.FromResult(DefaultChannel); + /// + Task IGuild.GetEmbedChannelAsync(CacheMode mode, RequestOptions options) + => Task.FromResult(EmbedChannel); + /// + Task IGuild.GetSystemChannelAsync(CacheMode mode, RequestOptions options) + => Task.FromResult(SystemChannel); + /// + async Task IGuild.CreateTextChannelAsync(string name, Action func, RequestOptions options) + => await CreateTextChannelAsync(name, func, options).ConfigureAwait(false); + /// + async Task IGuild.CreateVoiceChannelAsync(string name, Action func, RequestOptions options) + => await CreateVoiceChannelAsync(name, func, options).ConfigureAwait(false); + /// + async Task IGuild.CreateCategoryAsync(string name, Action func, RequestOptions options) + => await CreateCategoryChannelAsync(name, func, options).ConfigureAwait(false); + + /// + async Task> IGuild.GetVoiceRegionsAsync(RequestOptions options) + => await GetVoiceRegionsAsync(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); + /// + async Task IGuild.GetVanityInviteAsync(RequestOptions options) + => await GetVanityInviteAsync(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, false, options).ConfigureAwait(false); + /// + async Task IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted, bool isMentionable, RequestOptions options) + => await CreateRoleAsync(name, permissions, color, isHoisted, isMentionable, options).ConfigureAwait(false); + + /// + Task> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options) + => Task.FromResult>(Users); + + /// + async Task IGuild.AddGuildUserAsync(ulong userId, string accessToken, Action func, RequestOptions options) + => await AddGuildUserAsync(userId, accessToken, func, options); + /// + Task IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetUser(id)); + /// + Task IGuild.GetCurrentUserAsync(CacheMode mode, RequestOptions options) + => Task.FromResult(CurrentUser); + /// + Task IGuild.GetOwnerAsync(CacheMode mode, RequestOptions options) + => Task.FromResult(Owner); + + /// + async Task> IGuild.GetAuditLogsAsync(int limit, CacheMode cacheMode, RequestOptions options, + ulong? beforeId, ulong? userId, ActionType? actionType) + { + if (cacheMode == CacheMode.AllowDownload) + return (await GetAuditLogsAsync(limit, options, beforeId: beforeId, userId: userId, actionType: actionType).FlattenAsync().ConfigureAwait(false)).ToImmutableArray(); + else + return ImmutableArray.Create(); + } + + /// + async Task IGuild.GetWebhookAsync(ulong id, RequestOptions options) + => await GetWebhookAsync(id, options).ConfigureAwait(false); + /// + async Task> IGuild.GetWebhooksAsync(RequestOptions options) + => await GetWebhooksAsync(options).ConfigureAwait(false); + + void IDisposable.Dispose() + { + DisconnectAudioAsync().GetAwaiter().GetResult(); + _audioLock?.Dispose(); + _audioClient?.Dispose(); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs new file mode 100644 index 0000000..24e46df --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Discord.WebSocket +{ + internal class MessageCache + { + private readonly ConcurrentDictionary _messages; + private readonly ConcurrentQueue _orderedMessages; + private readonly int _size; + + public IReadOnlyCollection Messages => _messages.ToReadOnlyCollection(); + + public MessageCache(DiscordSocketClient discord) + { + _size = discord.MessageCacheSize; + _messages = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(_size * 1.05)); + _orderedMessages = new ConcurrentQueue(); + } + + public void Add(SocketMessage message) + { + if (_messages.TryAdd(message.Id, message)) + { + _orderedMessages.Enqueue(message.Id); + + while (_orderedMessages.Count > _size && _orderedMessages.TryDequeue(out ulong msgId)) + _messages.TryRemove(msgId, out _); + } + } + + public SocketMessage Remove(ulong id) + { + _messages.TryRemove(id, out SocketMessage msg); + return msg; + } + + public SocketMessage Get(ulong id) + { + if (_messages.TryGetValue(id, out SocketMessage result)) + return result; + return null; + } + + /// is less than 0. + 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; + + IEnumerable cachedMessageIds; + if (fromMessageId == null) + cachedMessageIds = _orderedMessages; + else if (dir == Direction.Before) + cachedMessageIds = _orderedMessages.Where(x => x < fromMessageId.Value); + else + cachedMessageIds = _orderedMessages.Where(x => x > fromMessageId.Value); + + if (dir == Direction.Before) + cachedMessageIds = cachedMessageIds.Reverse(); + + return cachedMessageIds + .Select(x => + { + if (_messages.TryGetValue(x, out SocketMessage msg)) + return msg; + return null; + }) + .Where(x => x != null) + .Take(limit) + .ToImmutableArray(); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs new file mode 100644 index 0000000..7900b7e --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -0,0 +1,220 @@ +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 +{ + /// + /// Represents a WebSocket-based message. + /// + public abstract class SocketMessage : SocketEntity, IMessage + { + private long _timestampTicks; + private readonly List _reactions = new List(); + + /// + /// Gets the author of this message. + /// + /// + /// A WebSocket-based user object. + /// + public SocketUser Author { get; } + /// + /// Gets the source channel of the message. + /// + /// + /// A WebSocket-based message channel. + /// + public ISocketMessageChannel Channel { get; } + /// + public MessageSource Source { get; } + + /// + public string Content { get; private set; } + + /// + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + /// + public virtual bool IsTTS => false; + /// + public virtual bool IsPinned => false; + /// + public virtual bool IsSuppressed => false; + /// + public virtual DateTimeOffset? EditedTimestamp => null; + + /// + public MessageActivity Activity { get; private set; } + + /// + public MessageApplication Application { get; private set; } + + /// + public MessageReference Reference { get; private set; } + + /// + /// Returns all attachments included in this message. + /// + /// + /// Collection of attachments. + /// + public virtual IReadOnlyCollection Attachments => ImmutableArray.Create(); + /// + /// Returns all embeds included in this message. + /// + /// + /// Collection of embed objects. + /// + public virtual IReadOnlyCollection Embeds => ImmutableArray.Create(); + /// + /// Returns the channels mentioned in this message. + /// + /// + /// Collection of WebSocket-based guild channels. + /// + public virtual IReadOnlyCollection MentionedChannels => ImmutableArray.Create(); + /// + /// Returns the roles mentioned in this message. + /// + /// + /// Collection of WebSocket-based roles. + /// + public virtual IReadOnlyCollection MentionedRoles => ImmutableArray.Create(); + /// + /// Returns the users mentioned in this message. + /// + /// + /// Collection of WebSocket-based users. + /// + public virtual IReadOnlyCollection MentionedUsers => ImmutableArray.Create(); + /// + public virtual IReadOnlyCollection Tags => ImmutableArray.Create(); + /// + public IReadOnlyDictionary Reactions => _reactions.GroupBy(r => r.Emote).ToDictionary(x => x.Key, x => new ReactionMetadata { ReactionCount = x.Count(), IsMe = x.Any(y => y.UserId == Discord.CurrentUser.Id) }); + + /// + public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); + + internal SocketMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author, MessageSource source) + : base(discord, id) + { + Channel = channel; + Author = author; + Source = source; + } + 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; + + if (model.Application.IsSpecified) + { + // create a new Application from the API model + Application = new MessageApplication() + { + Id = model.Application.Value.Id, + CoverImage = model.Application.Value.CoverImage, + Description = model.Application.Value.Description, + Icon = model.Application.Value.Icon, + Name = model.Application.Value.Name + }; + } + + if (model.Activity.IsSpecified) + { + // create a new Activity from the API model + Activity = new MessageActivity() + { + Type = model.Activity.Value.Type.Value, + PartyId = model.Activity.Value.PartyId.Value + }; + } + + if (model.Reference.IsSpecified) + { + // Creates a new Reference from the API model + Reference = new MessageReference + { + GuildId = model.Reference.Value.GuildId, + ChannelId = model.Reference.Value.ChannelId, + MessageId = model.Reference.Value.MessageId + }; + } + } + + /// + public Task DeleteAsync(RequestOptions options = null) + => MessageHelper.DeleteAsync(this, Discord, options); + + /// + /// Gets the content of the message. + /// + /// + /// Content of the message. + /// + 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(); + + internal void AddReaction(SocketReaction reaction) + { + _reactions.Add(reaction); + } + internal void RemoveReaction(SocketReaction reaction) + { + if (_reactions.Contains(reaction)) + _reactions.Remove(reaction); + } + internal void ClearReactions() + { + _reactions.Clear(); + } + + /// + public Task AddReactionAsync(IEmote emote, RequestOptions options = null) + => MessageHelper.AddReactionAsync(this, emote, Discord, options); + /// + public Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null) + => MessageHelper.RemoveReactionAsync(this, user.Id, emote, Discord, options); + /// + public Task RemoveReactionAsync(IEmote emote, ulong userId, RequestOptions options = null) + => MessageHelper.RemoveReactionAsync(this, userId, emote, Discord, options); + /// + public Task RemoveAllReactionsAsync(RequestOptions options = null) + => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); + /// + public IAsyncEnumerable> GetReactionUsersAsync(IEmote emote, int limit, RequestOptions options = null) + => MessageHelper.GetReactionUsersAsync(this, emote, limit, Discord, options); + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs new file mode 100644 index 0000000..32cac7d --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs @@ -0,0 +1,110 @@ +using Model = Discord.API.Gateway.Reaction; + +namespace Discord.WebSocket +{ + /// + /// Represents a WebSocket-based reaction object. + /// + public class SocketReaction : IReaction + { + /// + /// Gets the ID of the user who added the reaction. + /// + /// + /// This property retrieves the snowflake identifier of the user responsible for this reaction. This + /// property will always contain the user identifier in event that + /// cannot be retrieved. + /// + /// + /// A user snowflake identifier associated with the user. + /// + public ulong UserId { get; } + /// + /// Gets the user who added the reaction if possible. + /// + /// + /// + /// This property attempts to retrieve a WebSocket-cached user that is responsible for this reaction from + /// the client. In other words, when the user is not in the WebSocket cache, this property may not + /// contain a value, leaving the only identifiable information to be + /// . + /// + /// + /// If you wish to obtain an identifiable user object, consider utilizing + /// which will attempt to retrieve the user from REST. + /// + /// + /// + /// A user object where possible; a value is not always returned. + /// + /// + public Optional User { get; } + /// + /// Gets the ID of the message that has been reacted to. + /// + /// + /// A message snowflake identifier associated with the message. + /// + public ulong MessageId { get; } + /// + /// Gets the message that has been reacted to if possible. + /// + /// + /// A WebSocket-based message where possible; a value is not always returned. + /// + /// + public Optional Message { get; } + /// + /// Gets the channel where the reaction takes place in. + /// + /// + /// A WebSocket-based message channel. + /// + public ISocketMessageChannel Channel { get; } + /// + public IEmote Emote { get; } + + internal SocketReaction(ISocketMessageChannel channel, ulong messageId, Optional message, ulong userId, Optional user, IEmote emoji) + { + Channel = channel; + MessageId = messageId; + Message = message; + UserId = userId; + User = user; + Emote = emoji; + } + internal static SocketReaction Create(Model model, ISocketMessageChannel channel, Optional message, Optional user) + { + IEmote emote; + if (model.Emoji.Id.HasValue) + emote = new Emote(model.Emoji.Id.Value, model.Emoji.Name, model.Emoji.Animated.GetValueOrDefault()); + else + emote = new Emoji(model.Emoji.Name); + return new SocketReaction(channel, model.MessageId, message, model.UserId, user, emote); + } + + /// + public override bool Equals(object other) + { + if (other == null) return false; + if (other == this) return true; + + var otherReaction = other as SocketReaction; + if (otherReaction == null) return false; + + return UserId == otherReaction.UserId && MessageId == otherReaction.MessageId && Emote.Equals(otherReaction.Emote); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashCode = UserId.GetHashCode(); + hashCode = (hashCode * 397) ^ MessageId.GetHashCode(); + hashCode = (hashCode * 397) ^ Emote.GetHashCode(); + return hashCode; + } + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs new file mode 100644 index 0000000..d0ce502 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs @@ -0,0 +1,35 @@ +using System.Diagnostics; +using Model = Discord.API.Message; + +namespace Discord.WebSocket +{ + /// + /// Represents a WebSocket-based message sent by the system. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public 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, MessageSource.System) + { + } + 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/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs new file mode 100644 index 0000000..b26dfe5 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -0,0 +1,154 @@ +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.Message; + +namespace Discord.WebSocket +{ + /// + /// Represents a WebSocket-based message sent by a user. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketUserMessage : SocketMessage, IUserMessage + { + private bool _isMentioningEveryone, _isTTS, _isPinned, _isSuppressed; + private long? _editedTimestampTicks; + private ImmutableArray _attachments = ImmutableArray.Create(); + private ImmutableArray _embeds = ImmutableArray.Create(); + private ImmutableArray _tags = ImmutableArray.Create(); + + /// + public override bool IsTTS => _isTTS; + /// + public override bool IsPinned => _isPinned; + /// + public override bool IsSuppressed => _isSuppressed; + /// + 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, MessageSource source) + : base(discord, id, channel, author, source) + { + } + internal new static SocketUserMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) + { + var entity = new SocketUserMessage(discord, model.Id, channel, author, MessageHelper.GetSource(model)); + 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.Flags.IsSpecified) + { + _isSuppressed = model.Flags.Value.HasFlag(API.MessageFlags.Suppressed); + } + + 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(value[i].ToEntity()); + _embeds = embeds.ToImmutable(); + } + else + _embeds = ImmutableArray.Create(); + } + + IReadOnlyCollection mentions = ImmutableArray.Create(); //Is passed to ParseTags to get real mention collection + 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(SocketUnknownUser.Create(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; + } + } + + /// + /// Only the author of a message may modify the message. + /// Message content is too long, length must be less or equal to . + public Task ModifyAsync(Action func, RequestOptions options = null) + => MessageHelper.ModifyAsync(this, Discord, func, options); + + /// + public Task PinAsync(RequestOptions options = null) + => MessageHelper.PinAsync(this, Discord, options); + /// + public Task UnpinAsync(RequestOptions options = null) + => MessageHelper.UnpinAsync(this, Discord, options); + /// + public Task ModifySuppressionAsync(bool suppressEmbeds, RequestOptions options = null) + => MessageHelper.SuppressEmbedsAsync(this, Discord, suppressEmbeds, options); + + public string Resolve(int startIndex, TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, + TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) + => MentionUtils.Resolve(this, startIndex, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); + /// + 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, 0, 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/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs new file mode 100644 index 0000000..b5e26ad --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs @@ -0,0 +1,100 @@ +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Role; + +namespace Discord.WebSocket +{ + /// + /// Represents a WebSocket-based role to be given to a guild user. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketRole : SocketEntity, IRole + { + /// + /// Gets the guild that owns this role. + /// + /// + /// A representing the parent guild of this role. + /// + 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 => SnowflakeUtils.FromSnowflake(Id); + /// + /// Returns a value that determines if the role is an @everyone role. + /// + /// + /// true if the role is @everyone; otherwise false. + /// + public bool IsEveryone => Id == Guild.Id; + /// + public string Mention => IsEveryone ? "@everyone" : MentionUtils.MentionRole(Id); + public IEnumerable Members + => Guild.Users.Where(x => x.Roles.Any(r => r.Id == 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); + + /// + /// Gets the name of the role. + /// + /// + /// A string that resolves to . + /// + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id})"; + internal SocketRole Clone() => MemberwiseClone() as SocketRole; + + /// + public int CompareTo(IRole role) => RoleUtils.Compare(this, role); + + //IRole + /// + IGuild IRole.Guild => Guild; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/SocketEntity.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/SocketEntity.cs new file mode 100644 index 0000000..f76694e --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/SocketEntity.cs @@ -0,0 +1,18 @@ +using System; + +namespace Discord.WebSocket +{ + public abstract class SocketEntity : IEntity + where T : IEquatable + { + internal DiscordSocketClient Discord { get; } + /// + public T Id { get; } + + internal SocketEntity(DiscordSocketClient discord, T id) + { + Discord = discord; + Id = id; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs new file mode 100644 index 0000000..48de755 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs @@ -0,0 +1,61 @@ +using System.Diagnostics; +using System.Linq; +using Model = Discord.API.User; +using PresenceModel = Discord.API.Presence; + +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 SocketPresence Presence { get; set; } + + public override bool IsWebhook => false; + internal override SocketGlobalUser GlobalUser => this; + + 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 void Update(ClientState state, PresenceModel model) + { + Presence = SocketPresence.Create(model); + DMChannel = state.DMChannels.FirstOrDefault(x => x.Recipient.Id == Id); + } + + private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Global)"; + internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs new file mode 100644 index 0000000..676c0a8 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs @@ -0,0 +1,67 @@ +using System.Diagnostics; +using Model = Discord.API.User; + +namespace Discord.WebSocket +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class SocketGroupUser : SocketUser, IGroupUser + { + /// + /// Gets the group channel of the user. + /// + /// + /// A representing the channel of which the user belongs to. + /// + 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; } } + + /// + public override bool IsWebhook => false; + + 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; + } + + private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Group)"; + 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; + /// + bool IVoiceState.IsStreaming => false; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs new file mode 100644 index 0000000..e5dbfa0 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -0,0 +1,205 @@ +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 UserModel = Discord.API.User; +using MemberModel = Discord.API.GuildMember; +using PresenceModel = Discord.API.Presence; + +namespace Discord.WebSocket +{ + /// + /// Represents a WebSocket-based guild user. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketGuildUser : SocketUser, IGuildUser + { + private long? _premiumSinceTicks; + private long? _joinedAtTicks; + private ImmutableArray _roleIds; + + internal override SocketGlobalUser GlobalUser { get; } + /// + /// Gets the guild the user is in. + /// + 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)); + internal override SocketPresence Presence { get; set; } + + /// + public override bool IsWebhook => false; + /// + public bool IsSelfDeafened => VoiceState?.IsSelfDeafened ?? false; + /// + public bool IsSelfMuted => VoiceState?.IsSelfMuted ?? false; + /// + public bool IsSuppressed => VoiceState?.IsSuppressed ?? false; + /// + public bool IsDeafened => VoiceState?.IsDeafened ?? false; + /// + public bool IsMuted => VoiceState?.IsMuted ?? false; + /// + public bool IsStreaming => VoiceState?.IsStreaming ?? false; + /// + public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); + /// + /// Returns a collection of roles that the user possesses. + /// + public IReadOnlyCollection Roles + => _roleIds.Select(id => Guild.GetRole(id)).Where(x => x != null).ToReadOnlyCollection(() => _roleIds.Length); + /// + /// Returns the voice channel the user is in, or null if none. + /// + public SocketVoiceChannel VoiceChannel => VoiceState?.VoiceChannel; + /// + public string VoiceSessionId => VoiceState?.VoiceSessionId ?? ""; + /// + /// Gets the voice connection status of the user if any. + /// + /// + /// A representing the user's voice status; null if the user is not + /// connected to a voice channel. + /// + public SocketVoiceState? VoiceState => Guild.GetVoiceState(Id); + public AudioInStream AudioStream => Guild.GetAudioStream(Id); + /// + public DateTimeOffset? PremiumSince => DateTimeUtils.FromTicks(_premiumSinceTicks); + + /// + /// Returns the position of the user within the role hierarchy. + /// + /// + /// The returned value equal to the position of the highest role the user has, or + /// if user is the server owner. + /// + public int Hierarchy + { + get + { + if (Guild.OwnerId == Id) + return int.MaxValue; + + int maxPos = 0; + for (int i = 0; i < _roleIds.Length; i++) + { + var role = Guild.GetRole(_roleIds[i]); + if (role != null && role.Position > maxPos) + maxPos = role.Position; + } + return maxPos; + } + } + + internal SocketGuildUser(SocketGuild guild, SocketGlobalUser globalUser) + : base(guild.Discord, globalUser.Id) + { + Guild = guild; + GlobalUser = globalUser; + } + internal static SocketGuildUser Create(SocketGuild guild, ClientState state, UserModel model) + { + var entity = new SocketGuildUser(guild, guild.Discord.GetOrCreateUser(state, model)); + entity.Update(state, model); + entity.UpdateRoles(new ulong[0]); + return entity; + } + internal static SocketGuildUser Create(SocketGuild guild, ClientState state, MemberModel 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, false); + return entity; + } + internal void Update(ClientState state, MemberModel model) + { + base.Update(state, model.User); + if (model.JoinedAt.IsSpecified) + _joinedAtTicks = model.JoinedAt.Value.UtcTicks; + if (model.Nick.IsSpecified) + Nickname = model.Nick.Value; + if (model.Roles.IsSpecified) + UpdateRoles(model.Roles.Value); + if (model.PremiumSince.IsSpecified) + _premiumSinceTicks = model.PremiumSince.Value?.UtcTicks; + } + internal void Update(ClientState state, PresenceModel model, bool updatePresence) + { + if (updatePresence) + { + Presence = SocketPresence.Create(model); + GlobalUser.Update(state, model); + } + if (model.Nick.IsSpecified) + Nickname = model.Nick.Value; + if (model.Roles.IsSpecified) + UpdateRoles(model.Roles.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(string reason = null, RequestOptions options = null) + => UserHelper.KickAsync(this, Discord, reason, options); + /// + public Task AddRoleAsync(IRole role, RequestOptions options = null) + => AddRolesAsync(new[] { role }, options); + /// + public Task AddRolesAsync(IEnumerable roles, RequestOptions options = null) + => UserHelper.AddRolesAsync(this, Discord, roles, options); + /// + public Task RemoveRoleAsync(IRole role, RequestOptions options = null) + => RemoveRolesAsync(new[] { role }, options); + /// + public Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null) + => UserHelper.RemoveRolesAsync(this, Discord, roles, options); + + /// + public ChannelPermissions GetPermissions(IGuildChannel channel) + => new ChannelPermissions(Permissions.ResolveChannel(Guild, this, channel, GuildPermissions.RawValue)); + + private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Guild)"; + internal new SocketGuildUser Clone() => MemberwiseClone() as SocketGuildUser; + + //IGuildUser + /// + IGuild IGuildUser.Guild => Guild; + /// + ulong IGuildUser.GuildId => Guild.Id; + /// + IReadOnlyCollection IGuildUser.RoleIds => _roleIds; + + //IVoiceState + /// + IVoiceChannel IVoiceState.VoiceChannel => VoiceChannel; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs new file mode 100644 index 0000000..52f1113 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using Model = Discord.API.Presence; + +namespace Discord.WebSocket +{ + /// + /// Represents the WebSocket user's presence status. This may include their online status and their activity. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public struct SocketPresence : IPresence + { + /// + public UserStatus Status { get; } + /// + public IActivity Activity { get; } + /// + public IImmutableSet ActiveClients { get; } + internal SocketPresence(UserStatus status, IActivity activity, IImmutableSet activeClients) + { + Status = status; + Activity= activity; + ActiveClients = activeClients; + } + internal static SocketPresence Create(Model model) + { + var clients = ConvertClientTypesDict(model.ClientStatus.GetValueOrDefault()); + return new SocketPresence(model.Status, model.Game?.ToEntity(), clients); + } + /// + /// Creates a new containing all of the client types + /// where a user is active from the data supplied in the Presence update frame. + /// + /// + /// A dictionary keyed by the + /// and where the value is the . + /// + /// + /// A collection of all s that this user is active. + /// + private static IImmutableSet ConvertClientTypesDict(IDictionary clientTypesDict) + { + if (clientTypesDict == null || clientTypesDict.Count == 0) + return ImmutableHashSet.Empty; + var set = new HashSet(); + foreach (var key in clientTypesDict.Keys) + { + if (Enum.TryParse(key, true, out ClientType type)) + set.Add(type); + // quietly discard ClientTypes that do not match + } + return set.ToImmutableHashSet(); + } + + /// + /// Gets the status of the user. + /// + /// + /// A string that resolves to . + /// + public override string ToString() => Status.ToString(); + private string DebuggerDisplay => $"{Status}{(Activity != null ? $", {Activity.Name}": "")}"; + + internal SocketPresence Clone() => this; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs new file mode 100644 index 0000000..7b11257 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs @@ -0,0 +1,97 @@ +using Discord.Rest; +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.User; + +namespace Discord.WebSocket +{ + /// + /// Represents the logged-in WebSocket-based user. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketSelfUser : SocketUser, ISelfUser + { + /// + 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; } } + /// + public UserProperties Flags { get; internal set; } + /// + public PremiumType PremiumType { get; internal set; } + /// + public string Locale { get; internal set; } + + /// + public override bool IsWebhook => false; + + 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 bool Update(ClientState state, Model model) + { + bool hasGlobalChanges = base.Update(state, model); + if (model.Email.IsSpecified) + { + Email = model.Email.Value; + hasGlobalChanges = true; + } + if (model.Verified.IsSpecified) + { + IsVerified = model.Verified.Value; + hasGlobalChanges = true; + } + if (model.MfaEnabled.IsSpecified) + { + IsMfaEnabled = model.MfaEnabled.Value; + hasGlobalChanges = true; + } + if (model.Flags.IsSpecified && model.Flags.Value != Flags) + { + Flags = (UserProperties)model.Flags.Value; + hasGlobalChanges = true; + } + if (model.PremiumType.IsSpecified && model.PremiumType.Value != PremiumType) + { + PremiumType = model.PremiumType.Value; + hasGlobalChanges = true; + } + if (model.Locale.IsSpecified && model.Locale.Value != Locale) + { + Locale = model.Locale.Value; + hasGlobalChanges = true; + } + return hasGlobalChanges; + } + + /// + public Task ModifyAsync(Action func, RequestOptions options = null) + => UserHelper.ModifyAsync(this, Discord, func, options); + + private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Self)"; + internal new SocketSelfUser Clone() => MemberwiseClone() as SocketSelfUser; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs new file mode 100644 index 0000000..840a1c3 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs @@ -0,0 +1,48 @@ +using System; +using System.Diagnostics; +using Model = Discord.API.User; + +namespace Discord.WebSocket +{ + /// + /// Represents a WebSocket-based user that is yet to be recognized by the client. + /// + /// + /// A user may not be recognized due to the user missing from the cache or failed to be recognized properly. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketUnknownUser : SocketUser + { + /// + public override string Username { get; internal set; } + /// + public override ushort DiscriminatorValue { get; internal set; } + /// + public override string AvatarId { get; internal set; } + /// + public override bool IsBot { get; internal set; } + + /// + public override bool IsWebhook => false; + /// + internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null, null); } set { } } + /// + /// This field is not supported for an unknown user. + internal override SocketGlobalUser GlobalUser => + throw new NotSupportedException(); + + internal SocketUnknownUser(DiscordSocketClient discord, ulong id) + : base(discord, id) + { + } + internal static SocketUnknownUser Create(DiscordSocketClient discord, ClientState state, Model model) + { + var entity = new SocketUnknownUser(discord, model.Id); + entity.Update(state, model); + return entity; + } + + private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Unknown)"; + internal new SocketUnknownUser Clone() => MemberwiseClone() as SocketUnknownUser; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketUser.cs new file mode 100644 index 0000000..09c4165 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Discord.Rest; +using Model = Discord.API.User; + +namespace Discord.WebSocket +{ + /// + /// Represents a WebSocket-based user. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + 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; } + /// + public abstract bool IsWebhook { get; } + internal abstract SocketGlobalUser GlobalUser { get; } + internal abstract SocketPresence Presence { get; set; } + + /// + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + /// + public string Discriminator => DiscriminatorValue.ToString("D4"); + /// + public string Mention => MentionUtils.MentionUser(Id); + /// + public IActivity Activity => Presence.Activity; + /// + public UserStatus Status => Presence.Status; + /// + public IImmutableSet ActiveClients => Presence.ActiveClients ?? ImmutableHashSet.Empty; + /// + /// Gets mutual guilds shared with this user. + /// + public IReadOnlyCollection MutualGuilds + => Discord.Guilds.Where(g => g.Users.Any(u => u.Id == Id)).ToImmutableArray(); + + internal SocketUser(DiscordSocketClient discord, ulong id) + : base(discord, id) + { + } + internal virtual bool Update(ClientState state, Model model) + { + bool hasChanges = false; + if (model.Avatar.IsSpecified && model.Avatar.Value != AvatarId) + { + AvatarId = model.Avatar.Value; + hasChanges = true; + } + if (model.Discriminator.IsSpecified) + { + var newVal = ushort.Parse(model.Discriminator.Value, NumberStyles.None, CultureInfo.InvariantCulture); + if (newVal != DiscriminatorValue) + { + DiscriminatorValue = ushort.Parse(model.Discriminator.Value, NumberStyles.None, CultureInfo.InvariantCulture); + hasChanges = true; + } + } + if (model.Bot.IsSpecified && model.Bot.Value != IsBot) + { + IsBot = model.Bot.Value; + hasChanges = true; + } + if (model.Username.IsSpecified && model.Username.Value != Username) + { + Username = model.Username.Value; + hasChanges = true; + } + return hasChanges; + } + + /// + public async Task GetOrCreateDMChannelAsync(RequestOptions options = null) + => GlobalUser.DMChannel ?? await UserHelper.CreateDMChannelAsync(this, Discord, options).ConfigureAwait(false) as IDMChannel; + + /// + public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + + /// + public string GetDefaultAvatarUrl() + => CDN.GetDefaultUserAvatarUrl(DiscriminatorValue); + + /// + /// Gets the full name of the user (e.g. Example#0001). + /// + /// + /// The full name of the user. + /// + public override string ToString() => $"{Username}#{Discriminator}"; + private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; + internal SocketUser Clone() => MemberwiseClone() as SocketUser; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs new file mode 100644 index 0000000..5bf36e7 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs @@ -0,0 +1,90 @@ +using System; +using System.Diagnostics; +using Model = Discord.API.VoiceState; + +namespace Discord.WebSocket +{ + /// + /// Represents a WebSocket user's voice connection status. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public struct SocketVoiceState : IVoiceState + { + /// + /// Initializes a default with everything set to null or false. + /// + public static readonly SocketVoiceState Default = new SocketVoiceState(null, null, false, false, false, false, false, false); + + [Flags] + private enum Flags : byte + { + Normal = 0x00, + Suppressed = 0x01, + Muted = 0x02, + Deafened = 0x04, + SelfMuted = 0x08, + SelfDeafened = 0x10, + SelfStream = 0x20, + } + + private readonly Flags _voiceStates; + + /// + /// Gets the voice channel that the user is currently in; or null if none. + /// + public SocketVoiceChannel VoiceChannel { get; } + /// + public string VoiceSessionId { get; } + + /// + public bool IsMuted => (_voiceStates & Flags.Muted) != 0; + /// + public bool IsDeafened => (_voiceStates & Flags.Deafened) != 0; + /// + public bool IsSuppressed => (_voiceStates & Flags.Suppressed) != 0; + /// + public bool IsSelfMuted => (_voiceStates & Flags.SelfMuted) != 0; + /// + public bool IsSelfDeafened => (_voiceStates & Flags.SelfDeafened) != 0; + /// + public bool IsStreaming => (_voiceStates & Flags.SelfStream) != 0; + + internal SocketVoiceState(SocketVoiceChannel voiceChannel, string sessionId, bool isSelfMuted, bool isSelfDeafened, bool isMuted, bool isDeafened, bool isSuppressed, bool isStream) + { + VoiceChannel = voiceChannel; + VoiceSessionId = sessionId; + + Flags voiceStates = Flags.Normal; + if (isSelfMuted) + voiceStates |= Flags.SelfMuted; + if (isSelfDeafened) + voiceStates |= Flags.SelfDeafened; + if (isMuted) + voiceStates |= Flags.Muted; + if (isDeafened) + voiceStates |= Flags.Deafened; + if (isSuppressed) + voiceStates |= Flags.Suppressed; + if (isStream) + voiceStates |= Flags.SelfStream; + _voiceStates = voiceStates; + } + internal static SocketVoiceState Create(SocketVoiceChannel voiceChannel, Model model) + { + return new SocketVoiceState(voiceChannel, model.SessionId, model.SelfMute, model.SelfDeaf, model.Mute, model.Deaf, model.Suppress, model.SelfStream); + } + + /// + /// Gets the name of this voice channel. + /// + /// + /// A string that resolves to name of this voice channel; otherwise "Unknown". + /// + public override string ToString() => VoiceChannel?.Name ?? "Unknown"; + private string DebuggerDisplay => $"{VoiceChannel?.Name ?? "Unknown"} ({_voiceStates})"; + internal SocketVoiceState Clone() => this; + + /// + IVoiceChannel IVoiceState.VoiceChannel => VoiceChannel; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs new file mode 100644 index 0000000..8819fe1 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.User; + +namespace Discord.WebSocket +{ + /// + /// Represents a WebSocket-based webhook user. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketWebhookUser : SocketUser, IWebhookUser + { + /// Gets the guild of this webhook. + public SocketGuild Guild { get; } + /// + public ulong WebhookId { get; } + + /// + public override string Username { get; internal set; } + /// + public override ushort DiscriminatorValue { get; internal set; } + /// + public override string AvatarId { get; internal set; } + /// + public override bool IsBot { get; internal set; } + + /// + public override bool IsWebhook => true; + /// + internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null, null); } set { } } + internal override SocketGlobalUser GlobalUser => + throw new NotSupportedException(); + + internal SocketWebhookUser(SocketGuild guild, ulong id, ulong webhookId) + : base(guild.Discord, id) + { + Guild = guild; + WebhookId = webhookId; + } + internal static SocketWebhookUser Create(SocketGuild guild, ClientState state, Model model, ulong webhookId) + { + var entity = new SocketWebhookUser(guild, model.Id, webhookId); + entity.Update(state, model); + return entity; + } + + private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Webhook)"; + internal new SocketWebhookUser Clone() => MemberwiseClone() as SocketWebhookUser; + + + //IGuildUser + /// + IGuild IGuildUser.Guild => Guild; + /// + ulong IGuildUser.GuildId => Guild.Id; + /// + IReadOnlyCollection IGuildUser.RoleIds => ImmutableArray.Create(); + /// + DateTimeOffset? IGuildUser.JoinedAt => null; + /// + string IGuildUser.Nickname => null; + /// + DateTimeOffset? IGuildUser.PremiumSince => null; + /// + GuildPermissions IGuildUser.GuildPermissions => GuildPermissions.Webhook; + + /// + ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel) => Permissions.ToChannelPerms(channel, GuildPermissions.Webhook.RawValue); + /// + /// Webhook users cannot be kicked. + Task IGuildUser.KickAsync(string reason, RequestOptions options) => + throw new NotSupportedException("Webhook users cannot be kicked."); + + /// + /// Webhook users cannot be modified. + Task IGuildUser.ModifyAsync(Action func, RequestOptions options) => + throw new NotSupportedException("Webhook users cannot be modified."); + + /// + /// Roles are not supported on webhook users. + Task IGuildUser.AddRoleAsync(IRole role, RequestOptions options) => + throw new NotSupportedException("Roles are not supported on webhook users."); + + /// + /// Roles are not supported on webhook users. + Task IGuildUser.AddRolesAsync(IEnumerable roles, RequestOptions options) => + throw new NotSupportedException("Roles are not supported on webhook users."); + + /// + /// Roles are not supported on webhook users. + Task IGuildUser.RemoveRoleAsync(IRole role, RequestOptions options) => + throw new NotSupportedException("Roles are not supported on webhook users."); + + /// + /// Roles are not supported on webhook users. + Task IGuildUser.RemoveRolesAsync(IEnumerable roles, RequestOptions options) => + throw new NotSupportedException("Roles are not supported on webhook users."); + + //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; + /// + bool IVoiceState.IsStreaming => false; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Voice/SocketVoiceServer.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Voice/SocketVoiceServer.cs new file mode 100644 index 0000000..c5f13b1 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Entities/Voice/SocketVoiceServer.cs @@ -0,0 +1,42 @@ +using System.Diagnostics; + +namespace Discord.WebSocket +{ + /// + /// Represents a WebSocket-based voice server. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketVoiceServer + { + /// + /// Gets the guild associated with the voice server. + /// + /// + /// A cached entity of the guild. + /// + public Cacheable Guild { get; } + /// + /// Gets the endpoint URL of the voice server host. + /// + /// + /// An URL representing the voice server host. + /// + public string Endpoint { get; } + /// + /// Gets the voice connection token. + /// + /// + /// A voice connection token. + /// + public string Token { get; } + + internal SocketVoiceServer(Cacheable guild, string endpoint, string token) + { + Guild = guild; + Endpoint = endpoint; + Token = token; + } + + private string DebuggerDisplay => $"SocketVoiceServer ({Guild.Id})"; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Extensions/EntityExtensions.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Extensions/EntityExtensions.cs new file mode 100644 index 0000000..cbe5750 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Extensions/EntityExtensions.cs @@ -0,0 +1,133 @@ +using Discord.Rest; +using System; +using System.Collections.Immutable; +using System.Linq; + +namespace Discord.WebSocket +{ + internal static class EntityExtensions + { + public static IActivity ToEntity(this API.Game model) + { + // Custom Status Game + if (model.Id.IsSpecified && model.Id.Value == "custom") + { + return new CustomStatusGame() + { + Type = ActivityType.CustomStatus, + Name = model.Name, + State = model.State.IsSpecified ? model.State.Value : null, + Emote = model.Emoji.IsSpecified ? model.Emoji.Value.ToIEmote() : null, + CreatedAt = DateTimeOffset.FromUnixTimeMilliseconds(model.CreatedAt.Value), + }; + } + + // Spotify Game + if (model.SyncId.IsSpecified) + { + var assets = model.Assets.GetValueOrDefault()?.ToEntity(); + string albumText = assets?[1]?.Text; + string albumArtId = assets?[1]?.ImageId?.Replace("spotify:",""); + var timestamps = model.Timestamps.IsSpecified ? model.Timestamps.Value.ToEntity() : null; + return new SpotifyGame + { + Name = model.Name, + SessionId = model.SessionId.GetValueOrDefault(), + TrackId = model.SyncId.Value, + TrackUrl = CDN.GetSpotifyDirectUrl(model.SyncId.Value), + AlbumTitle = albumText, + TrackTitle = model.Details.GetValueOrDefault(), + Artists = model.State.GetValueOrDefault()?.Split(';').Select(x=>x?.Trim()).ToImmutableArray(), + StartedAt = timestamps?.Start, + EndsAt = timestamps?.End, + Duration = timestamps?.End - timestamps?.Start, + AlbumArtUrl = albumArtId != null ? CDN.GetSpotifyAlbumArtUrl(albumArtId) : null, + Type = ActivityType.Listening, + Flags = model.Flags.GetValueOrDefault(), + }; + } + + // Rich Game + if (model.ApplicationId.IsSpecified) + { + ulong appId = model.ApplicationId.Value; + var assets = model.Assets.GetValueOrDefault()?.ToEntity(appId); + return new RichGame + { + ApplicationId = appId, + Name = model.Name, + Details = model.Details.GetValueOrDefault(), + State = model.State.GetValueOrDefault(), + SmallAsset = assets?[0], + LargeAsset = assets?[1], + Party = model.Party.IsSpecified ? model.Party.Value.ToEntity() : null, + Secrets = model.Secrets.IsSpecified ? model.Secrets.Value.ToEntity() : null, + Timestamps = model.Timestamps.IsSpecified ? model.Timestamps.Value.ToEntity() : null, + Flags = model.Flags.GetValueOrDefault() + }; + } + // Stream Game + if (model.StreamUrl.IsSpecified) + { + return new StreamingGame( + model.Name, + model.StreamUrl.Value) + { + Flags = model.Flags.GetValueOrDefault(), + Details = model.Details.GetValueOrDefault() + }; + } + // Normal Game + return new Game(model.Name, model.Type.GetValueOrDefault() ?? ActivityType.Playing, + model.Flags.IsSpecified ? model.Flags.Value : ActivityProperties.None, + model.Details.GetValueOrDefault()); + } + + // (Small, Large) + public static GameAsset[] ToEntity(this API.GameAssets model, ulong? appId = null) + { + return new GameAsset[] + { + model.SmallImage.IsSpecified ? new GameAsset + { + ApplicationId = appId, + ImageId = model.SmallImage.GetValueOrDefault(), + Text = model.SmallText.GetValueOrDefault() + } : null, + model.LargeImage.IsSpecified ? new GameAsset + { + ApplicationId = appId, + ImageId = model.LargeImage.GetValueOrDefault(), + Text = model.LargeText.GetValueOrDefault() + } : null, + }; + } + + public static GameParty ToEntity(this API.GameParty model) + { + // Discord will probably send bad data since they don't validate anything + long current = 0, cap = 0; + if (model.Size?.Length == 2) + { + current = model.Size[0]; + cap = model.Size[1]; + } + return new GameParty + { + Id = model.Id, + Members = current, + Capacity = cap, + }; + } + + public static GameSecrets ToEntity(this API.GameSecrets model) + { + return new GameSecrets(model.Match, model.Join, model.Spectate); + } + + public static GameTimestamps ToEntity(this API.GameTimestamps model) + { + return new GameTimestamps(model.Start.ToNullable(), model.End.ToNullable()); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs new file mode 100644 index 0000000..82079e9 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs @@ -0,0 +1,150 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.Udp +{ + internal class DefaultUdpSocket : IUdpSocket, IDisposable + { + public event Func ReceivedDatagram; + + private readonly SemaphoreSlim _lock; + private UdpClient _udp; + private IPEndPoint _destination; + private CancellationTokenSource _stopCancelTokenSource, _cancelTokenSource; + private CancellationToken _cancelToken, _parentToken; + private Task _task; + private bool _isDisposed; + + public ushort Port => (ushort)((_udp?.Client.LocalEndPoint as IPEndPoint)?.Port ?? 0); + + public DefaultUdpSocket() + { + _lock = new SemaphoreSlim(1, 1); + _stopCancelTokenSource = new CancellationTokenSource(); + } + private void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + StopInternalAsync(true).GetAwaiter().GetResult(); + _stopCancelTokenSource?.Dispose(); + _cancelTokenSource?.Dispose(); + _lock?.Dispose(); + } + _isDisposed = true; + } + } + public void Dispose() + { + Dispose(true); + } + + + public async Task StartAsync() + { + await _lock.WaitAsync().ConfigureAwait(false); + try + { + await StartInternalAsync(_cancelToken).ConfigureAwait(false); + } + finally + { + _lock.Release(); + } + } + public async Task StartInternalAsync(CancellationToken cancelToken) + { + await StopInternalAsync().ConfigureAwait(false); + + _stopCancelTokenSource?.Dispose(); + _cancelTokenSource?.Dispose(); + + _stopCancelTokenSource = new CancellationTokenSource(); + _cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _stopCancelTokenSource.Token); + _cancelToken = _cancelTokenSource.Token; + + _udp?.Dispose(); + _udp = new UdpClient(0); + + _task = RunAsync(_cancelToken); + } + public async Task StopAsync() + { + await _lock.WaitAsync().ConfigureAwait(false); + try + { + await StopInternalAsync().ConfigureAwait(false); + } + finally + { + _lock.Release(); + } + } + public async Task StopInternalAsync(bool isDisposing = false) + { + try { _stopCancelTokenSource.Cancel(false); } catch { } + + if (!isDisposing) + await (_task ?? Task.Delay(0)).ConfigureAwait(false); + + if (_udp != null) + { + try { _udp.Dispose(); } + catch { } + _udp = null; + } + } + + public void SetDestination(string ip, int port) + { + _destination = new IPEndPoint(IPAddress.Parse(ip), port); + } + public void SetCancelToken(CancellationToken cancelToken) + { + _cancelTokenSource?.Dispose(); + + _parentToken = cancelToken; + _cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _stopCancelTokenSource.Token); + _cancelToken = _cancelTokenSource.Token; + } + + public async Task SendAsync(byte[] data, int index, int count) + { + if (index != 0) //Should never happen? + { + var newData = new byte[count]; + Buffer.BlockCopy(data, index, newData, 0, count); + data = newData; + } + await _udp.SendAsync(data, count, _destination).ConfigureAwait(false); + } + + private async Task RunAsync(CancellationToken cancelToken) + { + var closeTask = Task.Delay(-1, cancelToken); + while (!cancelToken.IsCancellationRequested) + { + var receiveTask = _udp.ReceiveAsync(); + + _ = receiveTask.ContinueWith((receiveResult) => + { + //observe the exception as to not receive as unhandled exception + _ = receiveResult.Exception; + + }, TaskContinuationOptions.OnlyOnFaulted); + + var task = await Task.WhenAny(closeTask, receiveTask).ConfigureAwait(false); + if (task == closeTask) + break; + + var result = receiveTask.Result; + await ReceivedDatagram(result.Buffer, 0, result.Buffer.Length).ConfigureAwait(false); + } + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Net/DefaultUdpSocketProvider.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Net/DefaultUdpSocketProvider.cs new file mode 100644 index 0000000..d701fa7 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Net/DefaultUdpSocketProvider.cs @@ -0,0 +1,19 @@ +using System; + +namespace Discord.Net.Udp +{ + public static class DefaultUdpSocketProvider + { + public static readonly UdpSocketProvider Instance = () => + { + try + { + return new DefaultUdpSocket(); + } + catch (PlatformNotSupportedException ex) + { + throw new PlatformNotSupportedException("The default UdpSocketProvider is not supported on this platform.", ex); + } + }; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs new file mode 100644 index 0000000..36a6fea --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Net; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.WebSockets +{ + internal class DefaultWebSocketClient : IWebSocketClient, IDisposable + { + public const int ReceiveChunkSize = 16 * 1024; //16KB + public const int SendChunkSize = 4 * 1024; //4KB + private const int HR_TIMEOUT = -2147012894; + + public event Func BinaryMessage; + public event Func TextMessage; + public event Func Closed; + + private readonly SemaphoreSlim _lock; + private readonly Dictionary _headers; + private readonly IWebProxy _proxy; + private ClientWebSocket _client; + private Task _task; + private CancellationTokenSource _disconnectTokenSource, _cancelTokenSource; + private CancellationToken _cancelToken, _parentToken; + private bool _isDisposed, _isDisconnecting; + + public DefaultWebSocketClient(IWebProxy proxy = null) + { + _lock = new SemaphoreSlim(1, 1); + _disconnectTokenSource = new CancellationTokenSource(); + _cancelToken = CancellationToken.None; + _parentToken = CancellationToken.None; + _headers = new Dictionary(); + _proxy = proxy; + } + private void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + DisconnectInternalAsync(true).GetAwaiter().GetResult(); + _disconnectTokenSource?.Dispose(); + _cancelTokenSource?.Dispose(); + _lock?.Dispose(); + } + _isDisposed = true; + } + } + public void Dispose() + { + Dispose(true); + } + + public async Task ConnectAsync(string host) + { + await _lock.WaitAsync().ConfigureAwait(false); + try + { + await ConnectInternalAsync(host).ConfigureAwait(false); + } + finally + { + _lock.Release(); + } + } + private async Task ConnectInternalAsync(string host) + { + await DisconnectInternalAsync().ConfigureAwait(false); + + _disconnectTokenSource?.Dispose(); + _cancelTokenSource?.Dispose(); + + _disconnectTokenSource = new CancellationTokenSource(); + _cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _disconnectTokenSource.Token); + _cancelToken = _cancelTokenSource.Token; + + _client?.Dispose(); + _client = new ClientWebSocket(); + _client.Options.Proxy = _proxy; + _client.Options.KeepAliveInterval = TimeSpan.Zero; + foreach (var header in _headers) + { + if (header.Value != null) + _client.Options.SetRequestHeader(header.Key, header.Value); + } + + await _client.ConnectAsync(new Uri(host), _cancelToken).ConfigureAwait(false); + _task = RunAsync(_cancelToken); + } + + public async Task DisconnectAsync() + { + await _lock.WaitAsync().ConfigureAwait(false); + try + { + await DisconnectInternalAsync().ConfigureAwait(false); + } + finally + { + _lock.Release(); + } + } + private async Task DisconnectInternalAsync(bool isDisposing = false) + { + try { _disconnectTokenSource.Cancel(false); } + catch { } + + _isDisconnecting = true; + + if (_client != null) + { + if (!isDisposing) + { + try { await _client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", new CancellationToken()); } + catch { } + } + try { _client.Dispose(); } + catch { } + + _client = null; + } + + try + { + await (_task ?? Task.Delay(0)).ConfigureAwait(false); + _task = null; + } + finally { _isDisconnecting = false; } + } + private async Task OnClosed(Exception ex) + { + if (_isDisconnecting) + return; //Ignore, this disconnect was requested. + + await _lock.WaitAsync().ConfigureAwait(false); + try + { + await DisconnectInternalAsync(false); + } + finally + { + _lock.Release(); + } + await Closed(ex); + } + + public void SetHeader(string key, string value) + { + _headers[key] = value; + } + public void SetCancelToken(CancellationToken cancelToken) + { + _cancelTokenSource?.Dispose(); + + _parentToken = cancelToken; + _cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _disconnectTokenSource.Token); + _cancelToken = _cancelTokenSource.Token; + } + + public async Task SendAsync(byte[] data, int index, int count, bool isText) + { + await _lock.WaitAsync().ConfigureAwait(false); + try + { + if (_client == null) return; + + int frameCount = (int)Math.Ceiling((double)count / SendChunkSize); + + for (int i = 0; i < frameCount; i++, index += SendChunkSize) + { + bool isLast = i == (frameCount - 1); + + int frameSize; + if (isLast) + frameSize = count - (i * SendChunkSize); + else + frameSize = SendChunkSize; + + var type = isText ? WebSocketMessageType.Text : WebSocketMessageType.Binary; + await _client.SendAsync(new ArraySegment(data, index, count), type, isLast, _cancelToken).ConfigureAwait(false); + } + } + finally + { + _lock.Release(); + } + } + + private async Task RunAsync(CancellationToken cancelToken) + { + var buffer = new ArraySegment(new byte[ReceiveChunkSize]); + + try + { + while (!cancelToken.IsCancellationRequested) + { + WebSocketReceiveResult socketResult = await _client.ReceiveAsync(buffer, CancellationToken.None).ConfigureAwait(false); + byte[] result; + int resultCount; + + if (socketResult.MessageType == WebSocketMessageType.Close) + throw new WebSocketClosedException((int)socketResult.CloseStatus, socketResult.CloseStatusDescription); + + if (!socketResult.EndOfMessage) + { + //This is a large message (likely just READY), lets create a temporary expandable stream + using (var stream = new MemoryStream()) + { + stream.Write(buffer.Array, 0, socketResult.Count); + do + { + if (cancelToken.IsCancellationRequested) return; + socketResult = await _client.ReceiveAsync(buffer, cancelToken).ConfigureAwait(false); + stream.Write(buffer.Array, 0, socketResult.Count); + } + while (socketResult == null || !socketResult.EndOfMessage); + + //Use the internal buffer if we can get it + resultCount = (int)stream.Length; + + result = stream.TryGetBuffer(out var streamBuffer) ? streamBuffer.Array : stream.ToArray(); + + } + } + else + { + //Small message + resultCount = socketResult.Count; + result = buffer.Array; + } + + if (socketResult.MessageType == WebSocketMessageType.Text) + { + string text = Encoding.UTF8.GetString(result, 0, resultCount); + await TextMessage(text).ConfigureAwait(false); + } + else + await BinaryMessage(result, 0, resultCount).ConfigureAwait(false); + } + } + catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT) + { + var _ = OnClosed(new Exception("Connection timed out.", ex)); + } + catch (OperationCanceledException) { } + catch (Exception ex) + { + //This cannot be awaited otherwise we'll deadlock when DiscordApiClient waits for this task to complete. + var _ = OnClosed(ex); + } + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs new file mode 100644 index 0000000..bc580c4 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs @@ -0,0 +1,26 @@ +using System; +using System.Net; + +namespace Discord.Net.WebSockets +{ + public static class DefaultWebSocketProvider + { + public static readonly WebSocketProvider Instance = Create(); + + /// The default WebSocketProvider is not supported on this platform. + public static WebSocketProvider Create(IWebProxy proxy = null) + { + return () => + { + try + { + return new DefaultWebSocketClient(proxy); + } + catch (PlatformNotSupportedException ex) + { + throw new PlatformNotSupportedException("The default WebSocketProvider is not supported on this platform.", ex); + } + }; + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Webhook/Discord.Net.Webhook.csproj b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Webhook/Discord.Net.Webhook.csproj new file mode 100644 index 0000000..f1db663 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Webhook/Discord.Net.Webhook.csproj @@ -0,0 +1,13 @@ + + + + Discord.Net.Webhook + Discord.Webhook + A core Discord.Net library containing the Webhook client and models. + netstandard2.0;netstandard2.1 + + + + + + diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Webhook/DiscordWebhookClient.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Webhook/DiscordWebhookClient.cs new file mode 100644 index 0000000..542ec79 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Webhook/DiscordWebhookClient.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Discord.Logging; +using Discord.Rest; + +namespace Discord.Webhook +{ + /// A client responsible for connecting as a Webhook. + public class DiscordWebhookClient : IDisposable + { + public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } + internal readonly AsyncEvent> _logEvent = new AsyncEvent>(); + + private readonly ulong _webhookId; + internal IWebhook Webhook; + internal readonly Logger _restLogger; + + internal API.DiscordRestApiClient ApiClient { get; } + internal LogManager LogManager { get; } + + /// Creates a new Webhook Discord client. + public DiscordWebhookClient(IWebhook webhook) + : this(webhook.Id, webhook.Token, new DiscordRestConfig()) { } + /// Creates a new Webhook Discord client. + public DiscordWebhookClient(ulong webhookId, string webhookToken) + : this(webhookId, webhookToken, new DiscordRestConfig()) { } + /// Creates a new Webhook Discord client. + public DiscordWebhookClient(string webhookUrl) + : this(webhookUrl, new DiscordRestConfig()) { } + + // regex pattern to match webhook urls + private static Regex WebhookUrlRegex = new Regex(@"^.*discordapp\.com\/api\/webhooks\/([\d]+)\/([a-z0-9_-]+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + /// Creates a new Webhook Discord client. + public DiscordWebhookClient(ulong webhookId, string webhookToken, DiscordRestConfig config) + : this(config) + { + _webhookId = webhookId; + ApiClient.LoginAsync(TokenType.Webhook, webhookToken).GetAwaiter().GetResult(); + Webhook = WebhookClientHelper.GetWebhookAsync(this, webhookId).GetAwaiter().GetResult(); + } + /// Creates a new Webhook Discord client. + public DiscordWebhookClient(IWebhook webhook, DiscordRestConfig config) + : this(config) + { + Webhook = webhook; + _webhookId = Webhook.Id; + } + + /// + /// Creates a new Webhook Discord client. + /// + /// The url of the webhook. + /// The configuration options to use for this client. + /// Thrown if the is an invalid format. + /// Thrown if the is null or whitespace. + public DiscordWebhookClient(string webhookUrl, DiscordRestConfig config) : this(config) + { + string token; + ParseWebhookUrl(webhookUrl, out _webhookId, out token); + ApiClient.LoginAsync(TokenType.Webhook, token).GetAwaiter().GetResult(); + Webhook = WebhookClientHelper.GetWebhookAsync(this, _webhookId).GetAwaiter().GetResult(); + } + + private DiscordWebhookClient(DiscordRestConfig config) + { + ApiClient = CreateApiClient(config); + LogManager = new LogManager(config.LogLevel); + LogManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); + + _restLogger = LogManager.CreateLogger("Rest"); + + ApiClient.RequestQueue.RateLimitTriggered += async (id, info) => + { + if (info == null) + await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); + else + await _restLogger.WarningAsync($"Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); + }; + ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false); + } + private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config) + => new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent); + /// Sends a message using to the channel for this webhook. + /// Returns the ID of the created message. + public Task SendMessageAsync(string text = null, bool isTTS = false, IEnumerable embeds = null, + string username = null, string avatarUrl = null, RequestOptions options = null) + => WebhookClientHelper.SendMessageAsync(this, text, isTTS, embeds, username, avatarUrl, options); + + /// Sends a message to the channel for this webhook with an attachment. + /// Returns the ID of the created message. + public Task SendFileAsync(string filePath, string text, bool isTTS = false, + IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null, bool isSpoiler = false) + => WebhookClientHelper.SendFileAsync(this, filePath, text, isTTS, embeds, username, avatarUrl, options, isSpoiler); + /// Sends a message to the channel for this webhook with an attachment. + /// Returns the ID of the created message. + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, + IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null, bool isSpoiler = false) + => WebhookClientHelper.SendFileAsync(this, stream, filename, text, isTTS, embeds, username, avatarUrl, options, isSpoiler); + + /// Modifies the properties of this webhook. + public Task ModifyWebhookAsync(Action func, RequestOptions options = null) + => Webhook.ModifyAsync(func, options); + + /// Deletes this webhook from Discord and disposes the client. + public async Task DeleteWebhookAsync(RequestOptions options = null) + { + await Webhook.DeleteAsync(options).ConfigureAwait(false); + Dispose(); + } + + public void Dispose() + { + ApiClient?.Dispose(); + } + + internal static void ParseWebhookUrl(string webhookUrl, out ulong webhookId, out string webhookToken) + { + if (string.IsNullOrWhiteSpace(webhookUrl)) + throw new ArgumentNullException(paramName: nameof(webhookUrl), message: + "The given webhook Url cannot be null or whitespace."); + + // thrown when groups are not populated/valid, or when there is no match + ArgumentException ex(string reason = null) + => new ArgumentException(paramName: nameof(webhookUrl), message: + $"The given webhook Url was not in a valid format. {reason}"); + var match = WebhookUrlRegex.Match(webhookUrl); + if (match != null) + { + // ensure that the first group is a ulong, set the _webhookId + // 0th group is always the entire match, so start at index 1 + if (!(match.Groups[1].Success && ulong.TryParse(match.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out webhookId))) + throw ex("The webhook Id could not be parsed."); + + if (!match.Groups[2].Success) + throw ex("The webhook token could not be parsed."); + webhookToken = match.Groups[2].Value; + } + else + throw ex(); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Webhook/Entities/Webhooks/RestInternalWebhook.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Webhook/Entities/Webhooks/RestInternalWebhook.cs new file mode 100644 index 0000000..60cb89e --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Webhook/Entities/Webhooks/RestInternalWebhook.cs @@ -0,0 +1,66 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.Webhook; + +namespace Discord.Webhook +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + internal class RestInternalWebhook : IWebhook + { + private DiscordWebhookClient _client; + + public ulong Id { get; } + public ulong ChannelId { get; } + public string Token { get; } + + public string Name { get; private set; } + public string AvatarId { get; private set; } + public ulong? GuildId { get; private set; } + + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + + internal RestInternalWebhook(DiscordWebhookClient apiClient, Model model) + { + _client = apiClient; + Id = model.Id; + ChannelId = model.Id; + Token = model.Token; + } + internal static RestInternalWebhook Create(DiscordWebhookClient client, Model model) + { + var entity = new RestInternalWebhook(client, model); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + if (model.Avatar.IsSpecified) + AvatarId = model.Avatar.Value; + if (model.GuildId.IsSpecified) + GuildId = model.GuildId.Value; + if (model.Name.IsSpecified) + Name = model.Name.Value; + } + + public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + + public async Task ModifyAsync(Action func, RequestOptions options = null) + { + var model = await WebhookClientHelper.ModifyAsync(_client, func, options).ConfigureAwait(false); + Update(model); + } + + public Task DeleteAsync(RequestOptions options = null) + => WebhookClientHelper.DeleteAsync(_client, options); + + public override string ToString() => $"Webhook: {Name}:{Id}"; + private string DebuggerDisplay => $"Webhook: {Name} ({Id})"; + + IUser IWebhook.Creator => null; + ITextChannel IWebhook.Channel => null; + IGuild IWebhook.Guild => null; + } +} diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Webhook/WebhookClientHelper.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Webhook/WebhookClientHelper.cs new file mode 100644 index 0000000..311d58b --- /dev/null +++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Webhook/WebhookClientHelper.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Discord.API.Rest; +using Discord.Rest; +using ImageModel = Discord.API.Image; +using WebhookModel = Discord.API.Webhook; + +namespace Discord.Webhook +{ + internal static class WebhookClientHelper + { + /// Could not find a webhook with the supplied credentials. + public static async Task GetWebhookAsync(DiscordWebhookClient client, ulong webhookId) + { + var model = await client.ApiClient.GetWebhookAsync(webhookId).ConfigureAwait(false); + if (model == null) + throw new InvalidOperationException("Could not find a webhook with the supplied credentials."); + return RestInternalWebhook.Create(client, model); + } + public static async Task SendMessageAsync(DiscordWebhookClient client, + string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, RequestOptions options) + { + var args = new CreateWebhookMessageParams(text) { IsTTS = isTTS }; + if (embeds != null) + args.Embeds = embeds.Select(x => x.ToModel()).ToArray(); + if (username != null) + args.Username = username; + if (avatarUrl != null) + args.AvatarUrl = avatarUrl; + + var model = await client.ApiClient.CreateWebhookMessageAsync(client.Webhook.Id, args, options: options).ConfigureAwait(false); + return model.Id; + } + public static async Task SendFileAsync(DiscordWebhookClient client, string filePath, string text, bool isTTS, + IEnumerable embeds, string username, string avatarUrl, RequestOptions options, bool isSpoiler) + { + string filename = Path.GetFileName(filePath); + using (var file = File.OpenRead(filePath)) + return await SendFileAsync(client, file, filename, text, isTTS, embeds, username, avatarUrl, options, isSpoiler).ConfigureAwait(false); + } + public static async Task SendFileAsync(DiscordWebhookClient client, Stream stream, string filename, string text, bool isTTS, + IEnumerable embeds, string username, string avatarUrl, RequestOptions options, bool isSpoiler) + { + var args = new UploadWebhookFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS, IsSpoiler = isSpoiler }; + if (username != null) + args.Username = username; + if (avatarUrl != null) + args.AvatarUrl = avatarUrl; + if (embeds != null) + args.Embeds = embeds.Select(x => x.ToModel()).ToArray(); + var msg = await client.ApiClient.UploadWebhookFileAsync(client.Webhook.Id, args, options).ConfigureAwait(false); + return msg.Id; + } + + public static async Task ModifyAsync(DiscordWebhookClient client, + Action func, RequestOptions options) + { + var args = new WebhookProperties(); + func(args); + var apiArgs = new ModifyWebhookParams + { + Avatar = args.Image.IsSpecified ? args.Image.Value?.ToModel() : Optional.Create(), + Name = args.Name + }; + + if (!apiArgs.Avatar.IsSpecified && client.Webhook.AvatarId != null) + apiArgs.Avatar = new ImageModel(client.Webhook.AvatarId); + + return await client.ApiClient.ModifyWebhookAsync(client.Webhook.Id, apiArgs, options).ConfigureAwait(false); + } + + public static async Task DeleteAsync(DiscordWebhookClient client, RequestOptions options) + { + await client.ApiClient.DeleteWebhookAsync(client.Webhook.Id, options).ConfigureAwait(false); + } + } +} diff --git a/src/stream-sniper/stream-sniper/MainWindow.xaml b/src/stream-sniper/stream-sniper/MainWindow.xaml new file mode 100644 index 0000000..0e9e0d3 --- /dev/null +++ b/src/stream-sniper/stream-sniper/MainWindow.xaml @@ -0,0 +1,12 @@ + + + + + diff --git a/src/stream-sniper/stream-sniper/MainWindow.xaml.cs b/src/stream-sniper/stream-sniper/MainWindow.xaml.cs new file mode 100644 index 0000000..6c4429a --- /dev/null +++ b/src/stream-sniper/stream-sniper/MainWindow.xaml.cs @@ -0,0 +1,29 @@ +using Discord.WebSocket; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace stream_sniper +{ + /// + /// Interaction logic for MainWindow.xaml + /// + public partial class MainWindow : Window + { + public MainWindow() + { + InitializeComponent(); + } + } +} diff --git a/src/stream-sniper/stream-sniper/Properties/AssemblyInfo.cs b/src/stream-sniper/stream-sniper/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..dde89af --- /dev/null +++ b/src/stream-sniper/stream-sniper/Properties/AssemblyInfo.cs @@ -0,0 +1,55 @@ +using System.Reflection; +using System.Resources; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Windows; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("stream-sniper")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("stream-sniper")] +[assembly: AssemblyCopyright("Copyright © 2020")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +//In order to begin building localizable applications, set +//CultureYouAreCodingWith in your .csproj file +//inside a . For example, if you are using US english +//in your source files, set the to en-US. Then uncomment +//the NeutralResourceLanguage attribute below. Update the "en-US" in +//the line below to match the UICulture setting in the project file. + +//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] + + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] + + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/stream-sniper/stream-sniper/Properties/Resources.Designer.cs b/src/stream-sniper/stream-sniper/Properties/Resources.Designer.cs new file mode 100644 index 0000000..08ed735 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Properties/Resources.Designer.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace stream_sniper.Properties +{ + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources + { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() + { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if ((resourceMan == null)) + { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("stream_sniper.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get + { + return resourceCulture; + } + set + { + resourceCulture = value; + } + } + } +} diff --git a/src/stream-sniper/stream-sniper/Properties/Resources.resx b/src/stream-sniper/stream-sniper/Properties/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/src/stream-sniper/stream-sniper/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/src/stream-sniper/stream-sniper/Properties/Settings.Designer.cs b/src/stream-sniper/stream-sniper/Properties/Settings.Designer.cs new file mode 100644 index 0000000..aa212c9 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Properties/Settings.Designer.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace stream_sniper.Properties +{ + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase + { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default + { + get + { + return defaultInstance; + } + } + } +} diff --git a/src/stream-sniper/stream-sniper/Properties/Settings.settings b/src/stream-sniper/stream-sniper/Properties/Settings.settings new file mode 100644 index 0000000..033d7a5 --- /dev/null +++ b/src/stream-sniper/stream-sniper/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/stream-sniper/stream-sniper/packages.config b/src/stream-sniper/stream-sniper/packages.config new file mode 100644 index 0000000..d33c48d --- /dev/null +++ b/src/stream-sniper/stream-sniper/packages.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/stream-sniper/stream-sniper/stream-sniper.csproj b/src/stream-sniper/stream-sniper/stream-sniper.csproj new file mode 100644 index 0000000..1a5e2e9 --- /dev/null +++ b/src/stream-sniper/stream-sniper/stream-sniper.csproj @@ -0,0 +1,704 @@ + + + + + Debug + AnyCPU + {D6DCDFC5-4DB3-4340-94F7-A5EC846E9195} + WinExe + stream_sniper + stream-sniper + v4.8 + 512 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 4 + true + true + + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + true + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\MaterialDesignColors.1.2.7\lib\net45\MaterialDesignColors.dll + + + ..\packages\MaterialDesignThemes.3.2.0\lib\net45\MaterialDesignThemes.Wpf.dll + + + ..\packages\Microsoft.Bcl.AsyncInterfaces.1.1.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll + + + ..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll + + + + ..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll + + + ..\packages\System.Collections.Immutable.1.7.1\lib\net461\System.Collections.Immutable.dll + + + + + ..\packages\System.Interactive.Async.4.1.1\lib\net461\System.Interactive.Async.dll + + + ..\packages\System.Linq.Async.4.1.1\lib\net461\System.Linq.Async.dll + + + ..\packages\System.Memory.4.5.4\lib\net461\System.Memory.dll + + + + ..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll + + + ..\packages\System.Runtime.CompilerServices.Unsafe.4.7.1\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + + + ..\packages\System.Threading.Tasks.Extensions.4.5.2\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll + + + + + + + + + 4.0 + + + + + + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + App.xaml + Code + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MainWindow.xaml + Code + + + + + Code + + + True + True + Resources.resx + + + True + Settings.settings + True + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file