@@ -1,6 +1,6 @@ | |||||
Microsoft Visual Studio Solution File, Format Version 12.00 | Microsoft Visual Studio Solution File, Format Version 12.00 | ||||
# Visual Studio 15 | # Visual Studio 15 | ||||
VisualStudioVersion = 15.0.27004.2009 | |||||
VisualStudioVersion = 15.0.27130.0 | |||||
MinimumVisualStudioVersion = 10.0.40219.1 | MinimumVisualStudioVersion = 10.0.40219.1 | ||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Core", "src\Discord.Net.Core\Discord.Net.Core.csproj", "{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}" | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Core", "src\Discord.Net.Core\Discord.Net.Core.csproj", "{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}" | ||||
EndProject | EndProject | ||||
@@ -22,6 +22,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Tests", "test\D | |||||
EndProject | EndProject | ||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Webhook", "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj", "{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}" | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Webhook", "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj", "{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}" | ||||
EndProject | EndProject | ||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Analyzers", "src\Discord.Net.Analyzers\Discord.Net.Analyzers.csproj", "{BBA8E7FB-C834-40DC-822F-B112CB7F0140}" | |||||
EndProject | |||||
Global | Global | ||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution | GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||||
Debug|Any CPU = Debug|Any CPU | Debug|Any CPU = Debug|Any CPU | ||||
@@ -116,6 +118,18 @@ Global | |||||
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x64.Build.0 = Release|Any CPU | {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x64.Build.0 = Release|Any CPU | ||||
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x86.ActiveCfg = Release|Any CPU | {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x86.ActiveCfg = Release|Any CPU | ||||
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x86.Build.0 = Release|Any CPU | {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x86.Build.0 = Release|Any CPU | ||||
{BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | |||||
{BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Debug|Any CPU.Build.0 = Debug|Any CPU | |||||
{BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Debug|x64.ActiveCfg = Debug|Any CPU | |||||
{BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Debug|x64.Build.0 = Debug|Any CPU | |||||
{BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Debug|x86.ActiveCfg = Debug|Any CPU | |||||
{BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Debug|x86.Build.0 = Debug|Any CPU | |||||
{BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|Any CPU.ActiveCfg = Release|Any CPU | |||||
{BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|Any CPU.Build.0 = Release|Any CPU | |||||
{BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|x64.ActiveCfg = Release|Any CPU | |||||
{BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|x64.Build.0 = Release|Any CPU | |||||
{BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|x86.ActiveCfg = Release|Any CPU | |||||
{BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|x86.Build.0 = Release|Any CPU | |||||
EndGlobalSection | EndGlobalSection | ||||
GlobalSection(SolutionProperties) = preSolution | GlobalSection(SolutionProperties) = preSolution | ||||
HideSolutionNode = FALSE | HideSolutionNode = FALSE | ||||
@@ -126,6 +140,7 @@ Global | |||||
{688FD1D8-7F01-4539-B2E9-F473C5D699C7} = {288C363D-A636-4EAE-9AC1-4698B641B26E} | {688FD1D8-7F01-4539-B2E9-F473C5D699C7} = {288C363D-A636-4EAE-9AC1-4698B641B26E} | ||||
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012} | {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012} | ||||
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} | {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} | ||||
{BBA8E7FB-C834-40DC-822F-B112CB7F0140} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} | |||||
EndGlobalSection | EndGlobalSection | ||||
GlobalSection(ExtensibilityGlobals) = postSolution | GlobalSection(ExtensibilityGlobals) = postSolution | ||||
SolutionGuid = {D2404771-EEC8-45F2-9D71-F3373F6C1495} | SolutionGuid = {D2404771-EEC8-45F2-9D71-F3373F6C1495} | ||||
@@ -1,7 +1,7 @@ | |||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | ||||
<PropertyGroup> | <PropertyGroup> | ||||
<VersionPrefix>2.0.0</VersionPrefix> | <VersionPrefix>2.0.0</VersionPrefix> | ||||
<VersionSuffix>beta</VersionSuffix> | |||||
<VersionSuffix>beta2</VersionSuffix> | |||||
<Authors>RogueException</Authors> | <Authors>RogueException</Authors> | ||||
<PackageTags>discord;discordapp</PackageTags> | <PackageTags>discord;discordapp</PackageTags> | ||||
<PackageProjectUrl>https://github.com/RogueException/Discord.Net</PackageProjectUrl> | <PackageProjectUrl>https://github.com/RogueException/Discord.Net</PackageProjectUrl> | ||||
@@ -29,6 +29,7 @@ after_build: | |||||
- ps: dotnet pack "src\Discord.Net.Commands\Discord.Net.Commands.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" | - ps: dotnet pack "src\Discord.Net.Commands\Discord.Net.Commands.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" | ||||
- ps: dotnet pack "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" | - ps: dotnet pack "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" | ||||
- ps: dotnet pack "src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" | - ps: dotnet pack "src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" | ||||
- ps: dotnet pack "src\Discord.Net.Analyzers\Discord.Net.Analyzers.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" | |||||
- ps: >- | - ps: >- | ||||
if ($Env:APPVEYOR_REPO_TAG -eq "true") { | if ($Env:APPVEYOR_REPO_TAG -eq "true") { | ||||
nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="" | nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="" | ||||
@@ -0,0 +1,16 @@ | |||||
# Instructions for Building Documentation | |||||
The documentation for the Discord.NET library uses [DocFX][docfx-main]. [Instructions for installing this tool can be found here.][docfx-installing] | |||||
1. Navigate to the root of the repository. | |||||
2. (Optional) If you intend to target a specific version, ensure that you | |||||
have the correct version checked out. | |||||
3. Build the library. Run `dotnet build` in the root of this repository. | |||||
Ensure that the build passes without errors. | |||||
4. Build the docs using `docfx .\docs\docfx.json`. Add the `--serve` parameter | |||||
to preview the site locally. Some elements of the page may appear incorrect | |||||
when not hosted by a server. | |||||
- Remarks: According to the docfx website, this tool does work on Linux under mono. | |||||
[docfx-main]: https://dotnet.github.io/docfx/ | |||||
[docfx-installing]: https://dotnet.github.io/docfx/tutorial/docfx_getting_started.html |
@@ -67,8 +67,8 @@ | |||||
"default" | "default" | ||||
], | ], | ||||
"globalMetadata": { | "globalMetadata": { | ||||
"_appFooter": "Discord.Net (c) 2015-2017" | |||||
"_appFooter": "Discord.Net (c) 2015-2018 2.0.0-beta" | |||||
}, | }, | ||||
"noLangKeyword": false | "noLangKeyword": false | ||||
} | } | ||||
} | |||||
} |
@@ -3,7 +3,7 @@ public class Info : ModuleBase<SocketCommandContext> | |||||
{ | { | ||||
// ~say hello -> hello | // ~say hello -> hello | ||||
[Command("say")] | [Command("say")] | ||||
[Summary("Echos a message.")] | |||||
[Summary("Echoes a message.")] | |||||
public async Task SayAsync([Remainder] [Summary("The text to echo")] string echo) | public async Task SayAsync([Remainder] [Summary("The text to echo")] string echo) | ||||
{ | { | ||||
// ReplyAsync is a method on ModuleBase | // ReplyAsync is a method on ModuleBase | ||||
@@ -38,4 +38,4 @@ public class Sample : ModuleBase<SocketCommandContext> | |||||
var userInfo = user ?? Context.Client.CurrentUser; | var userInfo = user ?? Context.Client.CurrentUser; | ||||
await ReplyAsync($"{userInfo.Username}#{userInfo.Discriminator}"); | await ReplyAsync($"{userInfo.Username}#{userInfo.Discriminator}"); | ||||
} | } | ||||
} | |||||
} |
@@ -19,10 +19,10 @@ class Program | |||||
private readonly DiscordSocketClient _client; | private readonly DiscordSocketClient _client; | ||||
// Keep the CommandService and IServiceCollection around for use with commands. | |||||
// Keep the CommandService and DI container around for use with commands. | |||||
// These two types require you install the Discord.Net.Commands package. | // These two types require you install the Discord.Net.Commands package. | ||||
private readonly IServiceCollection _map = new ServiceCollection(); | |||||
private readonly CommandService _commands = new CommandService(); | |||||
private readonly CommandService _commands; | |||||
private readonly IServiceProvider _services; | |||||
private Program() | private Program() | ||||
{ | { | ||||
@@ -41,14 +41,45 @@ class Program | |||||
// add the `using` at the top, and uncomment this line: | // add the `using` at the top, and uncomment this line: | ||||
//WebSocketProvider = WS4NetProvider.Instance | //WebSocketProvider = WS4NetProvider.Instance | ||||
}); | }); | ||||
_commands = new CommandService(new CommandServiceConfig | |||||
{ | |||||
// Again, log level: | |||||
LogLevel = LogSeverity.Info, | |||||
// There's a few more properties you can set, | |||||
// for example, case-insensitive commands. | |||||
CaseSensitiveCommands = false, | |||||
}); | |||||
// Subscribe the logging handler to both the client and the CommandService. | // Subscribe the logging handler to both the client and the CommandService. | ||||
_client.Log += Logger; | |||||
_commands.Log += Logger; | |||||
_client.Log += Log; | |||||
_commands.Log += Log; | |||||
// Setup your DI container. | |||||
_services = ConfigureServices(), | |||||
} | |||||
// If any services require the client, or the CommandService, or something else you keep on hand, | |||||
// pass them as parameters into this method as needed. | |||||
// If this method is getting pretty long, you can seperate it out into another file using partials. | |||||
private static IServiceProvider ConfigureServices() | |||||
{ | |||||
var map = new ServiceCollection() | |||||
// Repeat this for all the service classes | |||||
// and other dependencies that your commands might need. | |||||
.AddSingleton(new SomeServiceClass()); | |||||
// When all your required services are in the collection, build the container. | |||||
// Tip: There's an overload taking in a 'validateScopes' bool to make sure | |||||
// you haven't made any mistakes in your dependency graph. | |||||
return map.BuildServiceProvider(); | |||||
} | } | ||||
// Example of a logging handler. This can be re-used by addons | // Example of a logging handler. This can be re-used by addons | ||||
// that ask for a Func<LogMessage, Task>. | // that ask for a Func<LogMessage, Task>. | ||||
private static Task Logger(LogMessage message) | |||||
private static Task Log(LogMessage message) | |||||
{ | { | ||||
switch (message.Severity) | switch (message.Severity) | ||||
{ | { | ||||
@@ -92,24 +123,15 @@ class Program | |||||
await Task.Delay(Timeout.Infinite); | await Task.Delay(Timeout.Infinite); | ||||
} | } | ||||
private IServiceProvider _services; | |||||
private async Task InitCommands() | private async Task InitCommands() | ||||
{ | { | ||||
// Repeat this for all the service classes | |||||
// and other dependencies that your commands might need. | |||||
_map.AddSingleton(new SomeServiceClass()); | |||||
// When all your required services are in the collection, build the container. | |||||
// Tip: There's an overload taking in a 'validateScopes' bool to make sure | |||||
// you haven't made any mistakes in your dependency graph. | |||||
_services = _map.BuildServiceProvider(); | |||||
// Either search the program and add all Module classes that can be found. | // Either search the program and add all Module classes that can be found. | ||||
// Module classes MUST be marked 'public' or they will be ignored. | // Module classes MUST be marked 'public' or they will be ignored. | ||||
await _commands.AddModulesAsync(Assembly.GetEntryAssembly()); | |||||
// You also need to pass your 'IServiceProvider' instance now, | |||||
// so make sure that's done before you get here. | |||||
await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); | |||||
// Or add Modules manually if you prefer to be a little more explicit: | // Or add Modules manually if you prefer to be a little more explicit: | ||||
await _commands.AddModuleAsync<SomeModule>(); | |||||
await _commands.AddModuleAsync<SomeModule>(_services); | |||||
// Note that the first one is 'Modules' (plural) and the second is 'Module' (singular). | // Note that the first one is 'Modules' (plural) and the second is 'Module' (singular). | ||||
// Subscribe a handler to see if a message invokes a command. | // Subscribe a handler to see if a message invokes a command. | ||||
@@ -123,8 +145,6 @@ class Program | |||||
if (msg == null) return; | if (msg == null) return; | ||||
// We don't want the bot to respond to itself or other bots. | // We don't want the bot to respond to itself or other bots. | ||||
// NOTE: Selfbots should invert this first check and remove the second | |||||
// as they should ONLY be allowed to respond to messages from the same account. | |||||
if (msg.Author.Id == _client.CurrentUser.Id || msg.Author.IsBot) return; | if (msg.Author.Id == _client.CurrentUser.Id || msg.Author.IsBot) return; | ||||
// Create a number to track where the prefix ends and the command begins | // Create a number to track where the prefix ends and the command begins | ||||
@@ -140,10 +160,12 @@ class Program | |||||
// Execute the command. (result does not indicate a return value, | // Execute the command. (result does not indicate a return value, | ||||
// rather an object stating if the command executed successfully). | // rather an object stating if the command executed successfully). | ||||
var result = await _commands.ExecuteAsync(context, pos, _services); | |||||
var result = await _commands.ExecuteAsync(context, pos); | |||||
// Uncomment the following lines if you want the bot | // Uncomment the following lines if you want the bot | ||||
// to send a message if it failed (not advised for most situations). | |||||
// to send a message if it failed. | |||||
// This does not catch errors from commands with 'RunMode.Async', | |||||
// subscribe a handler for '_commands.CommandExecuted' to see those. | |||||
//if (!result.IsSuccess && result.Error != CommandError.UnknownCommand) | //if (!result.IsSuccess && result.Error != CommandError.UnknownCommand) | ||||
// await msg.Channel.SendMessageAsync(result.ErrorReason); | // await msg.Channel.SendMessageAsync(result.ErrorReason); | ||||
} | } | ||||
@@ -0,0 +1,15 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk"> | |||||
<Import Project="../../Discord.Net.targets" /> | |||||
<PropertyGroup> | |||||
<AssemblyName>Discord.Net.Analyzers</AssemblyName> | |||||
<RootNamespace>Discord.Analyzers</RootNamespace> | |||||
<Description>A Discord.Net extension adding support for design-time analysis of the API usage.</Description> | |||||
<TargetFramework>netstandard1.3</TargetFramework> | |||||
</PropertyGroup> | |||||
<ItemGroup> | |||||
<PackageReference Include="Microsoft.CodeAnalysis" Version="2.6.0" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<ProjectReference Include="..\Discord.Net.Commands\Discord.Net.Commands.csproj" /> | |||||
</ItemGroup> | |||||
</Project> |
@@ -0,0 +1,70 @@ | |||||
using System; | |||||
using System.Collections.Immutable; | |||||
using System.Linq; | |||||
using Microsoft.CodeAnalysis; | |||||
using Microsoft.CodeAnalysis.CSharp; | |||||
using Microsoft.CodeAnalysis.CSharp.Syntax; | |||||
using Microsoft.CodeAnalysis.Diagnostics; | |||||
using Discord.Commands; | |||||
namespace Discord.Analyzers | |||||
{ | |||||
[DiagnosticAnalyzer(LanguageNames.CSharp)] | |||||
public sealed class GuildAccessAnalyzer : DiagnosticAnalyzer | |||||
{ | |||||
private const string DiagnosticId = "DNET0001"; | |||||
private const string Title = "Limit command to Guild contexts."; | |||||
private const string MessageFormat = "Command method '{0}' is accessing 'Context.Guild' but is not restricted to Guild contexts."; | |||||
private const string Description = "Accessing 'Context.Guild' in a command without limiting the command to run only in guilds."; | |||||
private const string Category = "API Usage"; | |||||
private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description); | |||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule); | |||||
public override void Initialize(AnalysisContext context) | |||||
{ | |||||
context.RegisterSyntaxNodeAction(AnalyzeMemberAccess, SyntaxKind.SimpleMemberAccessExpression); | |||||
} | |||||
private static void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context) | |||||
{ | |||||
// Bail out if the accessed member isn't named 'Guild' | |||||
var memberAccessSymbol = context.SemanticModel.GetSymbolInfo(context.Node).Symbol; | |||||
if (memberAccessSymbol.Name != "Guild") | |||||
return; | |||||
// Bail out if it happens to be 'ContextType.Guild' in the '[RequireContext]' argument | |||||
if (context.Node.Parent is AttributeArgumentSyntax) | |||||
return; | |||||
// Bail out if the containing class doesn't derive from 'ModuleBase<T>' | |||||
var typeNode = context.Node.FirstAncestorOrSelf<TypeDeclarationSyntax>(); | |||||
var typeSymbol = context.SemanticModel.GetDeclaredSymbol(typeNode); | |||||
if (!typeSymbol.DerivesFromModuleBase()) | |||||
return; | |||||
// Bail out if the containing method isn't marked with '[Command]' | |||||
var methodNode = context.Node.FirstAncestorOrSelf<MethodDeclarationSyntax>(); | |||||
var methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodNode); | |||||
var methodAttributes = methodSymbol.GetAttributes(); | |||||
if (!methodAttributes.Any(a => a.AttributeClass.Name == nameof(CommandAttribute))) | |||||
return; | |||||
// Is the '[RequireContext]' attribute not applied to either the | |||||
// method or the class, or its argument isn't 'ContextType.Guild'? | |||||
var ctxAttribute = methodAttributes.SingleOrDefault(_attributeDataPredicate) | |||||
?? typeSymbol.GetAttributes().SingleOrDefault(_attributeDataPredicate); | |||||
if (ctxAttribute == null || ctxAttribute.ConstructorArguments.Any(arg => !arg.Value.Equals((int)ContextType.Guild))) | |||||
{ | |||||
// Report the diagnostic | |||||
var diagnostic = Diagnostic.Create(Rule, context.Node.GetLocation(), methodSymbol.Name); | |||||
context.ReportDiagnostic(diagnostic); | |||||
} | |||||
} | |||||
private static readonly Func<AttributeData, bool> _attributeDataPredicate = | |||||
(a => a.AttributeClass.Name == nameof(RequireContextAttribute)); | |||||
} | |||||
} |
@@ -0,0 +1,21 @@ | |||||
using System; | |||||
using Microsoft.CodeAnalysis; | |||||
using Discord.Commands; | |||||
namespace Discord.Analyzers | |||||
{ | |||||
internal static class SymbolExtensions | |||||
{ | |||||
private static readonly string _moduleBaseName = typeof(ModuleBase<>).Name; | |||||
public static bool DerivesFromModuleBase(this ITypeSymbol symbol) | |||||
{ | |||||
for (var bType = symbol.BaseType; bType != null; bType = bType.BaseType) | |||||
{ | |||||
if (bType.MetadataName == _moduleBaseName) | |||||
return true; | |||||
} | |||||
return false; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,30 @@ | |||||
# DNET0001 | |||||
<table> | |||||
<tr> | |||||
<td>TypeName</td> | |||||
<td>GuildAccessAnalyzer</td> | |||||
</tr> | |||||
<tr> | |||||
<td>CheckId</td> | |||||
<td>DNET0001</td> | |||||
</tr> | |||||
<tr> | |||||
<td>Category</td> | |||||
<td>API Usage</td> | |||||
</tr> | |||||
</table> | |||||
## Cause | |||||
A method identified as a command is accessing `Context.Guild` without the requisite precondition. | |||||
## Rule description | |||||
The value of `Context.Guild` is `null` if a command is invoked in a DM channel. Attempting to access | |||||
guild properties in such a case will result in a `NullReferenceException` at runtime. | |||||
This exception is entirely avoidable by using the library's provided preconditions. | |||||
## How to fix violations | |||||
Add the precondition `[RequireContext(ContextType.Guild)]` to the command or module class. |
@@ -3,7 +3,7 @@ using System; | |||||
namespace Discord.Commands | namespace Discord.Commands | ||||
{ | { | ||||
/// <summary> Provides aliases for a command. </summary> | /// <summary> Provides aliases for a command. </summary> | ||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] | |||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] | |||||
public class AliasAttribute : Attribute | public class AliasAttribute : Attribute | ||||
{ | { | ||||
/// <summary> The aliases which have been defined for the command. </summary> | /// <summary> The aliases which have been defined for the command. </summary> | ||||
@@ -1,8 +1,8 @@ | |||||
using System; | |||||
using System; | |||||
namespace Discord.Commands | namespace Discord.Commands | ||||
{ | { | ||||
[AttributeUsage(AttributeTargets.Method)] | |||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] | |||||
public class CommandAttribute : Attribute | public class CommandAttribute : Attribute | ||||
{ | { | ||||
public string Text { get; } | public string Text { get; } | ||||
@@ -1,8 +1,8 @@ | |||||
using System; | |||||
using System; | |||||
namespace Discord.Commands | namespace Discord.Commands | ||||
{ | { | ||||
[AttributeUsage(AttributeTargets.Class)] | |||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] | |||||
public class DontAutoLoadAttribute : Attribute | public class DontAutoLoadAttribute : Attribute | ||||
{ | { | ||||
} | } | ||||
@@ -2,7 +2,7 @@ using System; | |||||
namespace Discord.Commands { | namespace Discord.Commands { | ||||
[AttributeUsage(AttributeTargets.Property)] | |||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] | |||||
public class DontInjectAttribute : Attribute { | public class DontInjectAttribute : Attribute { | ||||
} | } | ||||
@@ -1,8 +1,8 @@ | |||||
using System; | |||||
using System; | |||||
namespace Discord.Commands | namespace Discord.Commands | ||||
{ | { | ||||
[AttributeUsage(AttributeTargets.Class)] | |||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] | |||||
public class GroupAttribute : Attribute | public class GroupAttribute : Attribute | ||||
{ | { | ||||
public string Prefix { get; } | public string Prefix { get; } | ||||
@@ -3,7 +3,7 @@ using System; | |||||
namespace Discord.Commands | namespace Discord.Commands | ||||
{ | { | ||||
// Override public name of command/module | // Override public name of command/module | ||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter)] | |||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] | |||||
public class NameAttribute : Attribute | public class NameAttribute : Attribute | ||||
{ | { | ||||
public string Text { get; } | public string Text { get; } | ||||
@@ -4,7 +4,7 @@ using System.Reflection; | |||||
namespace Discord.Commands | namespace Discord.Commands | ||||
{ | { | ||||
[AttributeUsage(AttributeTargets.Parameter)] | |||||
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] | |||||
public class OverrideTypeReaderAttribute : Attribute | public class OverrideTypeReaderAttribute : Attribute | ||||
{ | { | ||||
private static readonly TypeInfo _typeReaderTypeInfo = typeof(TypeReader).GetTypeInfo(); | private static readonly TypeInfo _typeReaderTypeInfo = typeof(TypeReader).GetTypeInfo(); | ||||
@@ -19,4 +19,4 @@ namespace Discord.Commands | |||||
TypeReader = overridenTypeReader; | TypeReader = overridenTypeReader; | ||||
} | } | ||||
} | } | ||||
} | |||||
} |
@@ -1,6 +1,5 @@ | |||||
using System; | using System; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
using Microsoft.Extensions.DependencyInjection; | |||||
namespace Discord.Commands | namespace Discord.Commands | ||||
{ | { | ||||
@@ -9,4 +8,4 @@ namespace Discord.Commands | |||||
{ | { | ||||
public abstract Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, ParameterInfo parameter, object value, IServiceProvider services); | public abstract Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, ParameterInfo parameter, object value, IServiceProvider services); | ||||
} | } | ||||
} | |||||
} |
@@ -1,4 +1,4 @@ | |||||
using System; | |||||
using System; | |||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
using Microsoft.Extensions.DependencyInjection; | using Microsoft.Extensions.DependencyInjection; | ||||
@@ -15,7 +15,7 @@ namespace Discord.Commands | |||||
/// <summary> | /// <summary> | ||||
/// Require that the command be invoked in a specified context. | /// Require that the command be invoked in a specified context. | ||||
/// </summary> | /// </summary> | ||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] | |||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] | |||||
public class RequireContextAttribute : PreconditionAttribute | public class RequireContextAttribute : PreconditionAttribute | ||||
{ | { | ||||
public ContextType Contexts { get; } | public ContextType Contexts { get; } | ||||
@@ -1,4 +1,4 @@ | |||||
using System; | |||||
using System; | |||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
namespace Discord.Commands | namespace Discord.Commands | ||||
@@ -6,7 +6,7 @@ namespace Discord.Commands | |||||
/// <summary> | /// <summary> | ||||
/// Require that the command is invoked in a channel marked NSFW | /// Require that the command is invoked in a channel marked NSFW | ||||
/// </summary> | /// </summary> | ||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] | |||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] | |||||
public class RequireNsfwAttribute : PreconditionAttribute | public class RequireNsfwAttribute : PreconditionAttribute | ||||
{ | { | ||||
public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) | public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) | ||||
@@ -1,7 +1,5 @@ | |||||
#pragma warning disable CS0618 | |||||
using System; | using System; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
using Microsoft.Extensions.DependencyInjection; | |||||
namespace Discord.Commands | namespace Discord.Commands | ||||
{ | { | ||||
@@ -9,7 +7,7 @@ namespace Discord.Commands | |||||
/// Require that the command is invoked by the owner of the bot. | /// Require that the command is invoked by the owner of the bot. | ||||
/// </summary> | /// </summary> | ||||
/// <remarks>This precondition will only work if the bot is a bot account.</remarks> | /// <remarks>This precondition will only work if the bot is a bot account.</remarks> | ||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] | |||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] | |||||
public class RequireOwnerAttribute : PreconditionAttribute | public class RequireOwnerAttribute : PreconditionAttribute | ||||
{ | { | ||||
public override async Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) | public override async Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) | ||||
@@ -21,10 +19,6 @@ namespace Discord.Commands | |||||
if (context.User.Id != application.Owner.Id) | if (context.User.Id != application.Owner.Id) | ||||
return PreconditionResult.FromError("Command can only be run by the owner of the bot"); | return PreconditionResult.FromError("Command can only be run by the owner of the bot"); | ||||
return PreconditionResult.FromSuccess(); | return PreconditionResult.FromSuccess(); | ||||
case TokenType.User: | |||||
if (context.User.Id != context.Client.CurrentUser.Id) | |||||
return PreconditionResult.FromError("Command can only be run by the owner of the bot"); | |||||
return PreconditionResult.FromSuccess(); | |||||
default: | default: | ||||
return PreconditionResult.FromError($"{nameof(RequireOwnerAttribute)} is not supported by this {nameof(TokenType)}."); | return PreconditionResult.FromError($"{nameof(RequireOwnerAttribute)} is not supported by this {nameof(TokenType)}."); | ||||
} | } | ||||
@@ -3,7 +3,7 @@ using System; | |||||
namespace Discord.Commands | namespace Discord.Commands | ||||
{ | { | ||||
/// <summary> Sets priority of commands </summary> | /// <summary> Sets priority of commands </summary> | ||||
[AttributeUsage(AttributeTargets.Method)] | |||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] | |||||
public class PriorityAttribute : Attribute | public class PriorityAttribute : Attribute | ||||
{ | { | ||||
/// <summary> The priority which has been set for the command </summary> | /// <summary> The priority which has been set for the command </summary> | ||||
@@ -1,8 +1,8 @@ | |||||
using System; | |||||
using System; | |||||
namespace Discord.Commands | namespace Discord.Commands | ||||
{ | { | ||||
[AttributeUsage(AttributeTargets.Parameter)] | |||||
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] | |||||
public class RemainderAttribute : Attribute | public class RemainderAttribute : Attribute | ||||
{ | { | ||||
} | } | ||||
@@ -1,9 +1,9 @@ | |||||
using System; | |||||
using System; | |||||
namespace Discord.Commands | namespace Discord.Commands | ||||
{ | { | ||||
// Extension of the Cosmetic Summary, for Groups, Commands, and Parameters | // Extension of the Cosmetic Summary, for Groups, Commands, and Parameters | ||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] | |||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)] | |||||
public class RemarksAttribute : Attribute | public class RemarksAttribute : Attribute | ||||
{ | { | ||||
public string Text { get; } | public string Text { get; } | ||||
@@ -1,9 +1,9 @@ | |||||
using System; | |||||
using System; | |||||
namespace Discord.Commands | namespace Discord.Commands | ||||
{ | { | ||||
// Cosmetic Summary, for Groups and Commands | // Cosmetic Summary, for Groups and Commands | ||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter)] | |||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] | |||||
public class SummaryAttribute : Attribute | public class SummaryAttribute : Attribute | ||||
{ | { | ||||
public string Text { get; } | public string Text { get; } | ||||
@@ -1,5 +1,6 @@ | |||||
using System; | |||||
using System; | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Reflection; | |||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
using Microsoft.Extensions.DependencyInjection; | using Microsoft.Extensions.DependencyInjection; | ||||
@@ -18,6 +19,7 @@ namespace Discord.Commands.Builders | |||||
public string Name { get; set; } | public string Name { get; set; } | ||||
public string Summary { get; set; } | public string Summary { get; set; } | ||||
public string Remarks { get; set; } | public string Remarks { get; set; } | ||||
public string Group { get; set; } | |||||
public IReadOnlyList<CommandBuilder> Commands => _commands; | public IReadOnlyList<CommandBuilder> Commands => _commands; | ||||
public IReadOnlyList<ModuleBuilder> Modules => _submodules; | public IReadOnlyList<ModuleBuilder> Modules => _submodules; | ||||
@@ -25,6 +27,8 @@ namespace Discord.Commands.Builders | |||||
public IReadOnlyList<Attribute> Attributes => _attributes; | public IReadOnlyList<Attribute> Attributes => _attributes; | ||||
public IReadOnlyList<string> Aliases => _aliases; | public IReadOnlyList<string> Aliases => _aliases; | ||||
internal TypeInfo TypeInfo { get; set; } | |||||
//Automatic | //Automatic | ||||
internal ModuleBuilder(CommandService service, ModuleBuilder parent) | internal ModuleBuilder(CommandService service, ModuleBuilder parent) | ||||
{ | { | ||||
@@ -111,17 +115,23 @@ namespace Discord.Commands.Builders | |||||
return this; | return this; | ||||
} | } | ||||
private ModuleInfo BuildImpl(CommandService service, ModuleInfo parent = null) | |||||
private ModuleInfo BuildImpl(CommandService service, IServiceProvider services, ModuleInfo parent = null) | |||||
{ | { | ||||
//Default name to first alias | //Default name to first alias | ||||
if (Name == null) | if (Name == null) | ||||
Name = _aliases[0]; | Name = _aliases[0]; | ||||
return new ModuleInfo(this, service, parent); | |||||
if (TypeInfo != null) | |||||
{ | |||||
var moduleInstance = ReflectionUtils.CreateObject<IModuleBase>(TypeInfo, service, services); | |||||
moduleInstance.OnModuleBuilding(service, this); | |||||
} | |||||
return new ModuleInfo(this, service, services, parent); | |||||
} | } | ||||
public ModuleInfo Build(CommandService service) => BuildImpl(service); | |||||
public ModuleInfo Build(CommandService service, IServiceProvider services) => BuildImpl(service, services); | |||||
internal ModuleInfo Build(CommandService service, ModuleInfo parent) => BuildImpl(service, parent); | |||||
internal ModuleInfo Build(CommandService service, IServiceProvider services, ModuleInfo parent) => BuildImpl(service, services, parent); | |||||
} | } | ||||
} | } |
@@ -42,14 +42,13 @@ namespace Discord.Commands | |||||
} | } | ||||
public static Task<Dictionary<Type, ModuleInfo>> BuildAsync(CommandService service, params TypeInfo[] validTypes) => BuildAsync(validTypes, service); | |||||
public static async Task<Dictionary<Type, ModuleInfo>> BuildAsync(IEnumerable<TypeInfo> validTypes, CommandService service) | |||||
public static Task<Dictionary<Type, ModuleInfo>> BuildAsync(CommandService service, IServiceProvider services, params TypeInfo[] validTypes) => BuildAsync(validTypes, service, services); | |||||
public static async Task<Dictionary<Type, ModuleInfo>> BuildAsync(IEnumerable<TypeInfo> validTypes, CommandService service, IServiceProvider services) | |||||
{ | { | ||||
/*if (!validTypes.Any()) | /*if (!validTypes.Any()) | ||||
throw new InvalidOperationException("Could not find any valid modules from the given selection");*/ | throw new InvalidOperationException("Could not find any valid modules from the given selection");*/ | ||||
var topLevelGroups = validTypes.Where(x => x.DeclaringType == null); | |||||
var subGroups = validTypes.Intersect(topLevelGroups); | |||||
var topLevelGroups = validTypes.Where(x => x.DeclaringType == null || !IsValidModuleDefinition(x.DeclaringType.GetTypeInfo())); | |||||
var builtTypes = new List<TypeInfo>(); | var builtTypes = new List<TypeInfo>(); | ||||
@@ -63,11 +62,11 @@ namespace Discord.Commands | |||||
var module = new ModuleBuilder(service, null); | var module = new ModuleBuilder(service, null); | ||||
BuildModule(module, typeInfo, service); | |||||
BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service); | |||||
BuildModule(module, typeInfo, service, services); | |||||
BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service, services); | |||||
builtTypes.Add(typeInfo); | builtTypes.Add(typeInfo); | ||||
result[typeInfo.AsType()] = module.Build(service); | |||||
result[typeInfo.AsType()] = module.Build(service, services); | |||||
} | } | ||||
await service._cmdLogger.DebugAsync($"Successfully built {builtTypes.Count} modules.").ConfigureAwait(false); | await service._cmdLogger.DebugAsync($"Successfully built {builtTypes.Count} modules.").ConfigureAwait(false); | ||||
@@ -75,7 +74,7 @@ namespace Discord.Commands | |||||
return result; | return result; | ||||
} | } | ||||
private static void BuildSubTypes(ModuleBuilder builder, IEnumerable<TypeInfo> subTypes, List<TypeInfo> builtTypes, CommandService service) | |||||
private static void BuildSubTypes(ModuleBuilder builder, IEnumerable<TypeInfo> subTypes, List<TypeInfo> builtTypes, CommandService service, IServiceProvider services) | |||||
{ | { | ||||
foreach (var typeInfo in subTypes) | foreach (var typeInfo in subTypes) | ||||
{ | { | ||||
@@ -87,17 +86,18 @@ namespace Discord.Commands | |||||
builder.AddModule((module) => | builder.AddModule((module) => | ||||
{ | { | ||||
BuildModule(module, typeInfo, service); | |||||
BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service); | |||||
BuildModule(module, typeInfo, service, services); | |||||
BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service, services); | |||||
}); | }); | ||||
builtTypes.Add(typeInfo); | builtTypes.Add(typeInfo); | ||||
} | } | ||||
} | } | ||||
private static void BuildModule(ModuleBuilder builder, TypeInfo typeInfo, CommandService service) | |||||
private static void BuildModule(ModuleBuilder builder, TypeInfo typeInfo, CommandService service, IServiceProvider services) | |||||
{ | { | ||||
var attributes = typeInfo.GetCustomAttributes(); | var attributes = typeInfo.GetCustomAttributes(); | ||||
builder.TypeInfo = typeInfo; | |||||
foreach (var attribute in attributes) | foreach (var attribute in attributes) | ||||
{ | { | ||||
@@ -117,6 +117,7 @@ namespace Discord.Commands | |||||
break; | break; | ||||
case GroupAttribute group: | case GroupAttribute group: | ||||
builder.Name = builder.Name ?? group.Prefix; | builder.Name = builder.Name ?? group.Prefix; | ||||
builder.Group = group.Prefix; | |||||
builder.AddAliases(group.Prefix); | builder.AddAliases(group.Prefix); | ||||
break; | break; | ||||
case PreconditionAttribute precondition: | case PreconditionAttribute precondition: | ||||
@@ -140,12 +141,12 @@ namespace Discord.Commands | |||||
{ | { | ||||
builder.AddCommand((command) => | builder.AddCommand((command) => | ||||
{ | { | ||||
BuildCommand(command, typeInfo, method, service); | |||||
BuildCommand(command, typeInfo, method, service, services); | |||||
}); | }); | ||||
} | } | ||||
} | } | ||||
private static void BuildCommand(CommandBuilder builder, TypeInfo typeInfo, MethodInfo method, CommandService service) | |||||
private static void BuildCommand(CommandBuilder builder, TypeInfo typeInfo, MethodInfo method, CommandService service, IServiceProvider serviceprovider) | |||||
{ | { | ||||
var attributes = method.GetCustomAttributes(); | var attributes = method.GetCustomAttributes(); | ||||
@@ -191,7 +192,7 @@ namespace Discord.Commands | |||||
{ | { | ||||
builder.AddParameter((parameter) => | builder.AddParameter((parameter) => | ||||
{ | { | ||||
BuildParameter(parameter, paramInfo, pos++, count, service); | |||||
BuildParameter(parameter, paramInfo, pos++, count, service, serviceprovider); | |||||
}); | }); | ||||
} | } | ||||
@@ -227,7 +228,7 @@ namespace Discord.Commands | |||||
builder.Callback = ExecuteCallback; | builder.Callback = ExecuteCallback; | ||||
} | } | ||||
private static void BuildParameter(ParameterBuilder builder, System.Reflection.ParameterInfo paramInfo, int position, int count, CommandService service) | |||||
private static void BuildParameter(ParameterBuilder builder, System.Reflection.ParameterInfo paramInfo, int position, int count, CommandService service, IServiceProvider services) | |||||
{ | { | ||||
var attributes = paramInfo.GetCustomAttributes(); | var attributes = paramInfo.GetCustomAttributes(); | ||||
var paramType = paramInfo.ParameterType; | var paramType = paramInfo.ParameterType; | ||||
@@ -245,7 +246,7 @@ namespace Discord.Commands | |||||
builder.Summary = summary.Text; | builder.Summary = summary.Text; | ||||
break; | break; | ||||
case OverrideTypeReaderAttribute typeReader: | case OverrideTypeReaderAttribute typeReader: | ||||
builder.TypeReader = GetTypeReader(service, paramType, typeReader.TypeReader); | |||||
builder.TypeReader = GetTypeReader(service, paramType, typeReader.TypeReader, services); | |||||
break; | break; | ||||
case ParamArrayAttribute _: | case ParamArrayAttribute _: | ||||
builder.IsMultiple = true; | builder.IsMultiple = true; | ||||
@@ -273,19 +274,12 @@ namespace Discord.Commands | |||||
if (builder.TypeReader == null) | if (builder.TypeReader == null) | ||||
{ | { | ||||
var readers = service.GetTypeReaders(paramType); | |||||
TypeReader reader = null; | |||||
if (readers != null) | |||||
reader = readers.FirstOrDefault().Value; | |||||
else | |||||
reader = service.GetDefaultTypeReader(paramType); | |||||
builder.TypeReader = reader; | |||||
builder.TypeReader = service.GetDefaultTypeReader(paramType) | |||||
?? service.GetTypeReaders(paramType)?.FirstOrDefault().Value; | |||||
} | } | ||||
} | } | ||||
private static TypeReader GetTypeReader(CommandService service, Type paramType, Type typeReaderType) | |||||
private static TypeReader GetTypeReader(CommandService service, Type paramType, Type typeReaderType, IServiceProvider services) | |||||
{ | { | ||||
var readers = service.GetTypeReaders(paramType); | var readers = service.GetTypeReaders(paramType); | ||||
TypeReader reader = null; | TypeReader reader = null; | ||||
@@ -296,8 +290,8 @@ namespace Discord.Commands | |||||
} | } | ||||
//We dont have a cached type reader, create one | //We dont have a cached type reader, create one | ||||
reader = ReflectionUtils.CreateObject<TypeReader>(typeReaderType.GetTypeInfo(), service, EmptyServiceProvider.Instance); | |||||
service.AddTypeReader(paramType, reader); | |||||
reader = ReflectionUtils.CreateObject<TypeReader>(typeReaderType.GetTypeInfo(), service, services); | |||||
service.AddTypeReader(paramType, reader, false); | |||||
return reader; | return reader; | ||||
} | } | ||||
@@ -305,7 +299,8 @@ namespace Discord.Commands | |||||
private static bool IsValidModuleDefinition(TypeInfo typeInfo) | private static bool IsValidModuleDefinition(TypeInfo typeInfo) | ||||
{ | { | ||||
return _moduleTypeInfo.IsAssignableFrom(typeInfo) && | return _moduleTypeInfo.IsAssignableFrom(typeInfo) && | ||||
!typeInfo.IsAbstract; | |||||
!typeInfo.IsAbstract && | |||||
!typeInfo.ContainsGenericParameters; | |||||
} | } | ||||
private static bool IsValidCommandDefinition(MethodInfo methodInfo) | private static bool IsValidCommandDefinition(MethodInfo methodInfo) | ||||
@@ -1,5 +1,3 @@ | |||||
using Discord.Commands.Builders; | |||||
using Discord.Logging; | |||||
using System; | using System; | ||||
using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
@@ -8,6 +6,9 @@ using System.Linq; | |||||
using System.Reflection; | using System.Reflection; | ||||
using System.Threading; | using System.Threading; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
using Microsoft.Extensions.DependencyInjection; | |||||
using Discord.Commands.Builders; | |||||
using Discord.Logging; | |||||
namespace Discord.Commands | namespace Discord.Commands | ||||
{ | { | ||||
@@ -85,7 +86,8 @@ namespace Discord.Commands | |||||
var builder = new ModuleBuilder(this, null, primaryAlias); | var builder = new ModuleBuilder(this, null, primaryAlias); | ||||
buildFunc(builder); | buildFunc(builder); | ||||
var module = builder.Build(this); | |||||
var module = builder.Build(this, null); | |||||
return LoadModuleInternal(module); | return LoadModuleInternal(module); | ||||
} | } | ||||
finally | finally | ||||
@@ -93,9 +95,18 @@ namespace Discord.Commands | |||||
_moduleLock.Release(); | _moduleLock.Release(); | ||||
} | } | ||||
} | } | ||||
public Task<ModuleInfo> AddModuleAsync<T>() => AddModuleAsync(typeof(T)); | |||||
public async Task<ModuleInfo> AddModuleAsync(Type type) | |||||
/// <summary> | |||||
/// Add a command module from a type | |||||
/// </summary> | |||||
/// <typeparam name="T">The type of module</typeparam> | |||||
/// <param name="services">An IServiceProvider for your dependency injection solution, if using one - otherwise, pass null</param> | |||||
/// <returns>A built module</returns> | |||||
public Task<ModuleInfo> AddModuleAsync<T>(IServiceProvider services) => AddModuleAsync(typeof(T), services); | |||||
public async Task<ModuleInfo> AddModuleAsync(Type type, IServiceProvider services) | |||||
{ | { | ||||
services = services ?? EmptyServiceProvider.Instance; | |||||
await _moduleLock.WaitAsync().ConfigureAwait(false); | await _moduleLock.WaitAsync().ConfigureAwait(false); | ||||
try | try | ||||
{ | { | ||||
@@ -104,7 +115,7 @@ namespace Discord.Commands | |||||
if (_typedModuleDefs.ContainsKey(type)) | if (_typedModuleDefs.ContainsKey(type)) | ||||
throw new ArgumentException($"This module has already been added."); | throw new ArgumentException($"This module has already been added."); | ||||
var module = (await ModuleClassBuilder.BuildAsync(this, typeInfo).ConfigureAwait(false)).FirstOrDefault(); | |||||
var module = (await ModuleClassBuilder.BuildAsync(this, services, typeInfo).ConfigureAwait(false)).FirstOrDefault(); | |||||
if (module.Value == default(ModuleInfo)) | if (module.Value == default(ModuleInfo)) | ||||
throw new InvalidOperationException($"Could not build the module {type.FullName}, did you pass an invalid type?"); | throw new InvalidOperationException($"Could not build the module {type.FullName}, did you pass an invalid type?"); | ||||
@@ -118,13 +129,21 @@ namespace Discord.Commands | |||||
_moduleLock.Release(); | _moduleLock.Release(); | ||||
} | } | ||||
} | } | ||||
public async Task<IEnumerable<ModuleInfo>> AddModulesAsync(Assembly assembly) | |||||
/// <summary> | |||||
/// Add command modules from an assembly | |||||
/// </summary> | |||||
/// <param name="assembly">The assembly containing command modules</param> | |||||
/// <param name="services">An IServiceProvider for your dependency injection solution, if using one - otherwise, pass null</param> | |||||
/// <returns>A collection of built modules</returns> | |||||
public async Task<IEnumerable<ModuleInfo>> AddModulesAsync(Assembly assembly, IServiceProvider services) | |||||
{ | { | ||||
services = services ?? EmptyServiceProvider.Instance; | |||||
await _moduleLock.WaitAsync().ConfigureAwait(false); | await _moduleLock.WaitAsync().ConfigureAwait(false); | ||||
try | try | ||||
{ | { | ||||
var types = await ModuleClassBuilder.SearchAsync(assembly, this).ConfigureAwait(false); | var types = await ModuleClassBuilder.SearchAsync(assembly, this).ConfigureAwait(false); | ||||
var moduleDefs = await ModuleClassBuilder.BuildAsync(types, this).ConfigureAwait(false); | |||||
var moduleDefs = await ModuleClassBuilder.BuildAsync(types, this, services).ConfigureAwait(false); | |||||
foreach (var info in moduleDefs) | foreach (var info in moduleDefs) | ||||
{ | { | ||||
@@ -196,10 +215,11 @@ namespace Discord.Commands | |||||
return true; | return true; | ||||
} | } | ||||
//Type Readers | |||||
//Type Readers | |||||
/// <summary> | /// <summary> | ||||
/// Adds a custom <see cref="TypeReader"/> to this <see cref="CommandService"/> for the supplied object type. | /// Adds a custom <see cref="TypeReader"/> to this <see cref="CommandService"/> for the supplied object type. | ||||
/// If <typeparamref name="T"/> is a <see cref="ValueType"/>, a <see cref="NullableTypeReader{T}"/> will also be added. | /// If <typeparamref name="T"/> is a <see cref="ValueType"/>, a <see cref="NullableTypeReader{T}"/> will also be added. | ||||
/// If a default <see cref="TypeReader"/> exists for <typeparamref name="T"/>, a warning will be logged and the default <see cref="TypeReader"/> will be replaced. | |||||
/// </summary> | /// </summary> | ||||
/// <typeparam name="T">The object type to be read by the <see cref="TypeReader"/>.</typeparam> | /// <typeparam name="T">The object type to be read by the <see cref="TypeReader"/>.</typeparam> | ||||
/// <param name="reader">An instance of the <see cref="TypeReader"/> to be added.</param> | /// <param name="reader">An instance of the <see cref="TypeReader"/> to be added.</param> | ||||
@@ -207,24 +227,61 @@ namespace Discord.Commands | |||||
=> AddTypeReader(typeof(T), reader); | => AddTypeReader(typeof(T), reader); | ||||
/// <summary> | /// <summary> | ||||
/// Adds a custom <see cref="TypeReader"/> to this <see cref="CommandService"/> for the supplied object type. | /// Adds a custom <see cref="TypeReader"/> to this <see cref="CommandService"/> for the supplied object type. | ||||
/// If <paramref name="type"/> is a <see cref="ValueType"/>, a <see cref="NullableTypeReader{T}"/> for the value type will also be added. | |||||
/// If <paramref name="type"/> is a <see cref="ValueType"/>, a <see cref="NullableTypeReader{T}"/> for the value type will also be added. | |||||
/// If a default <see cref="TypeReader"/> exists for <paramref name="type"/>, a warning will be logged and the default <see cref="TypeReader"/> will be replaced. | |||||
/// </summary> | /// </summary> | ||||
/// <param name="type">A <see cref="Type"/> instance for the type to be read.</param> | /// <param name="type">A <see cref="Type"/> instance for the type to be read.</param> | ||||
/// <param name="reader">An instance of the <see cref="TypeReader"/> to be added.</param> | /// <param name="reader">An instance of the <see cref="TypeReader"/> to be added.</param> | ||||
public void AddTypeReader(Type type, TypeReader reader) | public void AddTypeReader(Type type, TypeReader reader) | ||||
{ | { | ||||
var readers = _typeReaders.GetOrAdd(type, x => new ConcurrentDictionary<Type, TypeReader>()); | |||||
readers[reader.GetType()] = 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<T>(reader, true)."); | |||||
AddTypeReader(type, reader, true); | |||||
} | |||||
/// <summary> | |||||
/// Adds a custom <see cref="TypeReader"/> to this <see cref="CommandService"/> for the supplied object type. | |||||
/// If <typeparamref name="T"/> is a <see cref="ValueType"/>, a <see cref="NullableTypeReader{T}"/> will also be added. | |||||
/// </summary> | |||||
/// <typeparam name="T">The object type to be read by the <see cref="TypeReader"/>.</typeparam> | |||||
/// <param name="reader">An instance of the <see cref="TypeReader"/> to be added.</param> | |||||
/// <param name="replaceDefault">If <paramref name="reader"/> should replace the default <see cref="TypeReader"/> for <typeparamref name="T"/> if one exists.</param> | |||||
public void AddTypeReader<T>(TypeReader reader, bool replaceDefault) | |||||
=> AddTypeReader(typeof(T), reader, replaceDefault); | |||||
/// <summary> | |||||
/// Adds a custom <see cref="TypeReader"/> to this <see cref="CommandService"/> for the supplied object type. | |||||
/// If <paramref name="type"/> is a <see cref="ValueType"/>, a <see cref="NullableTypeReader{T}"/> for the value type will also be added. | |||||
/// </summary> | |||||
/// <param name="type">A <see cref="Type"/> instance for the type to be read.</param> | |||||
/// <param name="reader">An instance of the <see cref="TypeReader"/> to be added.</param> | |||||
/// <param name="replaceDefault">If <paramref name="reader"/> should replace the default <see cref="TypeReader"/> for <paramref name="type"/> if one exists.</param> | |||||
public void AddTypeReader(Type type, TypeReader reader, bool replaceDefault) | |||||
{ | |||||
if (replaceDefault && _defaultTypeReaders.ContainsKey(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<Type, TypeReader>()); | |||||
readers[reader.GetType()] = reader; | |||||
if (type.GetTypeInfo().IsValueType) | |||||
AddNullableTypeReader(type, reader); | |||||
if (type.GetTypeInfo().IsValueType) | |||||
AddNullableTypeReader(type, reader); | |||||
} | |||||
} | } | ||||
internal void AddNullableTypeReader(Type valueType, TypeReader valueTypeReader) | internal void AddNullableTypeReader(Type valueType, TypeReader valueTypeReader) | ||||
{ | { | ||||
var readers = _typeReaders.GetOrAdd(typeof(Nullable<>).MakeGenericType(valueType), x => new ConcurrentDictionary<Type, TypeReader>()); | var readers = _typeReaders.GetOrAdd(typeof(Nullable<>).MakeGenericType(valueType), x => new ConcurrentDictionary<Type, TypeReader>()); | ||||
var nullableReader = NullableTypeReader.Create(valueType, valueTypeReader); | var nullableReader = NullableTypeReader.Create(valueType, valueTypeReader); | ||||
readers[nullableReader.GetType()] = nullableReader; | readers[nullableReader.GetType()] = nullableReader; | ||||
} | |||||
} | |||||
internal IDictionary<Type, TypeReader> GetTypeReaders(Type type) | internal IDictionary<Type, TypeReader> GetTypeReaders(Type type) | ||||
{ | { | ||||
if (_typeReaders.TryGetValue(type, out var definedTypeReaders)) | if (_typeReaders.TryGetValue(type, out var definedTypeReaders)) | ||||
@@ -272,9 +329,9 @@ namespace Discord.Commands | |||||
return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command."); | return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command."); | ||||
} | } | ||||
public Task<IResult> ExecuteAsync(ICommandContext context, int argPos, IServiceProvider services = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) | |||||
public Task<IResult> ExecuteAsync(ICommandContext context, int argPos, IServiceProvider services, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) | |||||
=> ExecuteAsync(context, context.Message.Content.Substring(argPos), services, multiMatchHandling); | => ExecuteAsync(context, context.Message.Content.Substring(argPos), services, multiMatchHandling); | ||||
public async Task<IResult> ExecuteAsync(ICommandContext context, string input, IServiceProvider services = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) | |||||
public async Task<IResult> ExecuteAsync(ICommandContext context, string input, IServiceProvider services, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) | |||||
{ | { | ||||
services = services ?? EmptyServiceProvider.Instance; | services = services ?? EmptyServiceProvider.Instance; | ||||
@@ -330,7 +387,7 @@ namespace Discord.Commands | |||||
float CalculateScore(CommandMatch match, ParseResult parseResult) | float CalculateScore(CommandMatch match, ParseResult parseResult) | ||||
{ | { | ||||
float argValuesScore = 0, paramValuesScore = 0; | float argValuesScore = 0, paramValuesScore = 0; | ||||
if (match.Command.Parameters.Count > 0) | if (match.Command.Parameters.Count > 0) | ||||
{ | { | ||||
var argValuesSum = parseResult.ArgValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; | var argValuesSum = parseResult.ArgValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; | ||||
@@ -1,4 +1,6 @@ | |||||
namespace Discord.Commands | |||||
using System; | |||||
namespace Discord.Commands | |||||
{ | { | ||||
public class CommandServiceConfig | public class CommandServiceConfig | ||||
{ | { | ||||
@@ -18,5 +20,11 @@ | |||||
/// <summary> Determines whether extra parameters should be ignored. </summary> | /// <summary> Determines whether extra parameters should be ignored. </summary> | ||||
public bool IgnoreExtraArgs { get; set; } = false; | public bool IgnoreExtraArgs { get; set; } = false; | ||||
///// <summary> Gets or sets the <see cref="IServiceProvider"/> to use. </summary> | |||||
//public IServiceProvider ServiceProvider { get; set; } = null; | |||||
///// <summary> Gets or sets a factory function for the <see cref="IServiceProvider"/> to use. </summary> | |||||
//public Func<CommandService, IServiceProvider> ServiceProviderFactory { get; set; } = null; | |||||
} | } | ||||
} | } |
@@ -1,4 +1,6 @@ | |||||
namespace Discord.Commands | |||||
using Discord.Commands.Builders; | |||||
namespace Discord.Commands | |||||
{ | { | ||||
internal interface IModuleBase | internal interface IModuleBase | ||||
{ | { | ||||
@@ -7,5 +9,7 @@ | |||||
void BeforeExecute(CommandInfo command); | void BeforeExecute(CommandInfo command); | ||||
void AfterExecute(CommandInfo command); | void AfterExecute(CommandInfo command); | ||||
void OnModuleBuilding(CommandService commandService, ModuleBuilder builder); | |||||
} | } | ||||
} | } |
@@ -165,11 +165,11 @@ namespace Discord.Commands | |||||
switch (RunMode) | switch (RunMode) | ||||
{ | { | ||||
case RunMode.Sync: //Always sync | case RunMode.Sync: //Always sync | ||||
return await ExecuteAsyncInternalAsync(context, args, services).ConfigureAwait(false); | |||||
return await ExecuteInternalAsync(context, args, services).ConfigureAwait(false); | |||||
case RunMode.Async: //Always async | case RunMode.Async: //Always async | ||||
var t2 = Task.Run(async () => | var t2 = Task.Run(async () => | ||||
{ | { | ||||
await ExecuteAsyncInternalAsync(context, args, services).ConfigureAwait(false); | |||||
await ExecuteInternalAsync(context, args, services).ConfigureAwait(false); | |||||
}); | }); | ||||
break; | break; | ||||
} | } | ||||
@@ -181,7 +181,7 @@ namespace Discord.Commands | |||||
} | } | ||||
} | } | ||||
private async Task<IResult> ExecuteAsyncInternalAsync(ICommandContext context, object[] args, IServiceProvider services) | |||||
private async Task<IResult> ExecuteInternalAsync(ICommandContext context, object[] args, IServiceProvider services) | |||||
{ | { | ||||
await Module.Service._cmdLogger.DebugAsync($"Executing {GetLogText(context)}").ConfigureAwait(false); | await Module.Service._cmdLogger.DebugAsync($"Executing {GetLogText(context)}").ConfigureAwait(false); | ||||
try | try | ||||
@@ -2,7 +2,7 @@ using System; | |||||
using System.Linq; | using System.Linq; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
using System.Reflection; | |||||
using Discord.Commands.Builders; | using Discord.Commands.Builders; | ||||
namespace Discord.Commands | namespace Discord.Commands | ||||
@@ -13,6 +13,7 @@ namespace Discord.Commands | |||||
public string Name { get; } | public string Name { get; } | ||||
public string Summary { get; } | public string Summary { get; } | ||||
public string Remarks { get; } | public string Remarks { get; } | ||||
public string Group { get; } | |||||
public IReadOnlyList<string> Aliases { get; } | public IReadOnlyList<string> Aliases { get; } | ||||
public IReadOnlyList<CommandInfo> Commands { get; } | public IReadOnlyList<CommandInfo> Commands { get; } | ||||
@@ -22,21 +23,26 @@ namespace Discord.Commands | |||||
public ModuleInfo Parent { get; } | public ModuleInfo Parent { get; } | ||||
public bool IsSubmodule => Parent != null; | public bool IsSubmodule => Parent != null; | ||||
internal ModuleInfo(ModuleBuilder builder, CommandService service, ModuleInfo parent = null) | |||||
//public TypeInfo TypeInfo { get; } | |||||
internal ModuleInfo(ModuleBuilder builder, CommandService service, IServiceProvider services, ModuleInfo parent = null) | |||||
{ | { | ||||
Service = service; | Service = service; | ||||
Name = builder.Name; | Name = builder.Name; | ||||
Summary = builder.Summary; | Summary = builder.Summary; | ||||
Remarks = builder.Remarks; | Remarks = builder.Remarks; | ||||
Group = builder.Group; | |||||
Parent = parent; | Parent = parent; | ||||
//TypeInfo = builder.TypeInfo; | |||||
Aliases = BuildAliases(builder, service).ToImmutableArray(); | Aliases = BuildAliases(builder, service).ToImmutableArray(); | ||||
Commands = builder.Commands.Select(x => x.Build(this, service)).ToImmutableArray(); | Commands = builder.Commands.Select(x => x.Build(this, service)).ToImmutableArray(); | ||||
Preconditions = BuildPreconditions(builder).ToImmutableArray(); | Preconditions = BuildPreconditions(builder).ToImmutableArray(); | ||||
Attributes = BuildAttributes(builder).ToImmutableArray(); | Attributes = BuildAttributes(builder).ToImmutableArray(); | ||||
Submodules = BuildSubmodules(builder, service).ToImmutableArray(); | |||||
Submodules = BuildSubmodules(builder, service, services).ToImmutableArray(); | |||||
} | } | ||||
private static IEnumerable<string> BuildAliases(ModuleBuilder builder, CommandService service) | private static IEnumerable<string> BuildAliases(ModuleBuilder builder, CommandService service) | ||||
@@ -66,12 +72,12 @@ namespace Discord.Commands | |||||
return result; | return result; | ||||
} | } | ||||
private List<ModuleInfo> BuildSubmodules(ModuleBuilder parent, CommandService service) | |||||
private List<ModuleInfo> BuildSubmodules(ModuleBuilder parent, CommandService service, IServiceProvider services) | |||||
{ | { | ||||
var result = new List<ModuleInfo>(); | var result = new List<ModuleInfo>(); | ||||
foreach (var submodule in parent.Modules) | foreach (var submodule in parent.Modules) | ||||
result.Add(submodule.Build(service, this)); | |||||
result.Add(submodule.Build(service, services, this)); | |||||
return result; | return result; | ||||
} | } | ||||
@@ -1,5 +1,6 @@ | |||||
using System; | |||||
using System; | |||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
using Discord.Commands.Builders; | |||||
namespace Discord.Commands | namespace Discord.Commands | ||||
{ | { | ||||
@@ -10,7 +11,13 @@ namespace Discord.Commands | |||||
{ | { | ||||
public T Context { get; private set; } | public T Context { get; private set; } | ||||
protected virtual async Task<IUserMessage> ReplyAsync(string message, bool isTTS = false, Embed embed = null, RequestOptions options = null) | |||||
/// <summary> | |||||
/// Sends a message to the source channel | |||||
/// </summary> | |||||
/// <param name="message">Contents of the message; optional only if <paramref name="embed"/> is specified</param> | |||||
/// <param name="isTTS">Specifies if Discord should read this message aloud using TTS</param> | |||||
/// <param name="embed">An embed to be displayed alongside the message</param> | |||||
protected virtual async Task<IUserMessage> ReplyAsync(string message = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) | |||||
{ | { | ||||
return await Context.Channel.SendMessageAsync(message, isTTS, embed, options).ConfigureAwait(false); | return await Context.Channel.SendMessageAsync(message, isTTS, embed, options).ConfigureAwait(false); | ||||
} | } | ||||
@@ -23,15 +30,18 @@ namespace Discord.Commands | |||||
{ | { | ||||
} | } | ||||
protected virtual void OnModuleBuilding(CommandService commandService, ModuleBuilder builder) | |||||
{ | |||||
} | |||||
//IModuleBase | //IModuleBase | ||||
void IModuleBase.SetContext(ICommandContext context) | void IModuleBase.SetContext(ICommandContext context) | ||||
{ | { | ||||
var newValue = context as T; | var newValue = context as T; | ||||
Context = newValue ?? throw new InvalidOperationException($"Invalid context type. Expected {typeof(T).Name}, got {context.GetType().Name}"); | 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.BeforeExecute(CommandInfo command) => BeforeExecute(command); | ||||
void IModuleBase.AfterExecute(CommandInfo command) => AfterExecute(command); | void IModuleBase.AfterExecute(CommandInfo command) => AfterExecute(command); | ||||
void IModuleBase.OnModuleBuilding(CommandService commandService, ModuleBuilder builder) => OnModuleBuilding(commandService, builder); | |||||
} | } | ||||
} | } |
@@ -1,4 +1,4 @@ | |||||
using System; | |||||
using System; | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Linq; | using System.Linq; | ||||
using System.Reflection; | using System.Reflection; | ||||
@@ -1,4 +1,4 @@ | |||||
using System; | |||||
using System; | |||||
namespace Discord | namespace Discord | ||||
{ | { | ||||
@@ -13,6 +13,10 @@ namespace Discord | |||||
string extension = FormatToExtension(format, avatarId); | string extension = FormatToExtension(format, avatarId); | ||||
return $"{DiscordConfig.CDNUrl}avatars/{userId}/{avatarId}.{extension}?size={size}"; | return $"{DiscordConfig.CDNUrl}avatars/{userId}/{avatarId}.{extension}?size={size}"; | ||||
} | } | ||||
public static string GetDefaultUserAvatarUrl(ushort discriminator) | |||||
{ | |||||
return $"{DiscordConfig.CDNUrl}embed/avatars/{discriminator % 5}.png"; | |||||
} | |||||
public static string GetGuildIconUrl(ulong guildId, string iconId) | public static string GetGuildIconUrl(ulong guildId, string iconId) | ||||
=> iconId != null ? $"{DiscordConfig.CDNUrl}icons/{guildId}/{iconId}.jpg" : null; | => iconId != null ? $"{DiscordConfig.CDNUrl}icons/{guildId}/{iconId}.jpg" : null; | ||||
public static string GetGuildSplashUrl(ulong guildId, string splashId) | public static string GetGuildSplashUrl(ulong guildId, string splashId) | ||||
@@ -28,17 +32,25 @@ namespace Discord | |||||
return $"{DiscordConfig.CDNUrl}app-assets/{appId}/{assetId}.{extension}?size={size}"; | return $"{DiscordConfig.CDNUrl}app-assets/{appId}/{assetId}.{extension}?size={size}"; | ||||
} | } | ||||
public static string GetSpotifyAlbumArtUrl(string albumArtId) | |||||
=> $"https://i.scdn.co/image/{albumArtId}"; | |||||
private static string FormatToExtension(ImageFormat format, string imageId) | private static string FormatToExtension(ImageFormat format, string imageId) | ||||
{ | { | ||||
if (format == ImageFormat.Auto) | if (format == ImageFormat.Auto) | ||||
format = imageId.StartsWith("a_") ? ImageFormat.Gif : ImageFormat.Png; | format = imageId.StartsWith("a_") ? ImageFormat.Gif : ImageFormat.Png; | ||||
switch (format) | 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)); | |||||
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)); | |||||
} | } | ||||
} | } | ||||
} | } | ||||
@@ -0,0 +1,10 @@ | |||||
namespace Discord | |||||
{ | |||||
public enum ActivityType | |||||
{ | |||||
Playing = 0, | |||||
Streaming = 1, | |||||
Listening = 2, | |||||
Watching = 3 | |||||
} | |||||
} |
@@ -1,4 +1,4 @@ | |||||
using System.Diagnostics; | |||||
using System.Diagnostics; | |||||
namespace Discord | namespace Discord | ||||
{ | { | ||||
@@ -6,11 +6,13 @@ namespace Discord | |||||
public class Game : IActivity | public class Game : IActivity | ||||
{ | { | ||||
public string Name { get; internal set; } | public string Name { get; internal set; } | ||||
public ActivityType Type { get; internal set; } | |||||
internal Game() { } | internal Game() { } | ||||
public Game(string name) | |||||
public Game(string name, ActivityType type = ActivityType.Playing) | |||||
{ | { | ||||
Name = name; | Name = name; | ||||
Type = type; | |||||
} | } | ||||
public override string ToString() => Name; | public override string ToString() => Name; | ||||
@@ -1,15 +1,15 @@ | |||||
namespace Discord | |||||
namespace Discord | |||||
{ | { | ||||
public class GameAsset | public class GameAsset | ||||
{ | { | ||||
internal GameAsset() { } | internal GameAsset() { } | ||||
internal ulong ApplicationId { get; set; } | |||||
internal ulong? ApplicationId { get; set; } | |||||
public string Text { get; internal set; } | public string Text { get; internal set; } | ||||
public string ImageId { get; internal set; } | public string ImageId { get; internal set; } | ||||
public string GetImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) | public string GetImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) | ||||
=> CDN.GetRichAssetUrl(ApplicationId, ImageId, size, format); | |||||
=> ApplicationId.HasValue ? CDN.GetRichAssetUrl(ApplicationId.Value, ImageId, size, format) : null; | |||||
} | } | ||||
} | |||||
} |
@@ -1,11 +1,11 @@ | |||||
namespace Discord | |||||
namespace Discord | |||||
{ | { | ||||
public class GameParty | public class GameParty | ||||
{ | { | ||||
internal GameParty() { } | internal GameParty() { } | ||||
public string Id { get; internal set; } | public string Id { get; internal set; } | ||||
public int Members { get; internal set; } | |||||
public int Capacity { get; internal set; } | |||||
public long Members { get; internal set; } | |||||
public long Capacity { get; internal set; } | |||||
} | } | ||||
} | |||||
} |
@@ -1,13 +1,8 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace Discord | |||||
namespace Discord | |||||
{ | { | ||||
public interface IActivity | public interface IActivity | ||||
{ | { | ||||
string Name { get; } | string Name { get; } | ||||
ActivityType Type { get; } | |||||
} | } | ||||
} | } |
@@ -1,4 +1,4 @@ | |||||
using System.Diagnostics; | |||||
using System.Diagnostics; | |||||
namespace Discord | namespace Discord | ||||
{ | { | ||||
@@ -7,8 +7,8 @@ namespace Discord | |||||
{ | { | ||||
internal RichGame() { } | internal RichGame() { } | ||||
public string Details { get; internal set;} | |||||
public string State { get; internal set;} | |||||
public string Details { get; internal set; } | |||||
public string State { get; internal set; } | |||||
public ulong ApplicationId { get; internal set; } | public ulong ApplicationId { get; internal set; } | ||||
public GameAsset SmallAsset { get; internal set; } | public GameAsset SmallAsset { get; internal set; } | ||||
public GameAsset LargeAsset { get; internal set; } | public GameAsset LargeAsset { get; internal set; } | ||||
@@ -19,4 +19,4 @@ namespace Discord | |||||
public override string ToString() => Name; | public override string ToString() => Name; | ||||
private string DebuggerDisplay => $"{Name} (Rich)"; | private string DebuggerDisplay => $"{Name} (Rich)"; | ||||
} | } | ||||
} | |||||
} |
@@ -0,0 +1,23 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Diagnostics; | |||||
namespace Discord | |||||
{ | |||||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||||
public class SpotifyGame : Game | |||||
{ | |||||
public IEnumerable<string> Artists { get; internal set; } | |||||
public string AlbumArt { get; internal set; } | |||||
public string AlbumTitle { get; internal set; } | |||||
public string TrackTitle { get; internal set; } | |||||
public string SyncId { get; internal set; } | |||||
public string SessionId { get; internal set; } | |||||
public TimeSpan? Duration { get; internal set; } | |||||
internal SpotifyGame() { } | |||||
public override string ToString() => Name; | |||||
private string DebuggerDisplay => $"{Name} (Spotify)"; | |||||
} | |||||
} |
@@ -6,15 +6,14 @@ namespace Discord | |||||
public class StreamingGame : Game | public class StreamingGame : Game | ||||
{ | { | ||||
public string Url { get; internal set; } | public string Url { get; internal set; } | ||||
public StreamType StreamType { get; internal set; } | |||||
public StreamingGame(string name, string url, StreamType streamType) | |||||
public StreamingGame(string name, string url) | |||||
{ | { | ||||
Name = name; | Name = name; | ||||
Url = url; | Url = url; | ||||
StreamType = streamType; | |||||
Type = ActivityType.Streaming; | |||||
} | } | ||||
public override string ToString() => Name; | public override string ToString() => Name; | ||||
private string DebuggerDisplay => $"{Name} ({Url})"; | private string DebuggerDisplay => $"{Name} ({Url})"; | ||||
} | } |
@@ -1,4 +1,4 @@ | |||||
using System; | |||||
using System; | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.IO; | using System.IO; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
@@ -11,10 +11,10 @@ namespace Discord | |||||
Task<IUserMessage> SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null); | Task<IUserMessage> SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null); | ||||
#if FILESYSTEM | #if FILESYSTEM | ||||
/// <summary> Sends a file to this text channel, with an optional caption. </summary> | /// <summary> Sends a file to this text channel, with an optional caption. </summary> | ||||
Task<IUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, RequestOptions options = null); | |||||
Task<IUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); | |||||
#endif | #endif | ||||
/// <summary> Sends a file to this text channel, with an optional caption. </summary> | /// <summary> Sends a file to this text channel, with an optional caption. </summary> | ||||
Task<IUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, RequestOptions options = null); | |||||
Task<IUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); | |||||
/// <summary> Gets a message from this message channel with the given id, or null if not found. </summary> | /// <summary> Gets a message from this message channel with the given id, or null if not found. </summary> | ||||
Task<IMessage> GetMessageAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); | Task<IMessage> GetMessageAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); | ||||
@@ -1,89 +1,105 @@ | |||||
using System; | |||||
using System; | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
using System.Linq; | |||||
namespace Discord | namespace Discord | ||||
{ | { | ||||
public class EmbedBuilder | public class EmbedBuilder | ||||
{ | { | ||||
private readonly Embed _embed; | |||||
private string _title; | |||||
private string _description; | |||||
private string _url; | |||||
private EmbedImage? _image; | |||||
private EmbedThumbnail? _thumbnail; | |||||
private List<EmbedFieldBuilder> _fields; | |||||
public const int MaxFieldCount = 25; | public const int MaxFieldCount = 25; | ||||
public const int MaxTitleLength = 256; | public const int MaxTitleLength = 256; | ||||
public const int MaxDescriptionLength = 2048; | public const int MaxDescriptionLength = 2048; | ||||
public const int MaxEmbedLength = 6000; // user bot limit is 2000, but we don't validate that here. | |||||
public const int MaxEmbedLength = 6000; | |||||
public EmbedBuilder() | public EmbedBuilder() | ||||
{ | { | ||||
_embed = new Embed(EmbedType.Rich); | |||||
Fields = new List<EmbedFieldBuilder>(); | Fields = new List<EmbedFieldBuilder>(); | ||||
} | } | ||||
public string Title | public string Title | ||||
{ | { | ||||
get => _embed.Title; | |||||
get => _title; | |||||
set | set | ||||
{ | { | ||||
if (value?.Length > MaxTitleLength) throw new ArgumentException($"Title length must be less than or equal to {MaxTitleLength}.", nameof(Title)); | if (value?.Length > MaxTitleLength) throw new ArgumentException($"Title length must be less than or equal to {MaxTitleLength}.", nameof(Title)); | ||||
_embed.Title = value; | |||||
_title = value; | |||||
} | } | ||||
} | } | ||||
public string Description | public string Description | ||||
{ | { | ||||
get => _embed.Description; | |||||
get => _description; | |||||
set | set | ||||
{ | { | ||||
if (value?.Length > MaxDescriptionLength) throw new ArgumentException($"Description length must be less than or equal to {MaxDescriptionLength}.", nameof(Description)); | if (value?.Length > MaxDescriptionLength) throw new ArgumentException($"Description length must be less than or equal to {MaxDescriptionLength}.", nameof(Description)); | ||||
_embed.Description = value; | |||||
_description = value; | |||||
} | } | ||||
} | } | ||||
public string Url | public string Url | ||||
{ | { | ||||
get => _embed.Url; | |||||
get => _url; | |||||
set | set | ||||
{ | { | ||||
if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(Url)); | if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(Url)); | ||||
_embed.Url = value; | |||||
_url = value; | |||||
} | } | ||||
} | } | ||||
public string ThumbnailUrl | public string ThumbnailUrl | ||||
{ | { | ||||
get => _embed.Thumbnail?.Url; | |||||
get => _thumbnail?.Url; | |||||
set | set | ||||
{ | { | ||||
if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(ThumbnailUrl)); | if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(ThumbnailUrl)); | ||||
_embed.Thumbnail = new EmbedThumbnail(value, null, null, null); | |||||
_thumbnail = new EmbedThumbnail(value, null, null, null); | |||||
} | } | ||||
} | } | ||||
public string ImageUrl | public string ImageUrl | ||||
{ | { | ||||
get => _embed.Image?.Url; | |||||
get => _image?.Url; | |||||
set | set | ||||
{ | { | ||||
if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(ImageUrl)); | if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(ImageUrl)); | ||||
_embed.Image = new EmbedImage(value, null, null, null); | |||||
_image = new EmbedImage(value, null, null, null); | |||||
} | } | ||||
} | } | ||||
public DateTimeOffset? Timestamp { get => _embed.Timestamp; set { _embed.Timestamp = value; } } | |||||
public Color? Color { get => _embed.Color; set { _embed.Color = value; } } | |||||
public EmbedAuthorBuilder Author { get; set; } | |||||
public EmbedFooterBuilder Footer { get; set; } | |||||
private List<EmbedFieldBuilder> _fields; | |||||
public List<EmbedFieldBuilder> Fields | public List<EmbedFieldBuilder> Fields | ||||
{ | { | ||||
get => _fields; | get => _fields; | ||||
set | set | ||||
{ | { | ||||
if (value == null) throw new ArgumentNullException("Cannot set an embed builder's fields collection to null", nameof(Fields)); | if (value == null) throw new ArgumentNullException("Cannot set an embed builder's fields collection to null", nameof(Fields)); | ||||
if (value.Count > MaxFieldCount) throw new ArgumentException($"Field count must be less than or equal to {MaxFieldCount}.", nameof(Fields)); | if (value.Count > MaxFieldCount) throw new ArgumentException($"Field count must be less than or equal to {MaxFieldCount}.", nameof(Fields)); | ||||
_fields = value; | _fields = value; | ||||
} | } | ||||
} | } | ||||
public DateTimeOffset? Timestamp { get; set; } | |||||
public Color? Color { get; set; } | |||||
public EmbedAuthorBuilder Author { get; set; } | |||||
public EmbedFooterBuilder Footer { get; set; } | |||||
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; | |||||
} | |||||
} | |||||
public EmbedBuilder WithTitle(string title) | public EmbedBuilder WithTitle(string title) | ||||
{ | { | ||||
Title = title; | Title = title; | ||||
@@ -180,7 +196,6 @@ namespace Discord | |||||
AddField(field); | AddField(field); | ||||
return this; | return this; | ||||
} | } | ||||
public EmbedBuilder AddField(EmbedFieldBuilder field) | public EmbedBuilder AddField(EmbedFieldBuilder field) | ||||
{ | { | ||||
if (Fields.Count >= MaxFieldCount) | if (Fields.Count >= MaxFieldCount) | ||||
@@ -195,63 +210,54 @@ namespace Discord | |||||
{ | { | ||||
var field = new EmbedFieldBuilder(); | var field = new EmbedFieldBuilder(); | ||||
action(field); | action(field); | ||||
this.AddField(field); | |||||
AddField(field); | |||||
return this; | return this; | ||||
} | } | ||||
public Embed Build() | public Embed Build() | ||||
{ | { | ||||
_embed.Footer = Footer?.Build(); | |||||
_embed.Author = Author?.Build(); | |||||
if (Length > MaxEmbedLength) | |||||
throw new InvalidOperationException($"Total embed length must be less than or equal to {MaxEmbedLength}"); | |||||
var fields = ImmutableArray.CreateBuilder<EmbedField>(Fields.Count); | var fields = ImmutableArray.CreateBuilder<EmbedField>(Fields.Count); | ||||
for (int i = 0; i < Fields.Count; i++) | for (int i = 0; i < Fields.Count; i++) | ||||
fields.Add(Fields[i].Build()); | fields.Add(Fields[i].Build()); | ||||
_embed.Fields = fields.ToImmutable(); | |||||
if (_embed.Length > MaxEmbedLength) | |||||
{ | |||||
throw new InvalidOperationException($"Total embed length must be less than or equal to {MaxEmbedLength}"); | |||||
} | |||||
return _embed; | |||||
return new Embed(EmbedType.Rich, Title, Description, Url, Timestamp, Color, _image, null, Author?.Build(), Footer?.Build(), null, _thumbnail, fields.ToImmutable()); | |||||
} | } | ||||
} | } | ||||
public class EmbedFieldBuilder | public class EmbedFieldBuilder | ||||
{ | { | ||||
private string _name; | |||||
private string _value; | |||||
private EmbedField _field; | private EmbedField _field; | ||||
public const int MaxFieldNameLength = 256; | public const int MaxFieldNameLength = 256; | ||||
public const int MaxFieldValueLength = 1024; | public const int MaxFieldValueLength = 1024; | ||||
public string Name | public string Name | ||||
{ | { | ||||
get => _field.Name; | |||||
get => _name; | |||||
set | set | ||||
{ | { | ||||
if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException($"Field name must not be null, empty or entirely whitespace.", nameof(Name)); | if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException($"Field name must not be null, empty or entirely whitespace.", nameof(Name)); | ||||
if (value.Length > MaxFieldNameLength) throw new ArgumentException($"Field name length must be less than or equal to {MaxFieldNameLength}.", nameof(Name)); | if (value.Length > MaxFieldNameLength) throw new ArgumentException($"Field name length must be less than or equal to {MaxFieldNameLength}.", nameof(Name)); | ||||
_field.Name = value; | |||||
_name = value; | |||||
} | } | ||||
} | } | ||||
public object Value | public object Value | ||||
{ | { | ||||
get => _field.Value; | |||||
get => _value; | |||||
set | set | ||||
{ | { | ||||
var stringValue = value?.ToString(); | var stringValue = value?.ToString(); | ||||
if (string.IsNullOrEmpty(stringValue)) throw new ArgumentException($"Field value must not be null or empty.", nameof(Value)); | if (string.IsNullOrEmpty(stringValue)) throw new ArgumentException($"Field value must not be null or empty.", nameof(Value)); | ||||
if (stringValue.Length > MaxFieldValueLength) throw new ArgumentException($"Field value length must be less than or equal to {MaxFieldValueLength}.", nameof(Value)); | if (stringValue.Length > MaxFieldValueLength) throw new ArgumentException($"Field value length must be less than or equal to {MaxFieldValueLength}.", nameof(Value)); | ||||
_field.Value = stringValue; | |||||
_value = stringValue; | |||||
} | } | ||||
} | } | ||||
public bool IsInline { get => _field.Inline; set { _field.Inline = value; } } | |||||
public EmbedFieldBuilder() | |||||
{ | |||||
_field = new EmbedField(); | |||||
} | |||||
public bool IsInline { get; set; } | |||||
public EmbedFieldBuilder WithName(string name) | public EmbedFieldBuilder WithName(string name) | ||||
{ | { | ||||
@@ -270,48 +276,44 @@ namespace Discord | |||||
} | } | ||||
public EmbedField Build() | public EmbedField Build() | ||||
=> _field; | |||||
=> new EmbedField(Name, Value.ToString(), IsInline); | |||||
} | } | ||||
public class EmbedAuthorBuilder | public class EmbedAuthorBuilder | ||||
{ | { | ||||
private EmbedAuthor _author; | |||||
private string _name; | |||||
private string _url; | |||||
private string _iconUrl; | |||||
public const int MaxAuthorNameLength = 256; | public const int MaxAuthorNameLength = 256; | ||||
public string Name | public string Name | ||||
{ | { | ||||
get => _author.Name; | |||||
get => _name; | |||||
set | set | ||||
{ | { | ||||
if (value?.Length > MaxAuthorNameLength) throw new ArgumentException($"Author name length must be less than or equal to {MaxAuthorNameLength}.", nameof(Name)); | if (value?.Length > MaxAuthorNameLength) throw new ArgumentException($"Author name length must be less than or equal to {MaxAuthorNameLength}.", nameof(Name)); | ||||
_author.Name = value; | |||||
_name = value; | |||||
} | } | ||||
} | } | ||||
public string Url | public string Url | ||||
{ | { | ||||
get => _author.Url; | |||||
get => _url; | |||||
set | set | ||||
{ | { | ||||
if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(Url)); | if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(Url)); | ||||
_author.Url = value; | |||||
_url = value; | |||||
} | } | ||||
} | } | ||||
public string IconUrl | public string IconUrl | ||||
{ | { | ||||
get => _author.IconUrl; | |||||
get => _iconUrl; | |||||
set | set | ||||
{ | { | ||||
if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(IconUrl)); | if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(IconUrl)); | ||||
_author.IconUrl = value; | |||||
_iconUrl = value; | |||||
} | } | ||||
} | } | ||||
public EmbedAuthorBuilder() | |||||
{ | |||||
_author = new EmbedAuthor(); | |||||
} | |||||
public EmbedAuthorBuilder WithName(string name) | public EmbedAuthorBuilder WithName(string name) | ||||
{ | { | ||||
Name = name; | Name = name; | ||||
@@ -329,39 +331,35 @@ namespace Discord | |||||
} | } | ||||
public EmbedAuthor Build() | public EmbedAuthor Build() | ||||
=> _author; | |||||
=> new EmbedAuthor(Name, Url, IconUrl, null); | |||||
} | } | ||||
public class EmbedFooterBuilder | public class EmbedFooterBuilder | ||||
{ | { | ||||
private EmbedFooter _footer; | |||||
private string _text; | |||||
private string _iconUrl; | |||||
public const int MaxFooterTextLength = 2048; | public const int MaxFooterTextLength = 2048; | ||||
public string Text | public string Text | ||||
{ | { | ||||
get => _footer.Text; | |||||
get => _text; | |||||
set | set | ||||
{ | { | ||||
if (value?.Length > MaxFooterTextLength) throw new ArgumentException($"Footer text length must be less than or equal to {MaxFooterTextLength}.", nameof(Text)); | if (value?.Length > MaxFooterTextLength) throw new ArgumentException($"Footer text length must be less than or equal to {MaxFooterTextLength}.", nameof(Text)); | ||||
_footer.Text = value; | |||||
_text = value; | |||||
} | } | ||||
} | } | ||||
public string IconUrl | public string IconUrl | ||||
{ | { | ||||
get => _footer.IconUrl; | |||||
get => _iconUrl; | |||||
set | set | ||||
{ | { | ||||
if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(IconUrl)); | if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(IconUrl)); | ||||
_footer.IconUrl = value; | |||||
_iconUrl = value; | |||||
} | } | ||||
} | } | ||||
public EmbedFooterBuilder() | |||||
{ | |||||
_footer = new EmbedFooter(); | |||||
} | |||||
public EmbedFooterBuilder WithText(string text) | public EmbedFooterBuilder WithText(string text) | ||||
{ | { | ||||
Text = text; | Text = text; | ||||
@@ -374,6 +372,6 @@ namespace Discord | |||||
} | } | ||||
public EmbedFooter Build() | public EmbedFooter Build() | ||||
=> _footer; | |||||
=> new EmbedFooter(Text, IconUrl, null); | |||||
} | } | ||||
} | } |
@@ -1,13 +1,15 @@ | |||||
namespace Discord | |||||
namespace Discord | |||||
{ | { | ||||
public enum EmbedType | public enum EmbedType | ||||
{ | { | ||||
Unknown = -1, | |||||
Rich, | Rich, | ||||
Link, | Link, | ||||
Video, | Video, | ||||
Image, | Image, | ||||
Gifv, | Gifv, | ||||
Article, | Article, | ||||
Tweet | |||||
Tweet, | |||||
Html, | |||||
} | } | ||||
} | } |
@@ -1,4 +1,4 @@ | |||||
using System; | |||||
using System; | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Diagnostics; | using System.Diagnostics; | ||||
@@ -13,6 +13,8 @@ namespace Discord | |||||
public static readonly ChannelPermissions Text = new ChannelPermissions(0b01100_0000000_1111111110001_010001); | public static readonly ChannelPermissions Text = new ChannelPermissions(0b01100_0000000_1111111110001_010001); | ||||
/// <summary> Gets a ChannelPermissions that grants all permissions for voice channels. </summary> | /// <summary> Gets a ChannelPermissions that grants all permissions for voice channels. </summary> | ||||
public static readonly ChannelPermissions Voice = new ChannelPermissions(0b00100_1111110_0000000000000_010001); | public static readonly ChannelPermissions Voice = new ChannelPermissions(0b00100_1111110_0000000000000_010001); | ||||
/// <summary> Gets a ChannelPermissions that grants all permissions for category channels. </summary> | |||||
public static readonly ChannelPermissions Category = new ChannelPermissions(0b01100_1111110_1111111110001_010001); | |||||
/// <summary> Gets a ChannelPermissions that grants all permissions for direct message channels. </summary> | /// <summary> Gets a ChannelPermissions that grants all permissions for direct message channels. </summary> | ||||
public static readonly ChannelPermissions DM = new ChannelPermissions(0b00000_1000110_1011100110000_000000); | public static readonly ChannelPermissions DM = new ChannelPermissions(0b00000_1000110_1011100110000_000000); | ||||
/// <summary> Gets a ChannelPermissions that grants all permissions for group channels. </summary> | /// <summary> Gets a ChannelPermissions that grants all permissions for group channels. </summary> | ||||
@@ -24,6 +26,7 @@ namespace Discord | |||||
{ | { | ||||
case ITextChannel _: return Text; | case ITextChannel _: return Text; | ||||
case IVoiceChannel _: return Voice; | case IVoiceChannel _: return Voice; | ||||
case ICategoryChannel _: return Category; | |||||
case IDMChannel _: return DM; | case IDMChannel _: return DM; | ||||
case IGroupChannel _: return Group; | case IGroupChannel _: return Group; | ||||
default: throw new ArgumentException("Unknown channel type", nameof(channel)); | default: throw new ArgumentException("Unknown channel type", nameof(channel)); | ||||
@@ -157,4 +160,4 @@ namespace Discord | |||||
public override string ToString() => RawValue.ToString(); | public override string ToString() => RawValue.ToString(); | ||||
private string DebuggerDisplay => $"{string.Join(", ", ToList())}"; | private string DebuggerDisplay => $"{string.Join(", ", ToList())}"; | ||||
} | } | ||||
} | |||||
} |
@@ -8,6 +8,8 @@ namespace Discord | |||||
string AvatarId { get; } | string AvatarId { get; } | ||||
/// <summary> Gets the url to this user's avatar. </summary> | /// <summary> Gets the url to this user's avatar. </summary> | ||||
string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128); | string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128); | ||||
/// <summary> Gets the url to this user's default avatar. </summary> | |||||
string GetDefaultAvatarUrl(); | |||||
/// <summary> Gets the per-username unique id for this user. </summary> | /// <summary> Gets the per-username unique id for this user. </summary> | ||||
string Discriminator { get; } | string Discriminator { get; } | ||||
/// <summary> Gets the per-username unique id for this user. </summary> | /// <summary> Gets the per-username unique id for this user. </summary> | ||||
@@ -1,8 +0,0 @@ | |||||
namespace Discord | |||||
{ | |||||
public enum StreamType | |||||
{ | |||||
NotStreaming = 0, | |||||
Twitch = 1 | |||||
} | |||||
} |
@@ -1,6 +1,5 @@ | |||||
using System.Collections.Generic; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | using System.Linq; | ||||
using System.Threading; | |||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
namespace Discord | namespace Discord | ||||
@@ -20,45 +19,7 @@ namespace Discord | |||||
public static IAsyncEnumerable<T> Flatten<T>(this IAsyncEnumerable<IEnumerable<T>> source) | public static IAsyncEnumerable<T> Flatten<T>(this IAsyncEnumerable<IEnumerable<T>> source) | ||||
{ | { | ||||
return new PagedCollectionEnumerator<T>(source); | |||||
} | |||||
internal class PagedCollectionEnumerator<T> : IAsyncEnumerator<T>, IAsyncEnumerable<T> | |||||
{ | |||||
readonly IAsyncEnumerator<IEnumerable<T>> _source; | |||||
IEnumerator<T> _enumerator; | |||||
public IAsyncEnumerator<T> GetEnumerator() => this; | |||||
internal PagedCollectionEnumerator(IAsyncEnumerable<IEnumerable<T>> source) | |||||
{ | |||||
_source = source.GetEnumerator(); | |||||
} | |||||
public T Current => _enumerator.Current; | |||||
public void Dispose() | |||||
{ | |||||
_enumerator?.Dispose(); | |||||
_source.Dispose(); | |||||
} | |||||
public async Task<bool> MoveNext(CancellationToken cancellationToken) | |||||
{ | |||||
cancellationToken.ThrowIfCancellationRequested(); | |||||
if(!_enumerator?.MoveNext() ?? true) | |||||
{ | |||||
if (!await _source.MoveNext(cancellationToken).ConfigureAwait(false)) | |||||
return false; | |||||
_enumerator?.Dispose(); | |||||
_enumerator = _source.Current.GetEnumerator(); | |||||
return _enumerator.MoveNext(); | |||||
} | |||||
return true; | |||||
} | |||||
return source.SelectMany(enumerable => enumerable.ToAsyncEnumerable()); | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -1,4 +1,4 @@ | |||||
using System.Threading.Tasks; | |||||
using System.Threading.Tasks; | |||||
using System.IO; | using System.IO; | ||||
namespace Discord | namespace Discord | ||||
@@ -8,10 +8,10 @@ namespace Discord | |||||
/// <summary> | /// <summary> | ||||
/// Sends a message to the user via DM. | /// Sends a message to the user via DM. | ||||
/// </summary> | /// </summary> | ||||
public static async Task<IUserMessage> SendMessageAsync(this IUser user, | |||||
string text, | |||||
public static async Task<IUserMessage> SendMessageAsync(this IUser user, | |||||
string text, | |||||
bool isTTS = false, | bool isTTS = false, | ||||
Embed embed = null, | |||||
Embed embed = null, | |||||
RequestOptions options = null) | RequestOptions options = null) | ||||
{ | { | ||||
return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); | return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); | ||||
@@ -25,24 +25,29 @@ namespace Discord | |||||
string filename, | string filename, | ||||
string text = null, | string text = null, | ||||
bool isTTS = false, | bool isTTS = false, | ||||
Embed embed = null, | |||||
RequestOptions options = null | RequestOptions options = null | ||||
) | ) | ||||
{ | { | ||||
return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); | |||||
return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false); | |||||
} | } | ||||
#if FILESYSTEM | #if FILESYSTEM | ||||
/// <summary> | /// <summary> | ||||
/// Sends a file to the user via DM. | /// Sends a file to the user via DM. | ||||
/// </summary> | /// </summary> | ||||
public static async Task<IUserMessage> SendFileAsync(this IUser user, | |||||
string filePath, | |||||
string text = null, | |||||
bool isTTS = false, | |||||
public static async Task<IUserMessage> SendFileAsync(this IUser user, | |||||
string filePath, | |||||
string text = null, | |||||
bool isTTS = false, | |||||
Embed embed = null, | |||||
RequestOptions options = null) | RequestOptions options = null) | ||||
{ | { | ||||
return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); | |||||
return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false); | |||||
} | } | ||||
#endif | #endif | ||||
public static Task BanAsync(this IGuildUser user, int pruneDays = 0, string reason = null, RequestOptions options = null) | |||||
=> user.Guild.AddBanAsync(user, pruneDays, reason, options); | |||||
} | } | ||||
} | } |
@@ -36,5 +36,7 @@ namespace Discord | |||||
Task<IVoiceRegion> GetVoiceRegionAsync(string id, RequestOptions options = null); | Task<IVoiceRegion> GetVoiceRegionAsync(string id, RequestOptions options = null); | ||||
Task<IWebhook> GetWebhookAsync(ulong id, RequestOptions options = null); | Task<IWebhook> GetWebhookAsync(ulong id, RequestOptions options = null); | ||||
Task<int> GetRecommendedShardCountAsync(RequestOptions options = null); | |||||
} | } | ||||
} | } |
@@ -1,4 +1,4 @@ | |||||
using System; | |||||
using System; | |||||
using System.Net; | using System.Net; | ||||
namespace Discord.Net | namespace Discord.Net | ||||
@@ -8,11 +8,13 @@ namespace Discord.Net | |||||
public HttpStatusCode HttpCode { get; } | public HttpStatusCode HttpCode { get; } | ||||
public int? DiscordCode { get; } | public int? DiscordCode { get; } | ||||
public string Reason { get; } | public string Reason { get; } | ||||
public IRequest Request { get; } | |||||
public HttpException(HttpStatusCode httpCode, int? discordCode = null, string reason = null) | |||||
public HttpException(HttpStatusCode httpCode, IRequest request, int? discordCode = null, string reason = null) | |||||
: base(CreateMessage(httpCode, discordCode, reason)) | : base(CreateMessage(httpCode, discordCode, reason)) | ||||
{ | { | ||||
HttpCode = httpCode; | HttpCode = httpCode; | ||||
Request = request; | |||||
DiscordCode = discordCode; | DiscordCode = discordCode; | ||||
Reason = reason; | Reason = reason; | ||||
} | } | ||||
@@ -0,0 +1,10 @@ | |||||
using System; | |||||
namespace Discord.Net | |||||
{ | |||||
public interface IRequest | |||||
{ | |||||
DateTimeOffset? TimeoutAt { get; } | |||||
RequestOptions Options { get; } | |||||
} | |||||
} |
@@ -1,12 +1,15 @@ | |||||
using System; | |||||
using System; | |||||
namespace Discord.Net | namespace Discord.Net | ||||
{ | { | ||||
public class RateLimitedException : TimeoutException | public class RateLimitedException : TimeoutException | ||||
{ | { | ||||
public RateLimitedException() | |||||
public IRequest Request { get; } | |||||
public RateLimitedException(IRequest request) | |||||
: base("You are being rate limited.") | : base("You are being rate limited.") | ||||
{ | { | ||||
Request = request; | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -1,10 +1,10 @@ | |||||
using System; | |||||
using System; | |||||
namespace Discord | namespace Discord | ||||
{ | { | ||||
public enum TokenType | public enum TokenType | ||||
{ | { | ||||
[Obsolete("User logins are being deprecated and may result in a ToS strike against your account - please see https://github.com/RogueException/Discord.Net/issues/827")] | |||||
[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, | User, | ||||
Bearer, | Bearer, | ||||
Bot, | Bot, | ||||
@@ -0,0 +1,45 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
namespace Discord | |||||
{ | |||||
public static class DiscordComparers | |||||
{ | |||||
// TODO: simplify with '??=' slated for C# 8.0 | |||||
public static IEqualityComparer<IUser> UserComparer => _userComparer ?? (_userComparer = new EntityEqualityComparer<IUser, ulong>()); | |||||
public static IEqualityComparer<IGuild> GuildComparer => _guildComparer ?? (_guildComparer = new EntityEqualityComparer<IGuild, ulong>()); | |||||
public static IEqualityComparer<IChannel> ChannelComparer => _channelComparer ?? (_channelComparer = new EntityEqualityComparer<IChannel, ulong>()); | |||||
public static IEqualityComparer<IRole> RoleComparer => _roleComparer ?? (_roleComparer = new EntityEqualityComparer<IRole, ulong>()); | |||||
public static IEqualityComparer<IMessage> MessageComparer => _messageComparer ?? (_messageComparer = new EntityEqualityComparer<IMessage, ulong>()); | |||||
private static IEqualityComparer<IUser> _userComparer; | |||||
private static IEqualityComparer<IGuild> _guildComparer; | |||||
private static IEqualityComparer<IChannel> _channelComparer; | |||||
private static IEqualityComparer<IRole> _roleComparer; | |||||
private static IEqualityComparer<IMessage> _messageComparer; | |||||
private sealed class EntityEqualityComparer<TEntity, TId> : EqualityComparer<TEntity> | |||||
where TEntity : IEntity<TId> | |||||
where TId : IEquatable<TId> | |||||
{ | |||||
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; | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -80,7 +80,7 @@ namespace Discord | |||||
} | } | ||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] | [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||||
private static bool HasFlag(ulong value, ulong flag) => (value & flag) != 0; | |||||
private static bool HasFlag(ulong value, ulong flag) => (value & flag) == flag; | |||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] | [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||||
public static void SetFlag(ref ulong value, ulong flag) => value |= flag; | public static void SetFlag(ref ulong value, ulong flag) => value |= flag; | ||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] | [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||||
@@ -133,9 +133,10 @@ namespace Discord | |||||
ulong deniedPermissions = 0UL, allowedPermissions = 0UL; | ulong deniedPermissions = 0UL, allowedPermissions = 0UL; | ||||
foreach (var roleId in user.RoleIds) | foreach (var roleId in user.RoleIds) | ||||
{ | { | ||||
if (roleId != guild.EveryoneRole.Id) | |||||
IRole role = null; | |||||
if (roleId != guild.EveryoneRole.Id && (role = guild.GetRole(roleId)) != null) | |||||
{ | { | ||||
perms = channel.GetPermissionOverwrite(guild.GetRole(roleId)); | |||||
perms = channel.GetPermissionOverwrite(role); | |||||
if (perms != null) | if (perms != null) | ||||
{ | { | ||||
allowedPermissions |= perms.Value.AllowValue; | allowedPermissions |= perms.Value.AllowValue; | ||||
@@ -160,10 +161,10 @@ namespace Discord | |||||
else if (!GetValue(resolvedPermissions, ChannelPermission.SendMessages)) | else if (!GetValue(resolvedPermissions, ChannelPermission.SendMessages)) | ||||
{ | { | ||||
//No send permissions on a text channel removes all send-related permissions | //No send permissions on a text channel removes all send-related permissions | ||||
resolvedPermissions &= ~(1UL << (int)ChannelPermission.SendTTSMessages); | |||||
resolvedPermissions &= ~(1UL << (int)ChannelPermission.MentionEveryone); | |||||
resolvedPermissions &= ~(1UL << (int)ChannelPermission.EmbedLinks); | |||||
resolvedPermissions &= ~(1UL << (int)ChannelPermission.AttachFiles); | |||||
resolvedPermissions &= ~(ulong)ChannelPermission.SendTTSMessages; | |||||
resolvedPermissions &= ~(ulong)ChannelPermission.MentionEveryone; | |||||
resolvedPermissions &= ~(ulong)ChannelPermission.EmbedLinks; | |||||
resolvedPermissions &= ~(ulong)ChannelPermission.AttachFiles; | |||||
} | } | ||||
} | } | ||||
resolvedPermissions &= mask; //Ensure we didnt get any permissions this channel doesnt support (from guildPerms, for example) | resolvedPermissions &= mask; //Ensure we didnt get any permissions this channel doesnt support (from guildPerms, for example) | ||||
@@ -172,4 +173,4 @@ namespace Discord | |||||
return resolvedPermissions; | return resolvedPermissions; | ||||
} | } | ||||
} | } | ||||
} | |||||
} |
@@ -10,6 +10,6 @@ | |||||
<ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" /> | <ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" /> | ||||
</ItemGroup> | </ItemGroup> | ||||
<ItemGroup> | <ItemGroup> | ||||
<PackageReference Include="WebSocket4Net" Version="0.15.0" /> | |||||
<PackageReference Include="WebSocket4Net" Version="0.15.2" /> | |||||
</ItemGroup> | </ItemGroup> | ||||
</Project> | </Project> |
@@ -66,7 +66,7 @@ namespace Discord.Net.Providers.WS4Net | |||||
_cancelTokenSource = new CancellationTokenSource(); | _cancelTokenSource = new CancellationTokenSource(); | ||||
_cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; | _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; | ||||
_client = new WS4NetSocket(host, customHeaderItems: _headers.ToList()) | |||||
_client = new WS4NetSocket(host, "", customHeaderItems: _headers.ToList()) | |||||
{ | { | ||||
EnableAutoSendPing = false, | EnableAutoSendPing = false, | ||||
NoDelay = true, | NoDelay = true, | ||||
@@ -163,4 +163,4 @@ namespace Discord.Net.Providers.WS4Net | |||||
Closed(ex).GetAwaiter().GetResult(); | Closed(ex).GetAwaiter().GetResult(); | ||||
} | } | ||||
} | } | ||||
} | |||||
} |
@@ -1,4 +1,4 @@ | |||||
#pragma warning disable CS1591 | |||||
#pragma warning disable CS1591 | |||||
using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
using Newtonsoft.Json.Serialization; | using Newtonsoft.Json.Serialization; | ||||
using System.Runtime.Serialization; | using System.Runtime.Serialization; | ||||
@@ -12,7 +12,7 @@ namespace Discord.API | |||||
[JsonProperty("url")] | [JsonProperty("url")] | ||||
public Optional<string> StreamUrl { get; set; } | public Optional<string> StreamUrl { get; set; } | ||||
[JsonProperty("type")] | [JsonProperty("type")] | ||||
public Optional<StreamType?> StreamType { get; set; } | |||||
public Optional<ActivityType?> Type { get; set; } | |||||
[JsonProperty("details")] | [JsonProperty("details")] | ||||
public Optional<string> Details { get; set; } | public Optional<string> Details { get; set; } | ||||
[JsonProperty("state")] | [JsonProperty("state")] | ||||
@@ -29,6 +29,10 @@ namespace Discord.API | |||||
public Optional<API.GameTimestamps> Timestamps { get; set; } | public Optional<API.GameTimestamps> Timestamps { get; set; } | ||||
[JsonProperty("instance")] | [JsonProperty("instance")] | ||||
public Optional<bool> Instance { get; set; } | public Optional<bool> Instance { get; set; } | ||||
[JsonProperty("sync_id")] | |||||
public Optional<string> SyncId { get; set; } | |||||
[JsonProperty("session_id")] | |||||
public Optional<string> SessionId { get; set; } | |||||
[OnError] | [OnError] | ||||
internal void OnError(StreamingContext context, ErrorContext errorContext) | internal void OnError(StreamingContext context, ErrorContext errorContext) | ||||
@@ -1,4 +1,4 @@ | |||||
using Newtonsoft.Json; | |||||
using Newtonsoft.Json; | |||||
namespace Discord.API | namespace Discord.API | ||||
{ | { | ||||
@@ -8,9 +8,9 @@ namespace Discord.API | |||||
public Optional<string> SmallText { get; set; } | public Optional<string> SmallText { get; set; } | ||||
[JsonProperty("small_image")] | [JsonProperty("small_image")] | ||||
public Optional<string> SmallImage { get; set; } | public Optional<string> SmallImage { get; set; } | ||||
[JsonProperty("large_image")] | |||||
public Optional<string> LargeText { get; set; } | |||||
[JsonProperty("large_text")] | [JsonProperty("large_text")] | ||||
public Optional<string> LargeText { get; set; } | |||||
[JsonProperty("large_image")] | |||||
public Optional<string> LargeImage { get; set; } | public Optional<string> LargeImage { get; set; } | ||||
} | } | ||||
} | |||||
} |
@@ -1,4 +1,4 @@ | |||||
using Newtonsoft.Json; | |||||
using Newtonsoft.Json; | |||||
namespace Discord.API | namespace Discord.API | ||||
{ | { | ||||
@@ -7,6 +7,6 @@ namespace Discord.API | |||||
[JsonProperty("id")] | [JsonProperty("id")] | ||||
public string Id { get; set; } | public string Id { get; set; } | ||||
[JsonProperty("size")] | [JsonProperty("size")] | ||||
public int[] Size { get; set; } | |||||
public long[] Size { get; set; } | |||||
} | } | ||||
} | |||||
} |
@@ -1,18 +1,25 @@ | |||||
#pragma warning disable CS1591 | |||||
#pragma warning disable CS1591 | |||||
using Discord.Net.Converters; | |||||
using Discord.Net.Rest; | using Discord.Net.Rest; | ||||
using Newtonsoft.Json; | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Globalization; | |||||
using System.IO; | using System.IO; | ||||
using System.Text; | |||||
namespace Discord.API.Rest | namespace Discord.API.Rest | ||||
{ | { | ||||
internal class UploadFileParams | internal class UploadFileParams | ||||
{ | { | ||||
private static JsonSerializer _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; | |||||
public Stream File { get; } | public Stream File { get; } | ||||
public Optional<string> Filename { get; set; } | public Optional<string> Filename { get; set; } | ||||
public Optional<string> Content { get; set; } | public Optional<string> Content { get; set; } | ||||
public Optional<string> Nonce { get; set; } | public Optional<string> Nonce { get; set; } | ||||
public Optional<bool> IsTTS { get; set; } | public Optional<bool> IsTTS { get; set; } | ||||
public Optional<Embed> Embed { get; set; } | |||||
public UploadFileParams(Stream file) | public UploadFileParams(Stream file) | ||||
{ | { | ||||
@@ -23,12 +30,24 @@ namespace Discord.API.Rest | |||||
{ | { | ||||
var d = new Dictionary<string, object>(); | var d = new Dictionary<string, object>(); | ||||
d["file"] = new MultipartFile(File, Filename.GetValueOrDefault("unknown.dat")); | d["file"] = new MultipartFile(File, Filename.GetValueOrDefault("unknown.dat")); | ||||
var payload = new Dictionary<string, object>(); | |||||
if (Content.IsSpecified) | if (Content.IsSpecified) | ||||
d["content"] = Content.Value; | |||||
payload["content"] = Content.Value; | |||||
if (IsTTS.IsSpecified) | if (IsTTS.IsSpecified) | ||||
d["tts"] = IsTTS.Value.ToString(); | |||||
payload["tts"] = IsTTS.Value.ToString(); | |||||
if (Nonce.IsSpecified) | if (Nonce.IsSpecified) | ||||
d["nonce"] = Nonce.Value; | |||||
payload["nonce"] = Nonce.Value; | |||||
if (Embed.IsSpecified) | |||||
payload["embed"] = Embed.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; | return d; | ||||
} | } | ||||
} | } | ||||
@@ -1,7 +1,22 @@ | |||||
using System.Runtime.CompilerServices; | |||||
using System.Runtime.CompilerServices; | |||||
[assembly: InternalsVisibleTo("Discord.Net.Rpc")] | [assembly: InternalsVisibleTo("Discord.Net.Rpc")] | ||||
[assembly: InternalsVisibleTo("Discord.Net.WebSocket")] | [assembly: InternalsVisibleTo("Discord.Net.WebSocket")] | ||||
[assembly: InternalsVisibleTo("Discord.Net.Webhook")] | [assembly: InternalsVisibleTo("Discord.Net.Webhook")] | ||||
[assembly: InternalsVisibleTo("Discord.Net.Commands")] | [assembly: InternalsVisibleTo("Discord.Net.Commands")] | ||||
[assembly: InternalsVisibleTo("Discord.Net.Tests")] | |||||
[assembly: InternalsVisibleTo("Discord.Net.Tests")] | |||||
[assembly: TypeForwardedTo(typeof(Discord.Embed))] | |||||
[assembly: TypeForwardedTo(typeof(Discord.EmbedBuilder))] | |||||
[assembly: TypeForwardedTo(typeof(Discord.EmbedBuilderExtensions))] | |||||
[assembly: TypeForwardedTo(typeof(Discord.EmbedAuthor))] | |||||
[assembly: TypeForwardedTo(typeof(Discord.EmbedAuthorBuilder))] | |||||
[assembly: TypeForwardedTo(typeof(Discord.EmbedField))] | |||||
[assembly: TypeForwardedTo(typeof(Discord.EmbedFieldBuilder))] | |||||
[assembly: TypeForwardedTo(typeof(Discord.EmbedFooter))] | |||||
[assembly: TypeForwardedTo(typeof(Discord.EmbedFooterBuilder))] | |||||
[assembly: TypeForwardedTo(typeof(Discord.EmbedImage))] | |||||
[assembly: TypeForwardedTo(typeof(Discord.EmbedProvider))] | |||||
[assembly: TypeForwardedTo(typeof(Discord.EmbedThumbnail))] | |||||
[assembly: TypeForwardedTo(typeof(Discord.EmbedType))] | |||||
[assembly: TypeForwardedTo(typeof(Discord.EmbedVideo))] |
@@ -1,4 +1,4 @@ | |||||
using Discord.Logging; | |||||
using Discord.Logging; | |||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
@@ -125,6 +125,10 @@ namespace Discord.Rest | |||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public void Dispose() => Dispose(true); | public void Dispose() => Dispose(true); | ||||
/// <inheritdoc /> | |||||
public Task<int> GetRecommendedShardCountAsync(RequestOptions options = null) | |||||
=> ClientHelper.GetRecommendShardCountAsync(this, options); | |||||
//IDiscordClient | //IDiscordClient | ||||
ConnectionState IDiscordClient.ConnectionState => ConnectionState.Disconnected; | ConnectionState IDiscordClient.ConnectionState => ConnectionState.Disconnected; | ||||
ISelfUser IDiscordClient.CurrentUser => CurrentUser; | ISelfUser IDiscordClient.CurrentUser => CurrentUser; | ||||
@@ -1,4 +1,4 @@ | |||||
using Discord.API.Rest; | |||||
using Discord.API.Rest; | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
using System.IO; | using System.IO; | ||||
@@ -163,5 +163,11 @@ namespace Discord.Rest | |||||
var models = await client.ApiClient.GetVoiceRegionsAsync(options).ConfigureAwait(false); | var models = await client.ApiClient.GetVoiceRegionsAsync(options).ConfigureAwait(false); | ||||
return models.Select(x => RestVoiceRegion.Create(client, x)).FirstOrDefault(x => x.Id == id); | return models.Select(x => RestVoiceRegion.Create(client, x)).FirstOrDefault(x => x.Id == id); | ||||
} | } | ||||
public static async Task<int> GetRecommendShardCountAsync(BaseDiscordClient client, RequestOptions options) | |||||
{ | |||||
var response = await client.ApiClient.GetBotGatewayAsync(options).ConfigureAwait(false); | |||||
return response.Shards; | |||||
} | |||||
} | } | ||||
} | } |
@@ -1,5 +1,4 @@ | |||||
#pragma warning disable CS1591 | #pragma warning disable CS1591 | ||||
#pragma warning disable CS0618 | |||||
using Discord.API.Rest; | using Discord.API.Rest; | ||||
using Discord.Net; | using Discord.Net; | ||||
using Discord.Net.Converters; | using Discord.Net.Converters; | ||||
@@ -70,12 +69,12 @@ namespace Discord.API | |||||
{ | { | ||||
switch (tokenType) | switch (tokenType) | ||||
{ | { | ||||
case default(TokenType): | |||||
return token; | |||||
case TokenType.Bot: | case TokenType.Bot: | ||||
return $"Bot {token}"; | return $"Bot {token}"; | ||||
case TokenType.Bearer: | case TokenType.Bearer: | ||||
return $"Bearer {token}"; | return $"Bearer {token}"; | ||||
case TokenType.User: | |||||
return token; | |||||
default: | default: | ||||
throw new ArgumentException("Unknown OAuth token type", nameof(tokenType)); | throw new ArgumentException("Unknown OAuth token type", nameof(tokenType)); | ||||
} | } | ||||
@@ -113,7 +112,6 @@ namespace Discord.API | |||||
{ | { | ||||
_loginCancelToken = new CancellationTokenSource(); | _loginCancelToken = new CancellationTokenSource(); | ||||
AuthTokenType = TokenType.User; | |||||
AuthToken = null; | AuthToken = null; | ||||
await RequestQueue.SetCancelTokenAsync(_loginCancelToken.Token).ConfigureAwait(false); | await RequestQueue.SetCancelTokenAsync(_loginCancelToken.Token).ConfigureAwait(false); | ||||
RestClient.SetCancelToken(_loginCancelToken.Token); | RestClient.SetCancelToken(_loginCancelToken.Token); | ||||
@@ -172,8 +170,7 @@ namespace Discord.API | |||||
{ | { | ||||
options = options ?? new RequestOptions(); | options = options ?? new RequestOptions(); | ||||
options.HeaderOnly = true; | options.HeaderOnly = true; | ||||
options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; | |||||
options.IsClientBucket = AuthTokenType == TokenType.User; | |||||
options.BucketId = bucketId; | |||||
var request = new RestRequest(RestClient, method, endpoint, options); | var request = new RestRequest(RestClient, method, endpoint, options); | ||||
await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); | await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); | ||||
@@ -187,8 +184,7 @@ namespace Discord.API | |||||
{ | { | ||||
options = options ?? new RequestOptions(); | options = options ?? new RequestOptions(); | ||||
options.HeaderOnly = true; | options.HeaderOnly = true; | ||||
options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; | |||||
options.IsClientBucket = AuthTokenType == TokenType.User; | |||||
options.BucketId = bucketId; | |||||
string json = payload != null ? SerializeJson(payload) : null; | string json = payload != null ? SerializeJson(payload) : null; | ||||
var request = new JsonRestRequest(RestClient, method, endpoint, json, options); | var request = new JsonRestRequest(RestClient, method, endpoint, json, options); | ||||
@@ -203,8 +199,7 @@ namespace Discord.API | |||||
{ | { | ||||
options = options ?? new RequestOptions(); | options = options ?? new RequestOptions(); | ||||
options.HeaderOnly = true; | options.HeaderOnly = true; | ||||
options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; | |||||
options.IsClientBucket = AuthTokenType == TokenType.User; | |||||
options.BucketId = bucketId; | |||||
var request = new MultipartRestRequest(RestClient, method, endpoint, multipartArgs, options); | var request = new MultipartRestRequest(RestClient, method, endpoint, multipartArgs, options); | ||||
await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); | await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); | ||||
@@ -217,8 +212,7 @@ namespace Discord.API | |||||
string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class | string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class | ||||
{ | { | ||||
options = options ?? new RequestOptions(); | options = options ?? new RequestOptions(); | ||||
options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; | |||||
options.IsClientBucket = AuthTokenType == TokenType.User; | |||||
options.BucketId = bucketId; | |||||
var request = new RestRequest(RestClient, method, endpoint, options); | var request = new RestRequest(RestClient, method, endpoint, options); | ||||
return DeserializeJson<TResponse>(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false)); | return DeserializeJson<TResponse>(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false)); | ||||
@@ -231,8 +225,7 @@ namespace Discord.API | |||||
string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class | string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class | ||||
{ | { | ||||
options = options ?? new RequestOptions(); | options = options ?? new RequestOptions(); | ||||
options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; | |||||
options.IsClientBucket = AuthTokenType == TokenType.User; | |||||
options.BucketId = bucketId; | |||||
string json = payload != null ? SerializeJson(payload) : null; | string json = payload != null ? SerializeJson(payload) : null; | ||||
var request = new JsonRestRequest(RestClient, method, endpoint, json, options); | var request = new JsonRestRequest(RestClient, method, endpoint, json, options); | ||||
@@ -246,8 +239,7 @@ namespace Discord.API | |||||
string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) | string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) | ||||
{ | { | ||||
options = options ?? new RequestOptions(); | options = options ?? new RequestOptions(); | ||||
options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; | |||||
options.IsClientBucket = AuthTokenType == TokenType.User; | |||||
options.BucketId = bucketId; | |||||
var request = new MultipartRestRequest(RestClient, method, endpoint, multipartArgs, options); | var request = new MultipartRestRequest(RestClient, method, endpoint, multipartArgs, options); | ||||
return DeserializeJson<TResponse>(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false)); | return DeserializeJson<TResponse>(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false)); | ||||
@@ -277,6 +269,18 @@ namespace Discord.API | |||||
await SendAsync("GET", () => "auth/login", new BucketIds(), options: options).ConfigureAwait(false); | await SendAsync("GET", () => "auth/login", new BucketIds(), options: options).ConfigureAwait(false); | ||||
} | } | ||||
//Gateway | |||||
public async Task<GetGatewayResponse> GetGatewayAsync(RequestOptions options = null) | |||||
{ | |||||
options = RequestOptions.CreateOrClone(options); | |||||
return await SendAsync<GetGatewayResponse>("GET", () => "gateway", new BucketIds(), options: options).ConfigureAwait(false); | |||||
} | |||||
public async Task<GetBotGatewayResponse> GetBotGatewayAsync(RequestOptions options = null) | |||||
{ | |||||
options = RequestOptions.CreateOrClone(options); | |||||
return await SendAsync<GetBotGatewayResponse>("GET", () => "gateway/bot", new BucketIds(), options: options).ConfigureAwait(false); | |||||
} | |||||
//Channels | //Channels | ||||
public async Task<Channel> GetChannelAsync(ulong channelId, RequestOptions options = null) | public async Task<Channel> GetChannelAsync(ulong channelId, RequestOptions options = null) | ||||
{ | { | ||||
@@ -466,7 +470,7 @@ namespace Discord.API | |||||
if (!args.Embed.IsSpecified || args.Embed.Value == null) | if (!args.Embed.IsSpecified || args.Embed.Value == null) | ||||
Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); | Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); | ||||
if (args.Content.Length > DiscordConfig.MaxMessageSize) | |||||
if (args.Content?.Length > DiscordConfig.MaxMessageSize) | |||||
throw new ArgumentException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); | throw new ArgumentException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); | ||||
options = RequestOptions.CreateOrClone(options); | options = RequestOptions.CreateOrClone(options); | ||||
@@ -483,7 +487,7 @@ namespace Discord.API | |||||
if (!args.Embeds.IsSpecified || args.Embeds.Value == null || args.Embeds.Value.Length == 0) | if (!args.Embeds.IsSpecified || args.Embeds.Value == null || args.Embeds.Value.Length == 0) | ||||
Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); | Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); | ||||
if (args.Content.Length > DiscordConfig.MaxMessageSize) | |||||
if (args.Content?.Length > DiscordConfig.MaxMessageSize) | |||||
throw new ArgumentException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); | throw new ArgumentException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); | ||||
options = RequestOptions.CreateOrClone(options); | options = RequestOptions.CreateOrClone(options); | ||||
@@ -564,7 +568,7 @@ namespace Discord.API | |||||
{ | { | ||||
if (!args.Embed.IsSpecified) | if (!args.Embed.IsSpecified) | ||||
Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); | Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); | ||||
if (args.Content.Value.Length > DiscordConfig.MaxMessageSize) | |||||
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)); | throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); | ||||
} | } | ||||
options = RequestOptions.CreateOrClone(options); | options = RequestOptions.CreateOrClone(options); | ||||
@@ -1,4 +1,4 @@ | |||||
using System.Collections.Generic; | |||||
using System.Collections.Generic; | |||||
using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
using System.IO; | using System.IO; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
@@ -1,4 +1,4 @@ | |||||
using Discord.API.Rest; | |||||
using Discord.API.Rest; | |||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
@@ -170,17 +170,17 @@ namespace Discord.Rest | |||||
#if FILESYSTEM | #if FILESYSTEM | ||||
public static async Task<RestUserMessage> SendFileAsync(IMessageChannel channel, BaseDiscordClient client, | public static async Task<RestUserMessage> SendFileAsync(IMessageChannel channel, BaseDiscordClient client, | ||||
string filePath, string text, bool isTTS, RequestOptions options) | |||||
string filePath, string text, bool isTTS, Embed embed, RequestOptions options) | |||||
{ | { | ||||
string filename = Path.GetFileName(filePath); | string filename = Path.GetFileName(filePath); | ||||
using (var file = File.OpenRead(filePath)) | using (var file = File.OpenRead(filePath)) | ||||
return await SendFileAsync(channel, client, file, filename, text, isTTS, options).ConfigureAwait(false); | |||||
return await SendFileAsync(channel, client, file, filename, text, isTTS, embed, options).ConfigureAwait(false); | |||||
} | } | ||||
#endif | #endif | ||||
public static async Task<RestUserMessage> SendFileAsync(IMessageChannel channel, BaseDiscordClient client, | public static async Task<RestUserMessage> SendFileAsync(IMessageChannel channel, BaseDiscordClient client, | ||||
Stream stream, string filename, string text, bool isTTS, RequestOptions options) | |||||
Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) | |||||
{ | { | ||||
var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS }; | |||||
var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS, Embed = embed != null ? embed.ToModel() : Optional<API.Embed>.Unspecified }; | |||||
var model = await client.ApiClient.UploadFileAsync(channel.Id, args, options).ConfigureAwait(false); | var model = await client.ApiClient.UploadFileAsync(channel.Id, args, options).ConfigureAwait(false); | ||||
return RestUserMessage.Create(client, channel, client.CurrentUser, model); | return RestUserMessage.Create(client, channel, client.CurrentUser, model); | ||||
} | } | ||||
@@ -1,4 +1,4 @@ | |||||
using System.Collections.Generic; | |||||
using System.Collections.Generic; | |||||
using System.IO; | using System.IO; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
@@ -10,10 +10,10 @@ namespace Discord.Rest | |||||
new Task<RestUserMessage> SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null); | new Task<RestUserMessage> SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null); | ||||
#if FILESYSTEM | #if FILESYSTEM | ||||
/// <summary> Sends a file to this text channel, with an optional caption. </summary> | /// <summary> Sends a file to this text channel, with an optional caption. </summary> | ||||
new Task<RestUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, RequestOptions options = null); | |||||
new Task<RestUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); | |||||
#endif | #endif | ||||
/// <summary> Sends a file to this text channel, with an optional caption. </summary> | /// <summary> Sends a file to this text channel, with an optional caption. </summary> | ||||
new Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, RequestOptions options = null); | |||||
new Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); | |||||
/// <summary> Gets a message from this message channel with the given id, or null if not found. </summary> | /// <summary> Gets a message from this message channel with the given id, or null if not found. </summary> | ||||
Task<RestMessage> GetMessageAsync(ulong id, RequestOptions options = null); | Task<RestMessage> GetMessageAsync(ulong id, RequestOptions options = null); | ||||
@@ -1,4 +1,4 @@ | |||||
using System; | |||||
using System; | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
using System.Diagnostics; | using System.Diagnostics; | ||||
@@ -37,7 +37,7 @@ namespace Discord.Rest | |||||
public override async Task UpdateAsync(RequestOptions options = null) | public override async Task UpdateAsync(RequestOptions options = null) | ||||
{ | { | ||||
var model = await Discord.ApiClient.GetChannelAsync(Id, options).ConfigureAwait(false); | var model = await Discord.ApiClient.GetChannelAsync(Id, options).ConfigureAwait(false); | ||||
Update(model); | |||||
Update(model); | |||||
} | } | ||||
public Task CloseAsync(RequestOptions options = null) | public Task CloseAsync(RequestOptions options = null) | ||||
=> ChannelHelper.DeleteAsync(this, Discord, options); | => ChannelHelper.DeleteAsync(this, Discord, options); | ||||
@@ -66,11 +66,11 @@ namespace Discord.Rest | |||||
public Task<RestUserMessage> SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) | public Task<RestUserMessage> SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) | ||||
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); | => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); | ||||
#if FILESYSTEM | #if FILESYSTEM | ||||
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) | |||||
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); | |||||
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) | |||||
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options); | |||||
#endif | #endif | ||||
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) | |||||
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); | |||||
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) | |||||
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options); | |||||
public Task TriggerTypingAsync(RequestOptions options = null) | public Task TriggerTypingAsync(RequestOptions options = null) | ||||
=> ChannelHelper.TriggerTypingAsync(this, Discord, options); | => ChannelHelper.TriggerTypingAsync(this, Discord, options); | ||||
@@ -122,11 +122,11 @@ namespace Discord.Rest | |||||
=> await GetPinnedMessagesAsync(options).ConfigureAwait(false); | => await GetPinnedMessagesAsync(options).ConfigureAwait(false); | ||||
#if FILESYSTEM | #if FILESYSTEM | ||||
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) | |||||
=> await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); | |||||
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options) | |||||
=> await SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false); | |||||
#endif | #endif | ||||
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) | |||||
=> await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); | |||||
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) | |||||
=> await SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false); | |||||
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) | async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) | ||||
=> await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); | => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); | ||||
IDisposable IMessageChannel.EnterTypingState(RequestOptions options) | IDisposable IMessageChannel.EnterTypingState(RequestOptions options) | ||||
@@ -1,4 +1,4 @@ | |||||
using Discord.Audio; | |||||
using Discord.Audio; | |||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
@@ -19,7 +19,7 @@ namespace Discord.Rest | |||||
public string Name { get; private set; } | public string Name { get; private set; } | ||||
public IReadOnlyCollection<RestGroupUser> Users => _users.ToReadOnlyCollection(); | public IReadOnlyCollection<RestGroupUser> Users => _users.ToReadOnlyCollection(); | ||||
public IReadOnlyCollection<RestGroupUser> Recipients | |||||
public IReadOnlyCollection<RestGroupUser> Recipients | |||||
=> _users.Select(x => x.Value).Where(x => x.Id != Discord.CurrentUser.Id).ToReadOnlyCollection(() => _users.Count - 1); | => _users.Select(x => x.Value).Where(x => x.Id != Discord.CurrentUser.Id).ToReadOnlyCollection(() => _users.Count - 1); | ||||
internal RestGroupChannel(BaseDiscordClient discord, ulong id) | internal RestGroupChannel(BaseDiscordClient discord, ulong id) | ||||
@@ -79,11 +79,11 @@ namespace Discord.Rest | |||||
public Task<RestUserMessage> SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) | public Task<RestUserMessage> SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) | ||||
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); | => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); | ||||
#if FILESYSTEM | #if FILESYSTEM | ||||
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) | |||||
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); | |||||
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) | |||||
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options); | |||||
#endif | #endif | ||||
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) | |||||
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); | |||||
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) | |||||
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options); | |||||
public Task TriggerTypingAsync(RequestOptions options = null) | public Task TriggerTypingAsync(RequestOptions options = null) | ||||
=> ChannelHelper.TriggerTypingAsync(this, Discord, options); | => ChannelHelper.TriggerTypingAsync(this, Discord, options); | ||||
@@ -132,11 +132,11 @@ namespace Discord.Rest | |||||
=> await GetPinnedMessagesAsync(options).ConfigureAwait(false); | => await GetPinnedMessagesAsync(options).ConfigureAwait(false); | ||||
#if FILESYSTEM | #if FILESYSTEM | ||||
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) | |||||
=> await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); | |||||
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options) | |||||
=> await SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false); | |||||
#endif | #endif | ||||
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) | |||||
=> await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); | |||||
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) | |||||
=> await SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false); | |||||
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) | async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) | ||||
=> await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); | => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); | ||||
IDisposable IMessageChannel.EnterTypingState(RequestOptions options) | IDisposable IMessageChannel.EnterTypingState(RequestOptions options) | ||||
@@ -1,4 +1,4 @@ | |||||
using System; | |||||
using System; | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Diagnostics; | using System.Diagnostics; | ||||
using System.IO; | using System.IO; | ||||
@@ -61,11 +61,11 @@ namespace Discord.Rest | |||||
public Task<RestUserMessage> SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) | public Task<RestUserMessage> SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) | ||||
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); | => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); | ||||
#if FILESYSTEM | #if FILESYSTEM | ||||
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) | |||||
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); | |||||
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) | |||||
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options); | |||||
#endif | #endif | ||||
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) | |||||
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); | |||||
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) | |||||
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options); | |||||
public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) | public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) | ||||
=> ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); | => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); | ||||
@@ -123,18 +123,18 @@ namespace Discord.Rest | |||||
else | else | ||||
return AsyncEnumerable.Empty<IReadOnlyCollection<IMessage>>(); | return AsyncEnumerable.Empty<IReadOnlyCollection<IMessage>>(); | ||||
} | } | ||||
async Task<IReadOnlyCollection<IMessage>> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) | |||||
async Task<IReadOnlyCollection<IMessage>> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) | |||||
=> await GetPinnedMessagesAsync(options).ConfigureAwait(false); | => await GetPinnedMessagesAsync(options).ConfigureAwait(false); | ||||
#if FILESYSTEM | #if FILESYSTEM | ||||
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) | |||||
=> await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); | |||||
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options) | |||||
=> await SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false); | |||||
#endif | #endif | ||||
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) | |||||
=> await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); | |||||
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) | |||||
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) | |||||
=> await SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false); | |||||
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) | |||||
=> await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); | => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); | ||||
IDisposable IMessageChannel.EnterTypingState(RequestOptions options) | |||||
IDisposable IMessageChannel.EnterTypingState(RequestOptions options) | |||||
=> EnterTypingState(options); | => EnterTypingState(options); | ||||
//IGuildChannel | //IGuildChannel | ||||
@@ -1,4 +1,4 @@ | |||||
using System; | |||||
using System; | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Diagnostics; | using System.Diagnostics; | ||||
using System.IO; | using System.IO; | ||||
@@ -21,7 +21,7 @@ namespace Discord.Rest | |||||
{ | { | ||||
return new RestVirtualMessageChannel(discord, id); | return new RestVirtualMessageChannel(discord, id); | ||||
} | } | ||||
public Task<RestMessage> GetMessageAsync(ulong id, RequestOptions options = null) | public Task<RestMessage> GetMessageAsync(ulong id, RequestOptions options = null) | ||||
=> ChannelHelper.GetMessageAsync(this, Discord, id, options); | => ChannelHelper.GetMessageAsync(this, Discord, id, options); | ||||
public IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) | public IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) | ||||
@@ -36,11 +36,11 @@ namespace Discord.Rest | |||||
public Task<RestUserMessage> SendMessageAsync(string text, bool isTTS, Embed embed = null, RequestOptions options = null) | public Task<RestUserMessage> SendMessageAsync(string text, bool isTTS, Embed embed = null, RequestOptions options = null) | ||||
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); | => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); | ||||
#if FILESYSTEM | #if FILESYSTEM | ||||
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options = null) | |||||
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); | |||||
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS, Embed embed = null, RequestOptions options = null) | |||||
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options); | |||||
#endif | #endif | ||||
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options = null) | |||||
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); | |||||
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed = null, RequestOptions options = null) | |||||
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options); | |||||
public Task TriggerTypingAsync(RequestOptions options = null) | public Task TriggerTypingAsync(RequestOptions options = null) | ||||
=> ChannelHelper.TriggerTypingAsync(this, Discord, options); | => ChannelHelper.TriggerTypingAsync(this, Discord, options); | ||||
@@ -82,11 +82,11 @@ namespace Discord.Rest | |||||
=> await GetPinnedMessagesAsync(options); | => await GetPinnedMessagesAsync(options); | ||||
#if FILESYSTEM | #if FILESYSTEM | ||||
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) | |||||
=> await SendFileAsync(filePath, text, isTTS, options); | |||||
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options) | |||||
=> await SendFileAsync(filePath, text, isTTS, embed, options); | |||||
#endif | #endif | ||||
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) | |||||
=> await SendFileAsync(stream, filename, text, isTTS, options); | |||||
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) | |||||
=> await SendFileAsync(stream, filename, text, isTTS, embed, options); | |||||
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) | async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) | ||||
=> await SendMessageAsync(text, isTTS, embed, options); | => await SendMessageAsync(text, isTTS, embed, options); | ||||
IDisposable IMessageChannel.EnterTypingState(RequestOptions options) | IDisposable IMessageChannel.EnterTypingState(RequestOptions options) | ||||
@@ -1,4 +1,4 @@ | |||||
using Discord.API.Rest; | |||||
using Discord.API.Rest; | |||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
@@ -13,6 +13,9 @@ namespace Discord.Rest | |||||
public static async Task<Model> ModifyAsync(IMessage msg, BaseDiscordClient client, Action<MessageProperties> func, | public static async Task<Model> ModifyAsync(IMessage msg, BaseDiscordClient client, Action<MessageProperties> func, | ||||
RequestOptions options) | RequestOptions options) | ||||
{ | { | ||||
if (msg.Author.Id != client.CurrentUser.Id) | |||||
throw new InvalidOperationException("Only the author of a message may change it."); | |||||
var args = new MessageProperties(); | var args = new MessageProperties(); | ||||
func(args); | func(args); | ||||
var apiArgs = new API.Rest.ModifyMessageParams | var apiArgs = new API.Rest.ModifyMessageParams | ||||
@@ -1,4 +1,4 @@ | |||||
using System; | |||||
using System; | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
using System.Diagnostics; | using System.Diagnostics; | ||||
@@ -1,4 +1,4 @@ | |||||
using System; | |||||
using System; | |||||
using System.Diagnostics; | using System.Diagnostics; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
using Model = Discord.API.User; | using Model = Discord.API.User; | ||||
@@ -60,6 +60,9 @@ namespace Discord.Rest | |||||
public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) | public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) | ||||
=> CDN.GetUserAvatarUrl(Id, AvatarId, size, format); | => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); | ||||
public string GetDefaultAvatarUrl() | |||||
=> CDN.GetDefaultUserAvatarUrl(DiscriminatorValue); | |||||
public override string ToString() => $"{Username}#{Discriminator}"; | public override string ToString() => $"{Username}#{Discriminator}"; | ||||
private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; | private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; | ||||
@@ -1,4 +1,4 @@ | |||||
using Newtonsoft.Json; | |||||
using Newtonsoft.Json; | |||||
using System; | using System; | ||||
namespace Discord.Net.Converters | namespace Discord.Net.Converters | ||||
@@ -19,10 +19,18 @@ namespace Discord.Net.Converters | |||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) | public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) | ||||
{ | { | ||||
T obj; | 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) | if (_innerConverter != null) | ||||
obj = (T)_innerConverter.ReadJson(reader, typeof(T), null, serializer); | |||||
{ | |||||
object o = _innerConverter.ReadJson(reader, typeof(T), null, serializer); | |||||
if (o is Optional<T>) | |||||
return o; | |||||
obj = (T)o; | |||||
} | |||||
else | else | ||||
obj = serializer.Deserialize<T>(reader); | obj = serializer.Deserialize<T>(reader); | ||||
return new Optional<T>(obj); | return new Optional<T>(obj); | ||||
} | } | ||||
@@ -1,4 +1,4 @@ | |||||
using System; | |||||
using System; | |||||
using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
namespace Discord.Net.Converters | namespace Discord.Net.Converters | ||||
@@ -11,13 +11,18 @@ namespace Discord.Net.Converters | |||||
public override bool CanRead => true; | public override bool CanRead => true; | ||||
public override bool CanWrite => 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) | public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) | ||||
{ | { | ||||
// Discord doesn't validate if timestamps contain decimals or not | |||||
if (reader.Value is double d) | |||||
// 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); | return new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, TimeSpan.Zero).AddMilliseconds(d); | ||||
long offset = (long)reader.Value; | |||||
return new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, TimeSpan.Zero).AddMilliseconds(offset); | |||||
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<DateTimeOffset>.Unspecified; | |||||
} | } | ||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) | public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) | ||||
@@ -25,4 +30,4 @@ namespace Discord.Net.Converters | |||||
throw new NotImplementedException(); | throw new NotImplementedException(); | ||||
} | } | ||||
} | } | ||||
} | |||||
} |
@@ -1,4 +1,5 @@ | |||||
using Newtonsoft.Json; | |||||
using Discord.Net.Converters; | |||||
using Newtonsoft.Json; | |||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Globalization; | using System.Globalization; | ||||
@@ -1,4 +1,4 @@ | |||||
using Newtonsoft.Json; | |||||
using Newtonsoft.Json; | |||||
using Newtonsoft.Json.Linq; | using Newtonsoft.Json.Linq; | ||||
using System; | using System; | ||||
#if DEBUG_LIMITS | #if DEBUG_LIMITS | ||||
@@ -86,7 +86,7 @@ namespace Discord.Net.Queue | |||||
Debug.WriteLine($"[{id}] (!) 502"); | Debug.WriteLine($"[{id}] (!) 502"); | ||||
#endif | #endif | ||||
if ((request.Options.RetryMode & RetryMode.Retry502) == 0) | if ((request.Options.RetryMode & RetryMode.Retry502) == 0) | ||||
throw new HttpException(HttpStatusCode.BadGateway, null); | |||||
throw new HttpException(HttpStatusCode.BadGateway, request, null); | |||||
continue; //Retry | continue; //Retry | ||||
default: | default: | ||||
@@ -106,7 +106,7 @@ namespace Discord.Net.Queue | |||||
} | } | ||||
catch { } | catch { } | ||||
} | } | ||||
throw new HttpException(response.StatusCode, code, reason); | |||||
throw new HttpException(response.StatusCode, request, code, reason); | |||||
} | } | ||||
} | } | ||||
else | else | ||||
@@ -163,7 +163,7 @@ namespace Discord.Net.Queue | |||||
if (!isRateLimited) | if (!isRateLimited) | ||||
throw new TimeoutException(); | throw new TimeoutException(); | ||||
else | else | ||||
throw new RateLimitedException(); | |||||
throw new RateLimitedException(request); | |||||
} | } | ||||
lock (_lock) | lock (_lock) | ||||
@@ -182,12 +182,12 @@ namespace Discord.Net.Queue | |||||
} | } | ||||
if ((request.Options.RetryMode & RetryMode.RetryRatelimit) == 0) | if ((request.Options.RetryMode & RetryMode.RetryRatelimit) == 0) | ||||
throw new RateLimitedException(); | |||||
throw new RateLimitedException(request); | |||||
if (resetAt.HasValue) | if (resetAt.HasValue) | ||||
{ | { | ||||
if (resetAt > timeoutAt) | if (resetAt > timeoutAt) | ||||
throw new RateLimitedException(); | |||||
throw new RateLimitedException(request); | |||||
int millis = (int)Math.Ceiling((resetAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds); | int millis = (int)Math.Ceiling((resetAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds); | ||||
#if DEBUG_LIMITS | #if DEBUG_LIMITS | ||||
Debug.WriteLine($"[{id}] Sleeping {millis} ms (Pre-emptive)"); | Debug.WriteLine($"[{id}] Sleeping {millis} ms (Pre-emptive)"); | ||||
@@ -198,7 +198,7 @@ namespace Discord.Net.Queue | |||||
else | else | ||||
{ | { | ||||
if ((timeoutAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds < 500.0) | if ((timeoutAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds < 500.0) | ||||
throw new RateLimitedException(); | |||||
throw new RateLimitedException(request); | |||||
#if DEBUG_LIMITS | #if DEBUG_LIMITS | ||||
Debug.WriteLine($"[{id}] Sleeping 500* ms (Pre-emptive)"); | Debug.WriteLine($"[{id}] Sleeping 500* ms (Pre-emptive)"); | ||||
#endif | #endif | ||||
@@ -1,11 +1,11 @@ | |||||
using Discord.Net.Rest; | |||||
using Discord.Net.Rest; | |||||
using System; | using System; | ||||
using System.IO; | using System.IO; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
namespace Discord.Net.Queue | namespace Discord.Net.Queue | ||||
{ | { | ||||
public class RestRequest | |||||
public class RestRequest : IRequest | |||||
{ | { | ||||
public IRestClient Client { get; } | public IRestClient Client { get; } | ||||
public string Method { get; } | public string Method { get; } | ||||
@@ -1,4 +1,4 @@ | |||||
using Discord.Net.WebSockets; | |||||
using Discord.Net.WebSockets; | |||||
using System; | using System; | ||||
using System.IO; | using System.IO; | ||||
using System.Threading; | using System.Threading; | ||||
@@ -6,7 +6,7 @@ using System.Threading.Tasks; | |||||
namespace Discord.Net.Queue | namespace Discord.Net.Queue | ||||
{ | { | ||||
public class WebSocketRequest | |||||
public class WebSocketRequest : IRequest | |||||
{ | { | ||||
public IWebSocketClient Client { get; } | public IWebSocketClient Client { get; } | ||||
public string BucketId { get; } | public string BucketId { get; } | ||||
@@ -1,4 +1,4 @@ | |||||
using System.Collections.Generic; | |||||
using System.Collections.Generic; | |||||
using System.IO; | using System.IO; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
using Discord.API; | using Discord.API; | ||||
@@ -44,7 +44,7 @@ namespace Discord.WebSocket | |||||
/// <inheritdoc /> | /// <inheritdoc /> | ||||
public abstract Task StopAsync(); | public abstract Task StopAsync(); | ||||
public abstract Task SetStatusAsync(UserStatus status); | public abstract Task SetStatusAsync(UserStatus status); | ||||
public abstract Task SetGameAsync(string name, string streamUrl = null, StreamType streamType = StreamType.NotStreaming); | |||||
public abstract Task SetGameAsync(string name, string streamUrl = null, ActivityType type = ActivityType.Playing); | |||||
public abstract Task SetActivityAsync(IActivity activity); | public abstract Task SetActivityAsync(IActivity activity); | ||||
public abstract Task DownloadUsersAsync(IEnumerable<IGuild> guilds); | public abstract Task DownloadUsersAsync(IEnumerable<IGuild> guilds); | ||||
@@ -75,8 +75,8 @@ namespace Discord.WebSocket | |||||
{ | { | ||||
if (_automaticShards) | if (_automaticShards) | ||||
{ | { | ||||
var response = await ApiClient.GetBotGatewayAsync().ConfigureAwait(false); | |||||
_shardIds = Enumerable.Range(0, response.Shards).ToArray(); | |||||
var shardCount = await GetRecommendedShardCountAsync().ConfigureAwait(false); | |||||
_shardIds = Enumerable.Range(0, shardCount).ToArray(); | |||||
_totalShards = _shardIds.Length; | _totalShards = _shardIds.Length; | ||||
_shards = new DiscordSocketClient[_shardIds.Length]; | _shards = new DiscordSocketClient[_shardIds.Length]; | ||||
for (int i = 0; i < _shardIds.Length; i++) | for (int i = 0; i < _shardIds.Length; i++) | ||||
@@ -238,13 +238,13 @@ namespace Discord.WebSocket | |||||
for (int i = 0; i < _shards.Length; i++) | for (int i = 0; i < _shards.Length; i++) | ||||
await _shards[i].SetStatusAsync(status).ConfigureAwait(false); | await _shards[i].SetStatusAsync(status).ConfigureAwait(false); | ||||
} | } | ||||
public override async Task SetGameAsync(string name, string streamUrl = null, StreamType streamType = StreamType.NotStreaming) | |||||
public override async Task SetGameAsync(string name, string streamUrl = null, ActivityType type = ActivityType.Playing) | |||||
{ | { | ||||
IActivity activity = null; | IActivity activity = null; | ||||
if (streamUrl != null) | |||||
activity = new StreamingGame(name, streamUrl, streamType); | |||||
else if (name != null) | |||||
activity = new Game(name); | |||||
if (!string.IsNullOrEmpty(streamUrl)) | |||||
activity = new StreamingGame(name, streamUrl); | |||||
else if (!string.IsNullOrEmpty(name)) | |||||
activity = new Game(name, type); | |||||
await SetActivityAsync(activity).ConfigureAwait(false); | await SetActivityAsync(activity).ConfigureAwait(false); | ||||
} | } | ||||
public override async Task SetActivityAsync(IActivity activity) | public override async Task SetActivityAsync(IActivity activity) | ||||
@@ -1,4 +1,4 @@ | |||||
#pragma warning disable CS1591 | |||||
#pragma warning disable CS1591 | |||||
using Discord.API.Gateway; | using Discord.API.Gateway; | ||||
using Discord.API.Rest; | using Discord.API.Rest; | ||||
using Discord.Net.Queue; | using Discord.Net.Queue; | ||||
@@ -207,18 +207,7 @@ namespace Discord.API | |||||
await RequestQueue.SendAsync(new WebSocketRequest(WebSocketClient, null, bytes, true, options)).ConfigureAwait(false); | await RequestQueue.SendAsync(new WebSocketRequest(WebSocketClient, null, bytes, true, options)).ConfigureAwait(false); | ||||
await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false); | await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false); | ||||
} | } | ||||
//Gateway | |||||
public async Task<GetGatewayResponse> GetGatewayAsync(RequestOptions options = null) | |||||
{ | |||||
options = RequestOptions.CreateOrClone(options); | |||||
return await SendAsync<GetGatewayResponse>("GET", () => "gateway", new BucketIds(), options: options).ConfigureAwait(false); | |||||
} | |||||
public async Task<GetBotGatewayResponse> GetBotGatewayAsync(RequestOptions options = null) | |||||
{ | |||||
options = RequestOptions.CreateOrClone(options); | |||||
return await SendAsync<GetBotGatewayResponse>("GET", () => "gateway/bot", new BucketIds(), options: options).ConfigureAwait(false); | |||||
} | |||||
public async Task SendIdentifyAsync(int largeThreshold = 100, int shardID = 0, int totalShards = 1, RequestOptions options = null) | public async Task SendIdentifyAsync(int largeThreshold = 100, int shardID = 0, int totalShards = 1, RequestOptions options = null) | ||||
{ | { | ||||
options = RequestOptions.CreateOrClone(options); | options = RequestOptions.CreateOrClone(options); | ||||
@@ -1,4 +1,3 @@ | |||||
#pragma warning disable CS0618 | |||||
using Discord.API; | using Discord.API; | ||||
using Discord.API.Gateway; | using Discord.API.Gateway; | ||||
using Discord.Logging; | using Discord.Logging; | ||||
@@ -326,12 +325,12 @@ namespace Discord.WebSocket | |||||
_statusSince = null; | _statusSince = null; | ||||
await SendStatusAsync().ConfigureAwait(false); | await SendStatusAsync().ConfigureAwait(false); | ||||
} | } | ||||
public override async Task SetGameAsync(string name, string streamUrl = null, StreamType streamType = StreamType.NotStreaming) | |||||
public override async Task SetGameAsync(string name, string streamUrl = null, ActivityType type = ActivityType.Playing) | |||||
{ | { | ||||
if (!string.IsNullOrEmpty(streamUrl)) | if (!string.IsNullOrEmpty(streamUrl)) | ||||
Activity = new StreamingGame(name, streamUrl, streamType); | |||||
Activity = new StreamingGame(name, streamUrl); | |||||
else if (!string.IsNullOrEmpty(name)) | else if (!string.IsNullOrEmpty(name)) | ||||
Activity = new Game(name); | |||||
Activity = new Game(name, type); | |||||
else | else | ||||
Activity = null; | Activity = null; | ||||
await SendStatusAsync().ConfigureAwait(false); | await SendStatusAsync().ConfigureAwait(false); | ||||
@@ -354,15 +353,13 @@ namespace Discord.WebSocket | |||||
// Discord only accepts rich presence over RPC, don't even bother building a payload | // Discord only accepts rich presence over RPC, don't even bother building a payload | ||||
if (Activity is RichGame game) | if (Activity is RichGame game) | ||||
throw new NotSupportedException("Outgoing Rich Presences are not supported"); | throw new NotSupportedException("Outgoing Rich Presences are not supported"); | ||||
else if (Activity is StreamingGame stream) | |||||
{ | |||||
gameModel.StreamUrl = stream.Url; | |||||
gameModel.StreamType = stream.StreamType; | |||||
} | |||||
else if (Activity != null) | |||||
if (Activity != null) | |||||
{ | { | ||||
gameModel.Name = Activity.Name; | gameModel.Name = Activity.Name; | ||||
gameModel.StreamType = StreamType.NotStreaming; | |||||
gameModel.Type = Activity.Type; | |||||
if (Activity is StreamingGame streamGame) | |||||
gameModel.StreamUrl = streamGame.Url; | |||||
} | } | ||||
await ApiClient.SendStatusUpdateAsync( | await ApiClient.SendStatusUpdateAsync( | ||||
@@ -418,11 +415,8 @@ namespace Discord.WebSocket | |||||
_sessionId = null; | _sessionId = null; | ||||
_lastSeq = 0; | _lastSeq = 0; | ||||
bool retry = (bool)payload; | |||||
if (retry) | |||||
_connection.Reconnect(); //TODO: Untested | |||||
else | |||||
await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards).ConfigureAwait(false); | |||||
await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards).ConfigureAwait(false); | |||||
} | } | ||||
break; | break; | ||||
case GatewayOpCode.Reconnect: | case GatewayOpCode.Reconnect: | ||||
@@ -451,7 +445,7 @@ namespace Discord.WebSocket | |||||
{ | { | ||||
var model = data.Guilds[i]; | var model = data.Guilds[i]; | ||||
var guild = AddGuild(model, state); | var guild = AddGuild(model, state); | ||||
if (!guild.IsAvailable || ApiClient.AuthTokenType == TokenType.User) | |||||
if (!guild.IsAvailable) | |||||
unavailableGuilds++; | unavailableGuilds++; | ||||
else | else | ||||
await GuildAvailableAsync(guild).ConfigureAwait(false); | await GuildAvailableAsync(guild).ConfigureAwait(false); | ||||
@@ -470,9 +464,6 @@ namespace Discord.WebSocket | |||||
return; | return; | ||||
} | } | ||||
if (ApiClient.AuthTokenType == TokenType.User) | |||||
await SyncGuildsAsync().ConfigureAwait(false); | |||||
_lastGuildAvailableTime = Environment.TickCount; | _lastGuildAvailableTime = Environment.TickCount; | ||||
_guildDownloadTask = WaitForGuildsAsync(_connection.CancelToken, _gatewayLogger) | _guildDownloadTask = WaitForGuildsAsync(_connection.CancelToken, _gatewayLogger) | ||||
.ContinueWith(async x => | .ContinueWith(async x => | ||||
@@ -547,8 +538,6 @@ namespace Discord.WebSocket | |||||
var guild = AddGuild(data, State); | var guild = AddGuild(data, State); | ||||
if (guild != null) | if (guild != null) | ||||
{ | { | ||||
if (ApiClient.AuthTokenType == TokenType.User) | |||||
await SyncGuildsAsync().ConfigureAwait(false); | |||||
await TimedInvokeAsync(_joinedGuildEvent, nameof(JoinedGuild), guild).ConfigureAwait(false); | await TimedInvokeAsync(_joinedGuildEvent, nameof(JoinedGuild), guild).ConfigureAwait(false); | ||||
} | } | ||||
else | else | ||||
@@ -1,4 +1,4 @@ | |||||
using Discord.Rest; | |||||
using Discord.Rest; | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.IO; | using System.IO; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
@@ -14,10 +14,10 @@ namespace Discord.WebSocket | |||||
new Task<RestUserMessage> SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null); | new Task<RestUserMessage> SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null); | ||||
#if FILESYSTEM | #if FILESYSTEM | ||||
/// <summary> Sends a file to this text channel, with an optional caption. </summary> | /// <summary> Sends a file to this text channel, with an optional caption. </summary> | ||||
new Task<RestUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, RequestOptions options = null); | |||||
new Task<RestUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); | |||||
#endif | #endif | ||||
/// <summary> Sends a file to this text channel, with an optional caption. </summary> | /// <summary> Sends a file to this text channel, with an optional caption. </summary> | ||||
new Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, RequestOptions options = null); | |||||
new Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); | |||||
SocketMessage GetCachedMessage(ulong id); | SocketMessage GetCachedMessage(ulong id); | ||||
/// <summary> Gets the last N messages from this message channel. </summary> | /// <summary> Gets the last N messages from this message channel. </summary> | ||||
@@ -1,4 +1,4 @@ | |||||
using System; | |||||
using System; | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
using System.Diagnostics; | using System.Diagnostics; | ||||
@@ -15,10 +15,12 @@ namespace Discord.WebSocket | |||||
public class SocketCategoryChannel : SocketGuildChannel, ICategoryChannel | public class SocketCategoryChannel : SocketGuildChannel, ICategoryChannel | ||||
{ | { | ||||
public override IReadOnlyCollection<SocketGuildUser> Users | public override IReadOnlyCollection<SocketGuildUser> Users | ||||
=> Guild.Users.Where(x => x.VoiceChannel?.Id == Id).ToImmutableArray(); | |||||
=> Guild.Users.Where(x => Permissions.GetValue( | |||||
Permissions.ResolveChannel(Guild, x, this, Permissions.ResolveGuild(Guild, x)), | |||||
ChannelPermission.ViewChannel)).ToImmutableArray(); | |||||
public IReadOnlyCollection<SocketGuildChannel> Channels | public IReadOnlyCollection<SocketGuildChannel> Channels | ||||
=> Guild.Channels.Where(x => x.CategoryId == CategoryId).ToImmutableArray(); | |||||
=> Guild.Channels.Where(x => x.CategoryId == Id).ToImmutableArray(); | |||||
internal SocketCategoryChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) | internal SocketCategoryChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) | ||||
: base(discord, id, guild) | : base(discord, id, guild) | ||||
@@ -31,14 +33,28 @@ namespace Discord.WebSocket | |||||
return entity; | 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)"; | private string DebuggerDisplay => $"{Name} ({Id}, Category)"; | ||||
internal new SocketCategoryChannel Clone() => MemberwiseClone() as SocketCategoryChannel; | internal new SocketCategoryChannel Clone() => MemberwiseClone() as SocketCategoryChannel; | ||||
// IGuildChannel | // IGuildChannel | ||||
IAsyncEnumerable<IReadOnlyCollection<IGuildUser>> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) | IAsyncEnumerable<IReadOnlyCollection<IGuildUser>> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) | ||||
=> throw new NotSupportedException(); | |||||
=> ImmutableArray.Create<IReadOnlyCollection<IGuildUser>>(Users).ToAsyncEnumerable(); | |||||
Task<IGuildUser> IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) | Task<IGuildUser> IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) | ||||
=> throw new NotSupportedException(); | |||||
=> Task.FromResult<IGuildUser>(GetUser(id)); | |||||
Task<IInviteMetadata> IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) | Task<IInviteMetadata> IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) | ||||
=> throw new NotSupportedException(); | => throw new NotSupportedException(); | ||||
Task<IReadOnlyCollection<IInviteMetadata>> IGuildChannel.GetInvitesAsync(RequestOptions options) | Task<IReadOnlyCollection<IInviteMetadata>> IGuildChannel.GetInvitesAsync(RequestOptions options) | ||||
@@ -46,8 +62,8 @@ namespace Discord.WebSocket | |||||
//IChannel | //IChannel | ||||
IAsyncEnumerable<IReadOnlyCollection<IUser>> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) | IAsyncEnumerable<IReadOnlyCollection<IUser>> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) | ||||
=> throw new NotSupportedException(); | |||||
=> ImmutableArray.Create<IReadOnlyCollection<IUser>>(Users).ToAsyncEnumerable(); | |||||
Task<IUser> IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) | Task<IUser> IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) | ||||
=> throw new NotSupportedException(); | |||||
=> Task.FromResult<IUser>(GetUser(id)); | |||||
} | } | ||||
} | } |
@@ -1,4 +1,4 @@ | |||||
using Discord.Rest; | |||||
using Discord.Rest; | |||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
@@ -70,11 +70,11 @@ namespace Discord.WebSocket | |||||
public Task<RestUserMessage> SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) | public Task<RestUserMessage> SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) | ||||
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); | => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); | ||||
#if FILESYSTEM | #if FILESYSTEM | ||||
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) | |||||
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); | |||||
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) | |||||
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options); | |||||
#endif | #endif | ||||
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) | |||||
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); | |||||
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) | |||||
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options); | |||||
public Task TriggerTypingAsync(RequestOptions options = null) | public Task TriggerTypingAsync(RequestOptions options = null) | ||||
=> ChannelHelper.TriggerTypingAsync(this, Discord, options); | => ChannelHelper.TriggerTypingAsync(this, Discord, options); | ||||
@@ -113,7 +113,7 @@ namespace Discord.WebSocket | |||||
//IPrivateChannel | //IPrivateChannel | ||||
IReadOnlyCollection<IUser> IPrivateChannel.Recipients => ImmutableArray.Create<IUser>(Recipient); | IReadOnlyCollection<IUser> IPrivateChannel.Recipients => ImmutableArray.Create<IUser>(Recipient); | ||||
//IMessageChannel | //IMessageChannel | ||||
async Task<IMessage> IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) | async Task<IMessage> IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) | ||||
{ | { | ||||
@@ -131,11 +131,11 @@ namespace Discord.WebSocket | |||||
async Task<IReadOnlyCollection<IMessage>> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) | async Task<IReadOnlyCollection<IMessage>> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) | ||||
=> await GetPinnedMessagesAsync(options).ConfigureAwait(false); | => await GetPinnedMessagesAsync(options).ConfigureAwait(false); | ||||
#if FILESYSTEM | #if FILESYSTEM | ||||
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) | |||||
=> await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); | |||||
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options) | |||||
=> await SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false); | |||||
#endif | #endif | ||||
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) | |||||
=> await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); | |||||
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) | |||||
=> await SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false); | |||||
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) | async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) | ||||
=> await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); | => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); | ||||
IDisposable IMessageChannel.EnterTypingState(RequestOptions options) | IDisposable IMessageChannel.EnterTypingState(RequestOptions options) | ||||
@@ -1,4 +1,4 @@ | |||||
using Discord.Audio; | |||||
using Discord.Audio; | |||||
using Discord.Rest; | using Discord.Rest; | ||||
using System; | using System; | ||||
using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
@@ -61,7 +61,7 @@ namespace Discord.WebSocket | |||||
users[models[i].Id] = SocketGroupUser.Create(this, state, models[i]); | users[models[i].Id] = SocketGroupUser.Create(this, state, models[i]); | ||||
_users = users; | _users = users; | ||||
} | } | ||||
public Task LeaveAsync(RequestOptions options = null) | public Task LeaveAsync(RequestOptions options = null) | ||||
=> ChannelHelper.DeleteAsync(this, Discord, options); | => ChannelHelper.DeleteAsync(this, Discord, options); | ||||
@@ -98,11 +98,11 @@ namespace Discord.WebSocket | |||||
public Task<RestUserMessage> SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) | public Task<RestUserMessage> SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) | ||||
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); | => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); | ||||
#if FILESYSTEM | #if FILESYSTEM | ||||
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) | |||||
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); | |||||
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) | |||||
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options); | |||||
#endif | #endif | ||||
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) | |||||
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); | |||||
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) | |||||
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options); | |||||
public Task TriggerTypingAsync(RequestOptions options = null) | public Task TriggerTypingAsync(RequestOptions options = null) | ||||
=> ChannelHelper.TriggerTypingAsync(this, Discord, options); | => ChannelHelper.TriggerTypingAsync(this, Discord, options); | ||||
@@ -195,11 +195,11 @@ namespace Discord.WebSocket | |||||
async Task<IReadOnlyCollection<IMessage>> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) | async Task<IReadOnlyCollection<IMessage>> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) | ||||
=> await GetPinnedMessagesAsync(options).ConfigureAwait(false); | => await GetPinnedMessagesAsync(options).ConfigureAwait(false); | ||||
#if FILESYSTEM | #if FILESYSTEM | ||||
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) | |||||
=> await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); | |||||
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options) | |||||
=> await SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false); | |||||
#endif | #endif | ||||
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) | |||||
=> await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); | |||||
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) | |||||
=> await SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false); | |||||
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) | async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) | ||||
=> await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); | => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); | ||||
IDisposable IMessageChannel.EnterTypingState(RequestOptions options) | IDisposable IMessageChannel.EnterTypingState(RequestOptions options) | ||||
@@ -1,4 +1,4 @@ | |||||
using Discord.Rest; | |||||
using Discord.Rest; | |||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
@@ -16,7 +16,7 @@ namespace Discord.WebSocket | |||||
private readonly MessageCache _messages; | private readonly MessageCache _messages; | ||||
public string Topic { get; private set; } | public string Topic { get; private set; } | ||||
private bool _nsfw; | private bool _nsfw; | ||||
public bool IsNsfw => _nsfw || ChannelHelper.IsNsfw(this); | public bool IsNsfw => _nsfw || ChannelHelper.IsNsfw(this); | ||||
@@ -24,9 +24,9 @@ namespace Discord.WebSocket | |||||
public IReadOnlyCollection<SocketMessage> CachedMessages => _messages?.Messages ?? ImmutableArray.Create<SocketMessage>(); | public IReadOnlyCollection<SocketMessage> CachedMessages => _messages?.Messages ?? ImmutableArray.Create<SocketMessage>(); | ||||
public override IReadOnlyCollection<SocketGuildUser> Users | public override IReadOnlyCollection<SocketGuildUser> Users | ||||
=> Guild.Users.Where(x => Permissions.GetValue( | => Guild.Users.Where(x => Permissions.GetValue( | ||||
Permissions.ResolveChannel(Guild, x, this, Permissions.ResolveGuild(Guild, x)), | |||||
Permissions.ResolveChannel(Guild, x, this, Permissions.ResolveGuild(Guild, x)), | |||||
ChannelPermission.ViewChannel)).ToImmutableArray(); | ChannelPermission.ViewChannel)).ToImmutableArray(); | ||||
internal SocketTextChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) | internal SocketTextChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) | ||||
: base(discord, id, guild) | : base(discord, id, guild) | ||||
{ | { | ||||
@@ -78,11 +78,11 @@ namespace Discord.WebSocket | |||||
public Task<RestUserMessage> SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) | public Task<RestUserMessage> SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) | ||||
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); | => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); | ||||
#if FILESYSTEM | #if FILESYSTEM | ||||
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) | |||||
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); | |||||
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) | |||||
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options); | |||||
#endif | #endif | ||||
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) | |||||
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); | |||||
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) | |||||
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options); | |||||
public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) | public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) | ||||
=> ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); | => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); | ||||
@@ -155,14 +155,14 @@ namespace Discord.WebSocket | |||||
async Task<IReadOnlyCollection<IMessage>> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) | async Task<IReadOnlyCollection<IMessage>> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) | ||||
=> await GetPinnedMessagesAsync(options).ConfigureAwait(false); | => await GetPinnedMessagesAsync(options).ConfigureAwait(false); | ||||
#if FILESYSTEM | #if FILESYSTEM | ||||
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) | |||||
=> await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); | |||||
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options) | |||||
=> await SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false); | |||||
#endif | #endif | ||||
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) | |||||
=> await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); | |||||
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) | |||||
=> await SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false); | |||||
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) | async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) | ||||
=> await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); | => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); | ||||
IDisposable IMessageChannel.EnterTypingState(RequestOptions options) | IDisposable IMessageChannel.EnterTypingState(RequestOptions options) | ||||
=> EnterTypingState(options); | => EnterTypingState(options); | ||||
} | } | ||||
} | |||||
} |
@@ -1,4 +1,3 @@ | |||||
#pragma warning disable CS0618 | |||||
using Discord.Audio; | using Discord.Audio; | ||||
using Discord.Rest; | using Discord.Rest; | ||||
using System; | using System; | ||||
@@ -64,7 +63,7 @@ namespace Discord.WebSocket | |||||
public Task DownloaderPromise => _downloaderPromise.Task; | public Task DownloaderPromise => _downloaderPromise.Task; | ||||
public IAudioClient AudioClient => _audioClient; | public IAudioClient AudioClient => _audioClient; | ||||
public SocketTextChannel DefaultChannel => TextChannels | public SocketTextChannel DefaultChannel => TextChannels | ||||
.Where(c => CurrentUser.GetPermissions(c).ReadMessages) | |||||
.Where(c => CurrentUser.GetPermissions(c).ViewChannel) | |||||
.OrderBy(c => c.Position) | .OrderBy(c => c.Position) | ||||
.FirstOrDefault(); | .FirstOrDefault(); | ||||
public SocketVoiceChannel AFKChannel | public SocketVoiceChannel AFKChannel | ||||
@@ -192,12 +191,9 @@ namespace Discord.WebSocket | |||||
_syncPromise = new TaskCompletionSource<bool>(); | _syncPromise = new TaskCompletionSource<bool>(); | ||||
_downloaderPromise = new TaskCompletionSource<bool>(); | _downloaderPromise = new TaskCompletionSource<bool>(); | ||||
if (Discord.ApiClient.AuthTokenType != TokenType.User) | |||||
{ | |||||
var _ = _syncPromise.TrySetResultAsync(true); | |||||
/*if (!model.Large) | |||||
_ = _downloaderPromise.TrySetResultAsync(true);*/ | |||||
} | |||||
var _ = _syncPromise.TrySetResultAsync(true); | |||||
/*if (!model.Large) | |||||
_ = _downloaderPromise.TrySetResultAsync(true);*/ | |||||
} | } | ||||
internal void Update(ClientState state, Model model) | internal void Update(ClientState state, Model model) | ||||
{ | { | ||||
@@ -696,7 +692,6 @@ namespace Discord.WebSocket | |||||
=> Task.FromResult<IGuildUser>(CurrentUser); | => Task.FromResult<IGuildUser>(CurrentUser); | ||||
Task<IGuildUser> IGuild.GetOwnerAsync(CacheMode mode, RequestOptions options) | Task<IGuildUser> IGuild.GetOwnerAsync(CacheMode mode, RequestOptions options) | ||||
=> Task.FromResult<IGuildUser>(Owner); | => Task.FromResult<IGuildUser>(Owner); | ||||
Task IGuild.DownloadUsersAsync() { throw new NotSupportedException(); } | |||||
async Task<IWebhook> IGuild.GetWebhookAsync(ulong id, RequestOptions options) | async Task<IWebhook> IGuild.GetWebhookAsync(ulong id, RequestOptions options) | ||||
=> await GetWebhookAsync(id, options); | => await GetWebhookAsync(id, options); | ||||
@@ -1,4 +1,4 @@ | |||||
using Discord.Rest; | |||||
using Discord.Rest; | |||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
@@ -1,4 +1,4 @@ | |||||
using Discord.Rest; | |||||
using Discord.Rest; | |||||
using System; | using System; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
using Model = Discord.API.User; | using Model = Discord.API.User; | ||||
@@ -37,23 +37,23 @@ namespace Discord.WebSocket | |||||
{ | { | ||||
var newVal = ushort.Parse(model.Discriminator.Value); | var newVal = ushort.Parse(model.Discriminator.Value); | ||||
if (newVal != DiscriminatorValue) | if (newVal != DiscriminatorValue) | ||||
{ | |||||
{ | |||||
DiscriminatorValue = ushort.Parse(model.Discriminator.Value); | DiscriminatorValue = ushort.Parse(model.Discriminator.Value); | ||||
hasChanges = true; | hasChanges = true; | ||||
} | } | ||||
} | } | ||||
if (model.Bot.IsSpecified && model.Bot.Value != IsBot) | if (model.Bot.IsSpecified && model.Bot.Value != IsBot) | ||||
{ | |||||
{ | |||||
IsBot = model.Bot.Value; | IsBot = model.Bot.Value; | ||||
hasChanges = true; | hasChanges = true; | ||||
} | } | ||||
if (model.Username.IsSpecified && model.Username.Value != Username) | if (model.Username.IsSpecified && model.Username.Value != Username) | ||||
{ | |||||
{ | |||||
Username = model.Username.Value; | Username = model.Username.Value; | ||||
hasChanges = true; | hasChanges = true; | ||||
} | } | ||||
return hasChanges; | return hasChanges; | ||||
} | |||||
} | |||||
public async Task<IDMChannel> GetOrCreateDMChannelAsync(RequestOptions options = null) | public async Task<IDMChannel> GetOrCreateDMChannelAsync(RequestOptions options = null) | ||||
=> GlobalUser.DMChannel ?? await UserHelper.CreateDMChannelAsync(this, Discord, options) as IDMChannel; | => GlobalUser.DMChannel ?? await UserHelper.CreateDMChannelAsync(this, Discord, options) as IDMChannel; | ||||
@@ -61,6 +61,9 @@ namespace Discord.WebSocket | |||||
public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) | public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) | ||||
=> CDN.GetUserAvatarUrl(Id, AvatarId, size, format); | => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); | ||||
public string GetDefaultAvatarUrl() | |||||
=> CDN.GetDefaultUserAvatarUrl(DiscriminatorValue); | |||||
public override string ToString() => $"{Username}#{Discriminator}"; | public override string ToString() => $"{Username}#{Discriminator}"; | ||||
private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; | private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; | ||||
internal SocketUser Clone() => MemberwiseClone() as SocketUser; | internal SocketUser Clone() => MemberwiseClone() as SocketUser; | ||||
@@ -1,9 +1,30 @@ | |||||
namespace Discord.WebSocket | |||||
namespace Discord.WebSocket | |||||
{ | { | ||||
internal static class EntityExtensions | internal static class EntityExtensions | ||||
{ | { | ||||
public static IActivity ToEntity(this API.Game model) | public static IActivity ToEntity(this API.Game model) | ||||
{ | { | ||||
// 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(), | |||||
SyncId = model.SyncId.Value, | |||||
AlbumTitle = albumText, | |||||
TrackTitle = model.Details.GetValueOrDefault(), | |||||
Artists = model.State.GetValueOrDefault()?.Split(';'), | |||||
Duration = timestamps?.End - timestamps?.Start, | |||||
AlbumArt = albumArtId != null ? CDN.GetSpotifyAlbumArtUrl(albumArtId) : null, | |||||
Type = ActivityType.Listening | |||||
}; | |||||
} | |||||
// Rich Game | // Rich Game | ||||
if (model.ApplicationId.IsSpecified) | if (model.ApplicationId.IsSpecified) | ||||
{ | { | ||||
@@ -27,15 +48,14 @@ | |||||
{ | { | ||||
return new StreamingGame( | return new StreamingGame( | ||||
model.Name, | model.Name, | ||||
model.StreamUrl.Value, | |||||
model.StreamType.Value.GetValueOrDefault()); | |||||
model.StreamUrl.Value); | |||||
} | } | ||||
// Normal Game | // Normal Game | ||||
return new Game(model.Name); | |||||
return new Game(model.Name, model.Type.GetValueOrDefault() ?? ActivityType.Playing); | |||||
} | } | ||||
// (Small, Large) | // (Small, Large) | ||||
public static GameAsset[] ToEntity(this API.GameAssets model, ulong appId) | |||||
public static GameAsset[] ToEntity(this API.GameAssets model, ulong? appId = null) | |||||
{ | { | ||||
return new GameAsset[] | return new GameAsset[] | ||||
{ | { | ||||
@@ -57,7 +77,7 @@ | |||||
public static GameParty ToEntity(this API.GameParty model) | public static GameParty ToEntity(this API.GameParty model) | ||||
{ | { | ||||
// Discord will probably send bad data since they don't validate anything | // Discord will probably send bad data since they don't validate anything | ||||
int current = 0, cap = 0; | |||||
long current = 0, cap = 0; | |||||
if (model.Size?.Length == 2) | if (model.Size?.Length == 2) | ||||
{ | { | ||||
current = model.Size[0]; | current = model.Size[0]; | ||||
@@ -49,7 +49,7 @@ namespace Discord.Webhook | |||||
if (username != null) | if (username != null) | ||||
args.Username = username; | args.Username = username; | ||||
if (avatarUrl != null) | if (avatarUrl != null) | ||||
args.AvatarUrl = username; | |||||
args.AvatarUrl = avatarUrl; | |||||
if (embeds != null) | if (embeds != null) | ||||
args.Embeds = embeds.Select(x => x.ToModel()).ToArray(); | args.Embeds = embeds.Select(x => x.ToModel()).ToArray(); | ||||
var msg = await client.ApiClient.UploadWebhookFileAsync(client.Webhook.Id, args, options).ConfigureAwait(false); | var msg = await client.ApiClient.UploadWebhookFileAsync(client.Webhook.Id, args, options).ConfigureAwait(false); | ||||