Browse Source

Merge remote-tracking branch 'upstream/dev' into dev

pull/1026/head
Acid Chicken (硫酸鶏) 7 years ago
parent
commit
f5bb99c77d
100 changed files with 954 additions and 530 deletions
  1. +16
    -1
      Discord.Net.sln
  2. +1
    -1
      Discord.Net.targets
  3. +1
    -0
      appveyor.yml
  4. +16
    -0
      docs/README.md
  5. +2
    -2
      docs/docfx.json
  6. +2
    -2
      docs/guides/commands/samples/module.cs
  7. +45
    -23
      docs/guides/getting_started/samples/intro/structure.cs
  8. +15
    -0
      src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj
  9. +70
    -0
      src/Discord.Net.Analyzers/GuildAccessAnalyzer.cs
  10. +21
    -0
      src/Discord.Net.Analyzers/SymbolExtensions.cs
  11. +30
    -0
      src/Discord.Net.Analyzers/docs/DNET0001.md
  12. +1
    -1
      src/Discord.Net.Commands/Attributes/AliasAttribute.cs
  13. +2
    -2
      src/Discord.Net.Commands/Attributes/CommandAttribute.cs
  14. +2
    -2
      src/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs
  15. +1
    -1
      src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs
  16. +2
    -2
      src/Discord.Net.Commands/Attributes/GroupAttribute.cs
  17. +1
    -1
      src/Discord.Net.Commands/Attributes/NameAttribute.cs
  18. +2
    -2
      src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs
  19. +1
    -2
      src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs
  20. +2
    -2
      src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs
  21. +2
    -2
      src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs
  22. +1
    -7
      src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs
  23. +1
    -1
      src/Discord.Net.Commands/Attributes/PriorityAttribute.cs
  24. +2
    -2
      src/Discord.Net.Commands/Attributes/RemainderAttribute.cs
  25. +2
    -2
      src/Discord.Net.Commands/Attributes/RemarksAttribute.cs
  26. +2
    -2
      src/Discord.Net.Commands/Attributes/SummaryAttribute.cs
  27. +15
    -5
      src/Discord.Net.Commands/Builders/ModuleBuilder.cs
  28. +24
    -29
      src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs
  29. +75
    -18
      src/Discord.Net.Commands/CommandService.cs
  30. +9
    -1
      src/Discord.Net.Commands/CommandServiceConfig.cs
  31. +5
    -1
      src/Discord.Net.Commands/IModuleBase.cs
  32. +3
    -3
      src/Discord.Net.Commands/Info/CommandInfo.cs
  33. +11
    -5
      src/Discord.Net.Commands/Info/ModuleInfo.cs
  34. +14
    -4
      src/Discord.Net.Commands/ModuleBase.cs
  35. +1
    -1
      src/Discord.Net.Commands/Utilities/ReflectionUtils.cs
  36. +18
    -6
      src/Discord.Net.Core/CDN.cs
  37. +10
    -0
      src/Discord.Net.Core/Entities/Activities/ActivityType.cs
  38. +4
    -2
      src/Discord.Net.Core/Entities/Activities/Game.cs
  39. +4
    -4
      src/Discord.Net.Core/Entities/Activities/GameAsset.cs
  40. +4
    -4
      src/Discord.Net.Core/Entities/Activities/GameParty.cs
  41. +2
    -7
      src/Discord.Net.Core/Entities/Activities/IActivity.cs
  42. +4
    -4
      src/Discord.Net.Core/Entities/Activities/RichGame.cs
  43. +23
    -0
      src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs
  44. +3
    -4
      src/Discord.Net.Core/Entities/Activities/StreamingGame.cs
  45. +3
    -3
      src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs
  46. +68
    -70
      src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs
  47. +4
    -2
      src/Discord.Net.Core/Entities/Messages/EmbedType.cs
  48. +5
    -2
      src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs
  49. +2
    -0
      src/Discord.Net.Core/Entities/Users/IUser.cs
  50. +0
    -8
      src/Discord.Net.Core/Entities/Users/StreamType.cs
  51. +2
    -41
      src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs
  52. +0
    -0
      src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs
  53. +15
    -10
      src/Discord.Net.Core/Extensions/UserExtensions.cs
  54. +2
    -0
      src/Discord.Net.Core/IDiscordClient.cs
  55. +4
    -2
      src/Discord.Net.Core/Net/HttpException.cs
  56. +10
    -0
      src/Discord.Net.Core/Net/IRequest.cs
  57. +5
    -2
      src/Discord.Net.Core/Net/RateLimitedException.cs
  58. +2
    -2
      src/Discord.Net.Core/TokenType.cs
  59. +45
    -0
      src/Discord.Net.Core/Utils/Comparers.cs
  60. +9
    -8
      src/Discord.Net.Core/Utils/Permissions.cs
  61. +1
    -1
      src/Discord.Net.Providers.WS4Net/Discord.Net.Providers.WS4Net.csproj
  62. +2
    -2
      src/Discord.Net.Providers.WS4Net/WS4NetClient.cs
  63. +6
    -2
      src/Discord.Net.Rest/API/Common/Game.cs
  64. +4
    -4
      src/Discord.Net.Rest/API/Common/GameAssets.cs
  65. +3
    -3
      src/Discord.Net.Rest/API/Common/GameParty.cs
  66. +23
    -4
      src/Discord.Net.Rest/API/Rest/UploadFileParams.cs
  67. +17
    -2
      src/Discord.Net.Rest/AssemblyInfo.cs
  68. +5
    -1
      src/Discord.Net.Rest/BaseDiscordClient.cs
  69. +7
    -1
      src/Discord.Net.Rest/ClientHelper.cs
  70. +23
    -19
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  71. +1
    -1
      src/Discord.Net.Rest/DiscordRestClient.cs
  72. +5
    -5
      src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs
  73. +3
    -3
      src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs
  74. +10
    -10
      src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs
  75. +10
    -10
      src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs
  76. +12
    -12
      src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs
  77. +10
    -10
      src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs
  78. +4
    -1
      src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs
  79. +1
    -1
      src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs
  80. +4
    -1
      src/Discord.Net.Rest/Entities/Users/RestUser.cs
  81. +10
    -2
      src/Discord.Net.Rest/Net/Converters/OptionalConverter.cs
  82. +11
    -6
      src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs
  83. +2
    -1
      src/Discord.Net.Rest/Net/DefaultRestClient.cs
  84. +7
    -7
      src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs
  85. +2
    -2
      src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs
  86. +2
    -2
      src/Discord.Net.Rest/Net/Queue/Requests/WebSocketRequest.cs
  87. +2
    -2
      src/Discord.Net.WebSocket/BaseSocketClient.cs
  88. +7
    -7
      src/Discord.Net.WebSocket/DiscordShardedClient.cs
  89. +2
    -13
      src/Discord.Net.WebSocket/DiscordSocketApiClient.cs
  90. +11
    -22
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  91. +3
    -3
      src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs
  92. +23
    -7
      src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs
  93. +10
    -10
      src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs
  94. +10
    -10
      src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs
  95. +13
    -13
      src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs
  96. +4
    -9
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
  97. +1
    -1
      src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs
  98. +8
    -5
      src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs
  99. +26
    -6
      src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs
  100. +1
    -1
      src/Discord.Net.Webhook/WebhookClientHelper.cs

+ 16
- 1
Discord.Net.sln View File

@@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27004.2009
VisualStudioVersion = 15.0.27130.0
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}"
EndProject
@@ -22,6 +22,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Tests", "test\D
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Webhook", "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj", "{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}"
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
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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|x86.ActiveCfg = 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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -126,6 +140,7 @@ Global
{688FD1D8-7F01-4539-B2E9-F473C5D699C7} = {288C363D-A636-4EAE-9AC1-4698B641B26E}
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012}
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A}
{BBA8E7FB-C834-40DC-822F-B112CB7F0140} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D2404771-EEC8-45F2-9D71-F3373F6C1495}


+ 1
- 1
Discord.Net.targets View File

@@ -1,7 +1,7 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VersionPrefix>2.0.0</VersionPrefix>
<VersionSuffix>beta</VersionSuffix>
<VersionSuffix>beta2</VersionSuffix>
<Authors>RogueException</Authors>
<PackageTags>discord;discordapp</PackageTags>
<PackageProjectUrl>https://github.com/RogueException/Discord.Net</PackageProjectUrl>


+ 1
- 0
appveyor.yml View File

@@ -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.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.Analyzers\Discord.Net.Analyzers.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG"
- ps: >-
if ($Env:APPVEYOR_REPO_TAG -eq "true") {
nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix=""


+ 16
- 0
docs/README.md View File

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

+ 2
- 2
docs/docfx.json View File

@@ -67,8 +67,8 @@
"default"
],
"globalMetadata": {
"_appFooter": "Discord.Net (c) 2015-2017"
"_appFooter": "Discord.Net (c) 2015-2018 2.0.0-beta"
},
"noLangKeyword": false
}
}
}

+ 2
- 2
docs/guides/commands/samples/module.cs View File

@@ -3,7 +3,7 @@ public class Info : ModuleBase<SocketCommandContext>
{
// ~say hello -> hello
[Command("say")]
[Summary("Echos a message.")]
[Summary("Echoes a message.")]
public async Task SayAsync([Remainder] [Summary("The text to echo")] string echo)
{
// ReplyAsync is a method on ModuleBase
@@ -38,4 +38,4 @@ public class Sample : ModuleBase<SocketCommandContext>
var userInfo = user ?? Context.Client.CurrentUser;
await ReplyAsync($"{userInfo.Username}#{userInfo.Discriminator}");
}
}
}

+ 45
- 23
docs/guides/getting_started/samples/intro/structure.cs View File

@@ -19,10 +19,10 @@ class Program

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.
private readonly IServiceCollection _map = new ServiceCollection();
private readonly CommandService _commands = new CommandService();
private readonly CommandService _commands;
private readonly IServiceProvider _services;

private Program()
{
@@ -41,14 +41,45 @@ class Program
// add the `using` at the top, and uncomment this line:
//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.
_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
// that ask for a Func<LogMessage, Task>.
private static Task Logger(LogMessage message)
private static Task Log(LogMessage message)
{
switch (message.Severity)
{
@@ -92,24 +123,15 @@ class Program
await Task.Delay(Timeout.Infinite);
}

private IServiceProvider _services;
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.
// 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:
await _commands.AddModuleAsync<SomeModule>();
await _commands.AddModuleAsync<SomeModule>(_services);
// 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.
@@ -123,8 +145,6 @@ class Program
if (msg == null) return;

// 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;
// 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,
// 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
// 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)
// await msg.Channel.SendMessageAsync(result.ErrorReason);
}


+ 15
- 0
src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj View File

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

+ 70
- 0
src/Discord.Net.Analyzers/GuildAccessAnalyzer.cs View File

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

+ 21
- 0
src/Discord.Net.Analyzers/SymbolExtensions.cs View File

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

+ 30
- 0
src/Discord.Net.Analyzers/docs/DNET0001.md View File

@@ -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.

+ 1
- 1
src/Discord.Net.Commands/Attributes/AliasAttribute.cs View File

@@ -3,7 +3,7 @@ using System;
namespace Discord.Commands
{
/// <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
{
/// <summary> The aliases which have been defined for the command. </summary>


+ 2
- 2
src/Discord.Net.Commands/Attributes/CommandAttribute.cs View File

@@ -1,8 +1,8 @@
using System;
using System;

namespace Discord.Commands
{
[AttributeUsage(AttributeTargets.Method)]
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class CommandAttribute : Attribute
{
public string Text { get; }


+ 2
- 2
src/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs View File

@@ -1,8 +1,8 @@
using System;
using System;

namespace Discord.Commands
{
[AttributeUsage(AttributeTargets.Class)]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class DontAutoLoadAttribute : Attribute
{
}


+ 1
- 1
src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs View File

@@ -2,7 +2,7 @@ using System;

namespace Discord.Commands {

[AttributeUsage(AttributeTargets.Property)]
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class DontInjectAttribute : Attribute {
}



+ 2
- 2
src/Discord.Net.Commands/Attributes/GroupAttribute.cs View File

@@ -1,8 +1,8 @@
using System;
using System;

namespace Discord.Commands
{
[AttributeUsage(AttributeTargets.Class)]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class GroupAttribute : Attribute
{
public string Prefix { get; }


+ 1
- 1
src/Discord.Net.Commands/Attributes/NameAttribute.cs View File

@@ -3,7 +3,7 @@ using System;
namespace Discord.Commands
{
// 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 string Text { get; }


+ 2
- 2
src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs View File

@@ -4,7 +4,7 @@ using System.Reflection;

namespace Discord.Commands
{
[AttributeUsage(AttributeTargets.Parameter)]
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public class OverrideTypeReaderAttribute : Attribute
{
private static readonly TypeInfo _typeReaderTypeInfo = typeof(TypeReader).GetTypeInfo();
@@ -19,4 +19,4 @@ namespace Discord.Commands
TypeReader = overridenTypeReader;
}
}
}
}

+ 1
- 2
src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs View File

@@ -1,6 +1,5 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;

namespace Discord.Commands
{
@@ -9,4 +8,4 @@ namespace Discord.Commands
{
public abstract Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, ParameterInfo parameter, object value, IServiceProvider services);
}
}
}

+ 2
- 2
src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;

@@ -15,7 +15,7 @@ namespace Discord.Commands
/// <summary>
/// Require that the command be invoked in a specified context.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class RequireContextAttribute : PreconditionAttribute
{
public ContextType Contexts { get; }


+ 2
- 2
src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Threading.Tasks;

namespace Discord.Commands
@@ -6,7 +6,7 @@ namespace Discord.Commands
/// <summary>
/// Require that the command is invoked in a channel marked NSFW
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class RequireNsfwAttribute : PreconditionAttribute
{
public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)


+ 1
- 7
src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs View File

@@ -1,7 +1,5 @@
#pragma warning disable CS0618
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;

namespace Discord.Commands
{
@@ -9,7 +7,7 @@ namespace Discord.Commands
/// Require that the command is invoked by the owner of the bot.
/// </summary>
/// <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 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)
return PreconditionResult.FromError("Command can only be run by the owner of the bot");
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:
return PreconditionResult.FromError($"{nameof(RequireOwnerAttribute)} is not supported by this {nameof(TokenType)}.");
}


+ 1
- 1
src/Discord.Net.Commands/Attributes/PriorityAttribute.cs View File

@@ -3,7 +3,7 @@ using System;
namespace Discord.Commands
{
/// <summary> Sets priority of commands </summary>
[AttributeUsage(AttributeTargets.Method)]
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class PriorityAttribute : Attribute
{
/// <summary> The priority which has been set for the command </summary>


+ 2
- 2
src/Discord.Net.Commands/Attributes/RemainderAttribute.cs View File

@@ -1,8 +1,8 @@
using System;
using System;

namespace Discord.Commands
{
[AttributeUsage(AttributeTargets.Parameter)]
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public class RemainderAttribute : Attribute
{
}


+ 2
- 2
src/Discord.Net.Commands/Attributes/RemarksAttribute.cs View File

@@ -1,9 +1,9 @@
using System;
using System;

namespace Discord.Commands
{
// 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 string Text { get; }


+ 2
- 2
src/Discord.Net.Commands/Attributes/SummaryAttribute.cs View File

@@ -1,9 +1,9 @@
using System;
using System;

namespace Discord.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 string Text { get; }


+ 15
- 5
src/Discord.Net.Commands/Builders/ModuleBuilder.cs View File

@@ -1,5 +1,6 @@
using System;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;

@@ -18,6 +19,7 @@ namespace Discord.Commands.Builders
public string Name { get; set; }
public string Summary { get; set; }
public string Remarks { get; set; }
public string Group { get; set; }

public IReadOnlyList<CommandBuilder> Commands => _commands;
public IReadOnlyList<ModuleBuilder> Modules => _submodules;
@@ -25,6 +27,8 @@ namespace Discord.Commands.Builders
public IReadOnlyList<Attribute> Attributes => _attributes;
public IReadOnlyList<string> Aliases => _aliases;

internal TypeInfo TypeInfo { get; set; }

//Automatic
internal ModuleBuilder(CommandService service, ModuleBuilder parent)
{
@@ -111,17 +115,23 @@ namespace Discord.Commands.Builders
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
if (Name == null)
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);
}
}

+ 24
- 29
src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs View File

@@ -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())
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>();

@@ -63,11 +62,11 @@ namespace Discord.Commands

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);

result[typeInfo.AsType()] = module.Build(service);
result[typeInfo.AsType()] = module.Build(service, services);
}

await service._cmdLogger.DebugAsync($"Successfully built {builtTypes.Count} modules.").ConfigureAwait(false);
@@ -75,7 +74,7 @@ namespace Discord.Commands
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)
{
@@ -87,17 +86,18 @@ namespace Discord.Commands
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);
}
}

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();
builder.TypeInfo = typeInfo;

foreach (var attribute in attributes)
{
@@ -117,6 +117,7 @@ namespace Discord.Commands
break;
case GroupAttribute group:
builder.Name = builder.Name ?? group.Prefix;
builder.Group = group.Prefix;
builder.AddAliases(group.Prefix);
break;
case PreconditionAttribute precondition:
@@ -140,12 +141,12 @@ namespace Discord.Commands
{
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();
@@ -191,7 +192,7 @@ namespace Discord.Commands
{
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;
}

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 paramType = paramInfo.ParameterType;
@@ -245,7 +246,7 @@ namespace Discord.Commands
builder.Summary = summary.Text;
break;
case OverrideTypeReaderAttribute typeReader:
builder.TypeReader = GetTypeReader(service, paramType, typeReader.TypeReader);
builder.TypeReader = GetTypeReader(service, paramType, typeReader.TypeReader, services);
break;
case ParamArrayAttribute _:
builder.IsMultiple = true;
@@ -273,19 +274,12 @@ namespace Discord.Commands

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);
TypeReader reader = null;
@@ -296,8 +290,8 @@ namespace Discord.Commands
}

//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;
}
@@ -305,7 +299,8 @@ namespace Discord.Commands
private static bool IsValidModuleDefinition(TypeInfo typeInfo)
{
return _moduleTypeInfo.IsAssignableFrom(typeInfo) &&
!typeInfo.IsAbstract;
!typeInfo.IsAbstract &&
!typeInfo.ContainsGenericParameters;
}

private static bool IsValidCommandDefinition(MethodInfo methodInfo)


+ 75
- 18
src/Discord.Net.Commands/CommandService.cs View File

@@ -1,5 +1,3 @@
using Discord.Commands.Builders;
using Discord.Logging;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -8,6 +6,9 @@ using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Discord.Commands.Builders;
using Discord.Logging;

namespace Discord.Commands
{
@@ -85,7 +86,8 @@ namespace Discord.Commands
var builder = new ModuleBuilder(this, null, primaryAlias);
buildFunc(builder);

var module = builder.Build(this);
var module = builder.Build(this, null);

return LoadModuleInternal(module);
}
finally
@@ -93,9 +95,18 @@ namespace Discord.Commands
_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);
try
{
@@ -104,7 +115,7 @@ namespace Discord.Commands
if (_typedModuleDefs.ContainsKey(type))
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))
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();
}
}
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);
try
{
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)
{
@@ -196,10 +215,11 @@ namespace Discord.Commands
return true;
}

//Type Readers
//Type Readers
/// <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.
/// 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>
/// <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>
@@ -207,24 +227,61 @@ namespace Discord.Commands
=> AddTypeReader(typeof(T), reader);
/// <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.
/// 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>
/// <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>
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)
{
var readers = _typeReaders.GetOrAdd(typeof(Nullable<>).MakeGenericType(valueType), x => new ConcurrentDictionary<Type, TypeReader>());
var nullableReader = NullableTypeReader.Create(valueType, valueTypeReader);
readers[nullableReader.GetType()] = nullableReader;
}
}
internal IDictionary<Type, TypeReader> GetTypeReaders(Type type)
{
if (_typeReaders.TryGetValue(type, out var definedTypeReaders))
@@ -272,9 +329,9 @@ namespace Discord.Commands
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);
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;

@@ -330,7 +387,7 @@ namespace Discord.Commands
float CalculateScore(CommandMatch match, ParseResult parseResult)
{
float argValuesScore = 0, paramValuesScore = 0;
if (match.Command.Parameters.Count > 0)
{
var argValuesSum = parseResult.ArgValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0;


+ 9
- 1
src/Discord.Net.Commands/CommandServiceConfig.cs View File

@@ -1,4 +1,6 @@
namespace Discord.Commands
using System;

namespace Discord.Commands
{
public class CommandServiceConfig
{
@@ -18,5 +20,11 @@

/// <summary> Determines whether extra parameters should be ignored. </summary>
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;
}
}

+ 5
- 1
src/Discord.Net.Commands/IModuleBase.cs View File

@@ -1,4 +1,6 @@
namespace Discord.Commands
using Discord.Commands.Builders;

namespace Discord.Commands
{
internal interface IModuleBase
{
@@ -7,5 +9,7 @@
void BeforeExecute(CommandInfo command);
void AfterExecute(CommandInfo command);

void OnModuleBuilding(CommandService commandService, ModuleBuilder builder);
}
}

+ 3
- 3
src/Discord.Net.Commands/Info/CommandInfo.cs View File

@@ -165,11 +165,11 @@ namespace Discord.Commands
switch (RunMode)
{
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
var t2 = Task.Run(async () =>
{
await ExecuteAsyncInternalAsync(context, args, services).ConfigureAwait(false);
await ExecuteInternalAsync(context, args, services).ConfigureAwait(false);
});
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);
try


+ 11
- 5
src/Discord.Net.Commands/Info/ModuleInfo.cs View File

@@ -2,7 +2,7 @@ using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Reflection;
using Discord.Commands.Builders;

namespace Discord.Commands
@@ -13,6 +13,7 @@ namespace Discord.Commands
public string Name { get; }
public string Summary { get; }
public string Remarks { get; }
public string Group { get; }

public IReadOnlyList<string> Aliases { get; }
public IReadOnlyList<CommandInfo> Commands { get; }
@@ -22,21 +23,26 @@ namespace Discord.Commands
public ModuleInfo Parent { get; }
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;

Name = builder.Name;
Summary = builder.Summary;
Remarks = builder.Remarks;
Group = builder.Group;
Parent = parent;

//TypeInfo = builder.TypeInfo;

Aliases = BuildAliases(builder, service).ToImmutableArray();
Commands = builder.Commands.Select(x => x.Build(this, service)).ToImmutableArray();
Preconditions = BuildPreconditions(builder).ToImmutableArray();
Attributes = BuildAttributes(builder).ToImmutableArray();

Submodules = BuildSubmodules(builder, service).ToImmutableArray();
Submodules = BuildSubmodules(builder, service, services).ToImmutableArray();
}

private static IEnumerable<string> BuildAliases(ModuleBuilder builder, CommandService service)
@@ -66,12 +72,12 @@ namespace Discord.Commands
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>();

foreach (var submodule in parent.Modules)
result.Add(submodule.Build(service, this));
result.Add(submodule.Build(service, services, this));

return result;
}


+ 14
- 4
src/Discord.Net.Commands/ModuleBase.cs View File

@@ -1,5 +1,6 @@
using System;
using System;
using System.Threading.Tasks;
using Discord.Commands.Builders;

namespace Discord.Commands
{
@@ -10,7 +11,13 @@ namespace Discord.Commands
{
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);
}
@@ -23,15 +30,18 @@ namespace Discord.Commands
{
}

protected virtual void OnModuleBuilding(CommandService commandService, ModuleBuilder builder)
{
}

//IModuleBase
void IModuleBase.SetContext(ICommandContext context)
{
var newValue = context as T;
Context = newValue ?? throw new InvalidOperationException($"Invalid context type. Expected {typeof(T).Name}, got {context.GetType().Name}");
}

void IModuleBase.BeforeExecute(CommandInfo command) => BeforeExecute(command);

void IModuleBase.AfterExecute(CommandInfo command) => AfterExecute(command);
void IModuleBase.OnModuleBuilding(CommandService commandService, ModuleBuilder builder) => OnModuleBuilding(commandService, builder);
}
}

+ 1
- 1
src/Discord.Net.Commands/Utilities/ReflectionUtils.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;


+ 18
- 6
src/Discord.Net.Core/CDN.cs View File

@@ -1,4 +1,4 @@
using System;
using System;

namespace Discord
{
@@ -13,6 +13,10 @@ namespace Discord
string extension = FormatToExtension(format, avatarId);
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)
=> iconId != null ? $"{DiscordConfig.CDNUrl}icons/{guildId}/{iconId}.jpg" : null;
public static string GetGuildSplashUrl(ulong guildId, string splashId)
@@ -28,17 +32,25 @@ namespace Discord
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)
{
if (format == ImageFormat.Auto)
format = imageId.StartsWith("a_") ? ImageFormat.Gif : ImageFormat.Png;
switch (format)
{
case ImageFormat.Gif: return "gif";
case ImageFormat.Jpeg: return "jpeg";
case ImageFormat.Png: return "png";
case ImageFormat.WebP: return "webp";
default: throw new ArgumentException(nameof(format));
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));
}
}
}


+ 10
- 0
src/Discord.Net.Core/Entities/Activities/ActivityType.cs View File

@@ -0,0 +1,10 @@
namespace Discord
{
public enum ActivityType
{
Playing = 0,
Streaming = 1,
Listening = 2,
Watching = 3
}
}

+ 4
- 2
src/Discord.Net.Core/Entities/Activities/Game.cs View File

@@ -1,4 +1,4 @@
using System.Diagnostics;
using System.Diagnostics;

namespace Discord
{
@@ -6,11 +6,13 @@ namespace Discord
public class Game : IActivity
{
public string Name { get; internal set; }
public ActivityType Type { get; internal set; }

internal Game() { }
public Game(string name)
public Game(string name, ActivityType type = ActivityType.Playing)
{
Name = name;
Type = type;
}
public override string ToString() => Name;


+ 4
- 4
src/Discord.Net.Core/Entities/Activities/GameAsset.cs View File

@@ -1,15 +1,15 @@
namespace Discord
namespace Discord
{
public class GameAsset
{
internal GameAsset() { }

internal ulong ApplicationId { get; set; }
internal ulong? ApplicationId { get; set; }
public string Text { get; internal set; }
public string ImageId { get; internal set; }
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;
}
}
}

+ 4
- 4
src/Discord.Net.Core/Entities/Activities/GameParty.cs View File

@@ -1,11 +1,11 @@
namespace Discord
namespace Discord
{
public class GameParty
{
internal GameParty() { }

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

+ 2
- 7
src/Discord.Net.Core/Entities/Activities/IActivity.cs View File

@@ -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
{
string Name { get; }
ActivityType Type { get; }
}
}

+ 4
- 4
src/Discord.Net.Core/Entities/Activities/RichGame.cs View File

@@ -1,4 +1,4 @@
using System.Diagnostics;
using System.Diagnostics;

namespace Discord
{
@@ -7,8 +7,8 @@ namespace Discord
{
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 GameAsset SmallAsset { get; internal set; }
public GameAsset LargeAsset { get; internal set; }
@@ -19,4 +19,4 @@ namespace Discord
public override string ToString() => Name;
private string DebuggerDisplay => $"{Name} (Rich)";
}
}
}

+ 23
- 0
src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs View File

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

+ 3
- 4
src/Discord.Net.Core/Entities/Activities/StreamingGame.cs View File

@@ -6,15 +6,14 @@ namespace Discord
public class StreamingGame : Game
{
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;
Url = url;
StreamType = streamType;
Type = ActivityType.Streaming;
}
public override string ToString() => Name;
private string DebuggerDisplay => $"{Name} ({Url})";
}

+ 3
- 3
src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
@@ -11,10 +11,10 @@ namespace Discord
Task<IUserMessage> SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null);
#if FILESYSTEM
/// <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
/// <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>
Task<IMessage> GetMessageAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);


src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs → src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs View File

@@ -1,89 +1,105 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;

namespace Discord
{
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 MaxTitleLength = 256;
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()
{
_embed = new Embed(EmbedType.Rich);
Fields = new List<EmbedFieldBuilder>();
}

public string Title
{
get => _embed.Title;
get => _title;
set
{
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
{
get => _embed.Description;
get => _description;
set
{
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
{
get => _embed.Url;
get => _url;
set
{
if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(Url));
_embed.Url = value;
_url = value;
}
}
public string ThumbnailUrl
{
get => _embed.Thumbnail?.Url;
get => _thumbnail?.Url;
set
{
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
{
get => _embed.Image?.Url;
get => _image?.Url;
set
{
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
{
get => _fields;
set
{

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));
_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)
{
Title = title;
@@ -180,7 +196,6 @@ namespace Discord
AddField(field);
return this;
}

public EmbedBuilder AddField(EmbedFieldBuilder field)
{
if (Fields.Count >= MaxFieldCount)
@@ -195,63 +210,54 @@ namespace Discord
{
var field = new EmbedFieldBuilder();
action(field);
this.AddField(field);
AddField(field);
return this;
}

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);
for (int i = 0; i < Fields.Count; i++)
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
{
private string _name;
private string _value;
private EmbedField _field;

public const int MaxFieldNameLength = 256;
public const int MaxFieldValueLength = 1024;

public string Name
{
get => _field.Name;
get => _name;
set
{
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));
_field.Name = value;
_name = value;
}
}

public object Value
{
get => _field.Value;
get => _value;
set
{
var stringValue = value?.ToString();
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));
_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)
{
@@ -270,48 +276,44 @@ namespace Discord
}

public EmbedField Build()
=> _field;
=> new EmbedField(Name, Value.ToString(), IsInline);
}

public class EmbedAuthorBuilder
{
private EmbedAuthor _author;

private string _name;
private string _url;
private string _iconUrl;
public const int MaxAuthorNameLength = 256;

public string Name
{
get => _author.Name;
get => _name;
set
{
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
{
get => _author.Url;
get => _url;
set
{
if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(Url));
_author.Url = value;
_url = value;
}
}
public string IconUrl
{
get => _author.IconUrl;
get => _iconUrl;
set
{
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)
{
Name = name;
@@ -329,39 +331,35 @@ namespace Discord
}

public EmbedAuthor Build()
=> _author;
=> new EmbedAuthor(Name, Url, IconUrl, null);
}

public class EmbedFooterBuilder
{
private EmbedFooter _footer;
private string _text;
private string _iconUrl;

public const int MaxFooterTextLength = 2048;

public string Text
{
get => _footer.Text;
get => _text;
set
{
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
{
get => _footer.IconUrl;
get => _iconUrl;
set
{
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)
{
Text = text;
@@ -374,6 +372,6 @@ namespace Discord
}

public EmbedFooter Build()
=> _footer;
=> new EmbedFooter(Text, IconUrl, null);
}
}

+ 4
- 2
src/Discord.Net.Core/Entities/Messages/EmbedType.cs View File

@@ -1,13 +1,15 @@
namespace Discord
namespace Discord
{
public enum EmbedType
{
Unknown = -1,
Rich,
Link,
Video,
Image,
Gifv,
Article,
Tweet
Tweet,
Html,
}
}

+ 5
- 2
src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics;

@@ -13,6 +13,8 @@ namespace Discord
public static readonly ChannelPermissions Text = new ChannelPermissions(0b01100_0000000_1111111110001_010001);
/// <summary> Gets a ChannelPermissions that grants all permissions for voice channels. </summary>
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>
public static readonly ChannelPermissions DM = new ChannelPermissions(0b00000_1000110_1011100110000_000000);
/// <summary> Gets a ChannelPermissions that grants all permissions for group channels. </summary>
@@ -24,6 +26,7 @@ namespace Discord
{
case ITextChannel _: return Text;
case IVoiceChannel _: return Voice;
case ICategoryChannel _: return Category;
case IDMChannel _: return DM;
case IGroupChannel _: return Group;
default: throw new ArgumentException("Unknown channel type", nameof(channel));
@@ -157,4 +160,4 @@ namespace Discord
public override string ToString() => RawValue.ToString();
private string DebuggerDisplay => $"{string.Join(", ", ToList())}";
}
}
}

+ 2
- 0
src/Discord.Net.Core/Entities/Users/IUser.cs View File

@@ -8,6 +8,8 @@ namespace Discord
string AvatarId { get; }
/// <summary> Gets the url to this user's avatar. </summary>
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>
string Discriminator { get; }
/// <summary> Gets the per-username unique id for this user. </summary>


+ 0
- 8
src/Discord.Net.Core/Entities/Users/StreamType.cs View File

@@ -1,8 +0,0 @@
namespace Discord
{
public enum StreamType
{
NotStreaming = 0,
Twitch = 1
}
}

+ 2
- 41
src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs View File

@@ -1,6 +1,5 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Discord
@@ -20,45 +19,7 @@ namespace Discord

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());
}
}
}

src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs → src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs View File


+ 15
- 10
src/Discord.Net.Core/Extensions/UserExtensions.cs View File

@@ -1,4 +1,4 @@
using System.Threading.Tasks;
using System.Threading.Tasks;
using System.IO;

namespace Discord
@@ -8,10 +8,10 @@ namespace Discord
/// <summary>
/// Sends a message to the user via DM.
/// </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,
Embed embed = null,
Embed embed = null,
RequestOptions options = null)
{
return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false);
@@ -25,24 +25,29 @@ namespace Discord
string filename,
string text = null,
bool isTTS = false,
Embed embed = null,
RequestOptions options = null
)
{
return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false);
return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false);
}

#if FILESYSTEM
/// <summary>
/// Sends a file to the user via DM.
/// </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)
{
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

public static Task BanAsync(this IGuildUser user, int pruneDays = 0, string reason = null, RequestOptions options = null)
=> user.Guild.AddBanAsync(user, pruneDays, reason, options);
}
}

+ 2
- 0
src/Discord.Net.Core/IDiscordClient.cs View File

@@ -36,5 +36,7 @@ namespace Discord
Task<IVoiceRegion> GetVoiceRegionAsync(string id, RequestOptions options = null);

Task<IWebhook> GetWebhookAsync(ulong id, RequestOptions options = null);

Task<int> GetRecommendedShardCountAsync(RequestOptions options = null);
}
}

+ 4
- 2
src/Discord.Net.Core/Net/HttpException.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Net;

namespace Discord.Net
@@ -8,11 +8,13 @@ namespace Discord.Net
public HttpStatusCode HttpCode { get; }
public int? DiscordCode { 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))
{
HttpCode = httpCode;
Request = request;
DiscordCode = discordCode;
Reason = reason;
}


+ 10
- 0
src/Discord.Net.Core/Net/IRequest.cs View File

@@ -0,0 +1,10 @@
using System;

namespace Discord.Net
{
public interface IRequest
{
DateTimeOffset? TimeoutAt { get; }
RequestOptions Options { get; }
}
}

+ 5
- 2
src/Discord.Net.Core/Net/RateLimitedException.cs View File

@@ -1,12 +1,15 @@
using System;
using System;

namespace Discord.Net
{
public class RateLimitedException : TimeoutException
{
public RateLimitedException()
public IRequest Request { get; }

public RateLimitedException(IRequest request)
: base("You are being rate limited.")
{
Request = request;
}
}
}

+ 2
- 2
src/Discord.Net.Core/TokenType.cs View File

@@ -1,10 +1,10 @@
using System;
using System;

namespace Discord
{
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,
Bearer,
Bot,


+ 45
- 0
src/Discord.Net.Core/Utils/Comparers.cs View File

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

+ 9
- 8
src/Discord.Net.Core/Utils/Permissions.cs View File

@@ -80,7 +80,7 @@ namespace Discord
}

[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)]
public static void SetFlag(ref ulong value, ulong flag) => value |= flag;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -133,9 +133,10 @@ namespace Discord
ulong deniedPermissions = 0UL, allowedPermissions = 0UL;
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)
{
allowedPermissions |= perms.Value.AllowValue;
@@ -160,10 +161,10 @@ namespace Discord
else if (!GetValue(resolvedPermissions, ChannelPermission.SendMessages))
{
//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)
@@ -172,4 +173,4 @@ namespace Discord
return resolvedPermissions;
}
}
}
}

+ 1
- 1
src/Discord.Net.Providers.WS4Net/Discord.Net.Providers.WS4Net.csproj View File

@@ -10,6 +10,6 @@
<ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="WebSocket4Net" Version="0.15.0" />
<PackageReference Include="WebSocket4Net" Version="0.15.2" />
</ItemGroup>
</Project>

+ 2
- 2
src/Discord.Net.Providers.WS4Net/WS4NetClient.cs View File

@@ -66,7 +66,7 @@ namespace Discord.Net.Providers.WS4Net
_cancelTokenSource = new CancellationTokenSource();
_cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token;

_client = new WS4NetSocket(host, customHeaderItems: _headers.ToList())
_client = new WS4NetSocket(host, "", customHeaderItems: _headers.ToList())
{
EnableAutoSendPing = false,
NoDelay = true,
@@ -163,4 +163,4 @@ namespace Discord.Net.Providers.WS4Net
Closed(ex).GetAwaiter().GetResult();
}
}
}
}

+ 6
- 2
src/Discord.Net.Rest/API/Common/Game.cs View File

@@ -1,4 +1,4 @@
#pragma warning disable CS1591
#pragma warning disable CS1591
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System.Runtime.Serialization;
@@ -12,7 +12,7 @@ namespace Discord.API
[JsonProperty("url")]
public Optional<string> StreamUrl { get; set; }
[JsonProperty("type")]
public Optional<StreamType?> StreamType { get; set; }
public Optional<ActivityType?> Type { get; set; }
[JsonProperty("details")]
public Optional<string> Details { get; set; }
[JsonProperty("state")]
@@ -29,6 +29,10 @@ namespace Discord.API
public Optional<API.GameTimestamps> Timestamps { get; set; }
[JsonProperty("instance")]
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]
internal void OnError(StreamingContext context, ErrorContext errorContext)


+ 4
- 4
src/Discord.Net.Rest/API/Common/GameAssets.cs View File

@@ -1,4 +1,4 @@
using Newtonsoft.Json;
using Newtonsoft.Json;

namespace Discord.API
{
@@ -8,9 +8,9 @@ namespace Discord.API
public Optional<string> SmallText { get; set; }
[JsonProperty("small_image")]
public Optional<string> SmallImage { get; set; }
[JsonProperty("large_image")]
public Optional<string> LargeText { get; set; }
[JsonProperty("large_text")]
public Optional<string> LargeText { get; set; }
[JsonProperty("large_image")]
public Optional<string> LargeImage { get; set; }
}
}
}

+ 3
- 3
src/Discord.Net.Rest/API/Common/GameParty.cs View File

@@ -1,4 +1,4 @@
using Newtonsoft.Json;
using Newtonsoft.Json;

namespace Discord.API
{
@@ -7,6 +7,6 @@ namespace Discord.API
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("size")]
public int[] Size { get; set; }
public long[] Size { get; set; }
}
}
}

+ 23
- 4
src/Discord.Net.Rest/API/Rest/UploadFileParams.cs View File

@@ -1,18 +1,25 @@
#pragma warning disable CS1591
#pragma warning disable CS1591
using Discord.Net.Converters;
using Discord.Net.Rest;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;

namespace Discord.API.Rest
{
internal class UploadFileParams
{
private static JsonSerializer _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() };

public Stream File { get; }

public Optional<string> Filename { get; set; }
public Optional<string> Content { get; set; }
public Optional<string> Nonce { get; set; }
public Optional<bool> IsTTS { get; set; }
public Optional<Embed> Embed { get; set; }

public UploadFileParams(Stream file)
{
@@ -23,12 +30,24 @@ namespace Discord.API.Rest
{
var d = new Dictionary<string, object>();
d["file"] = new MultipartFile(File, Filename.GetValueOrDefault("unknown.dat"));

var payload = new Dictionary<string, object>();
if (Content.IsSpecified)
d["content"] = Content.Value;
payload["content"] = Content.Value;
if (IsTTS.IsSpecified)
d["tts"] = IsTTS.Value.ToString();
payload["tts"] = IsTTS.Value.ToString();
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;
}
}


+ 17
- 2
src/Discord.Net.Rest/AssemblyInfo.cs View File

@@ -1,7 +1,22 @@
using System.Runtime.CompilerServices;
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Discord.Net.Rpc")]
[assembly: InternalsVisibleTo("Discord.Net.WebSocket")]
[assembly: InternalsVisibleTo("Discord.Net.Webhook")]
[assembly: InternalsVisibleTo("Discord.Net.Commands")]
[assembly: InternalsVisibleTo("Discord.Net.Tests")]
[assembly: InternalsVisibleTo("Discord.Net.Tests")]

[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))]

+ 5
- 1
src/Discord.Net.Rest/BaseDiscordClient.cs View File

@@ -1,4 +1,4 @@
using Discord.Logging;
using Discord.Logging;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -125,6 +125,10 @@ namespace Discord.Rest
/// <inheritdoc />
public void Dispose() => Dispose(true);

/// <inheritdoc />
public Task<int> GetRecommendedShardCountAsync(RequestOptions options = null)
=> ClientHelper.GetRecommendShardCountAsync(this, options);

//IDiscordClient
ConnectionState IDiscordClient.ConnectionState => ConnectionState.Disconnected;
ISelfUser IDiscordClient.CurrentUser => CurrentUser;


+ 7
- 1
src/Discord.Net.Rest/ClientHelper.cs View File

@@ -1,4 +1,4 @@
using Discord.API.Rest;
using Discord.API.Rest;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
@@ -163,5 +163,11 @@ namespace Discord.Rest
var models = await client.ApiClient.GetVoiceRegionsAsync(options).ConfigureAwait(false);
return models.Select(x => RestVoiceRegion.Create(client, x)).FirstOrDefault(x => x.Id == id);
}

public static async Task<int> GetRecommendShardCountAsync(BaseDiscordClient client, RequestOptions options)
{
var response = await client.ApiClient.GetBotGatewayAsync(options).ConfigureAwait(false);
return response.Shards;
}
}
}

+ 23
- 19
src/Discord.Net.Rest/DiscordRestApiClient.cs View File

@@ -1,5 +1,4 @@
#pragma warning disable CS1591
#pragma warning disable CS0618
using Discord.API.Rest;
using Discord.Net;
using Discord.Net.Converters;
@@ -70,12 +69,12 @@ namespace Discord.API
{
switch (tokenType)
{
case default(TokenType):
return token;
case TokenType.Bot:
return $"Bot {token}";
case TokenType.Bearer:
return $"Bearer {token}";
case TokenType.User:
return token;
default:
throw new ArgumentException("Unknown OAuth token type", nameof(tokenType));
}
@@ -113,7 +112,6 @@ namespace Discord.API
{
_loginCancelToken = new CancellationTokenSource();

AuthTokenType = TokenType.User;
AuthToken = null;
await RequestQueue.SetCancelTokenAsync(_loginCancelToken.Token).ConfigureAwait(false);
RestClient.SetCancelToken(_loginCancelToken.Token);
@@ -172,8 +170,7 @@ namespace Discord.API
{
options = options ?? new RequestOptions();
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);
await SendInternalAsync(method, endpoint, request).ConfigureAwait(false);
@@ -187,8 +184,7 @@ namespace Discord.API
{
options = options ?? new RequestOptions();
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;
var request = new JsonRestRequest(RestClient, method, endpoint, json, options);
@@ -203,8 +199,7 @@ namespace Discord.API
{
options = options ?? new RequestOptions();
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);
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
{
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);
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
{
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;
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)
{
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);
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);
}

//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
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)
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));
options = RequestOptions.CreateOrClone(options);

@@ -483,7 +487,7 @@ namespace Discord.API
if (!args.Embeds.IsSpecified || args.Embeds.Value == null || args.Embeds.Value.Length == 0)
Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content));

if (args.Content.Length > DiscordConfig.MaxMessageSize)
if (args.Content?.Length > DiscordConfig.MaxMessageSize)
throw new ArgumentException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content));
options = RequestOptions.CreateOrClone(options);
@@ -564,7 +568,7 @@ namespace Discord.API
{
if (!args.Embed.IsSpecified)
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));
}
options = RequestOptions.CreateOrClone(options);


+ 1
- 1
src/Discord.Net.Rest/DiscordRestClient.cs View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Threading.Tasks;


+ 5
- 5
src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs View File

@@ -1,4 +1,4 @@
using Discord.API.Rest;
using Discord.API.Rest;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -170,17 +170,17 @@ namespace Discord.Rest

#if FILESYSTEM
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);
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
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);
return RestUserMessage.Create(client, channel, client.CurrentUser, model);
}


+ 3
- 3
src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.IO;
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);
#if FILESYSTEM
/// <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
/// <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>
Task<RestMessage> GetMessageAsync(ulong id, RequestOptions options = null);


+ 10
- 10
src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
@@ -37,7 +37,7 @@ namespace Discord.Rest
public override async Task UpdateAsync(RequestOptions options = null)
{
var model = await Discord.ApiClient.GetChannelAsync(Id, options).ConfigureAwait(false);
Update(model);
Update(model);
}
public Task CloseAsync(RequestOptions options = null)
=> 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)
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options);
#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
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)
=> ChannelHelper.TriggerTypingAsync(this, Discord, options);
@@ -122,11 +122,11 @@ namespace Discord.Rest
=> await GetPinnedMessagesAsync(options).ConfigureAwait(false);

#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
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)
=> await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false);
IDisposable IMessageChannel.EnterTypingState(RequestOptions options)


+ 10
- 10
src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs View File

@@ -1,4 +1,4 @@
using Discord.Audio;
using Discord.Audio;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -19,7 +19,7 @@ namespace Discord.Rest
public string Name { get; private set; }

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);

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)
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options);
#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
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)
=> ChannelHelper.TriggerTypingAsync(this, Discord, options);
@@ -132,11 +132,11 @@ namespace Discord.Rest
=> await GetPinnedMessagesAsync(options).ConfigureAwait(false);

#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
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)
=> await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false);
IDisposable IMessageChannel.EnterTypingState(RequestOptions options)


+ 12
- 12
src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics;
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)
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options);
#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
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)
=> ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options);
@@ -123,18 +123,18 @@ namespace Discord.Rest
else
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);

#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
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);
IDisposable IMessageChannel.EnterTypingState(RequestOptions options)
IDisposable IMessageChannel.EnterTypingState(RequestOptions options)
=> EnterTypingState(options);

//IGuildChannel


+ 10
- 10
src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
@@ -21,7 +21,7 @@ namespace Discord.Rest
{
return new RestVirtualMessageChannel(discord, id);
}
public Task<RestMessage> GetMessageAsync(ulong id, RequestOptions options = null)
=> ChannelHelper.GetMessageAsync(this, Discord, id, options);
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)
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options);
#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
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)
=> ChannelHelper.TriggerTypingAsync(this, Discord, options);
@@ -82,11 +82,11 @@ namespace Discord.Rest
=> await GetPinnedMessagesAsync(options);

#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
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)
=> await SendMessageAsync(text, isTTS, embed, options);
IDisposable IMessageChannel.EnterTypingState(RequestOptions options)


+ 4
- 1
src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs View File

@@ -1,4 +1,4 @@
using Discord.API.Rest;
using Discord.API.Rest;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -13,6 +13,9 @@ namespace Discord.Rest
public static async Task<Model> ModifyAsync(IMessage msg, BaseDiscordClient client, Action<MessageProperties> func,
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();
func(args);
var apiArgs = new API.Rest.ModifyMessageParams


+ 1
- 1
src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;


+ 4
- 1
src/Discord.Net.Rest/Entities/Users/RestUser.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Model = Discord.API.User;
@@ -60,6 +60,9 @@ namespace Discord.Rest
public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128)
=> CDN.GetUserAvatarUrl(Id, AvatarId, size, format);

public string GetDefaultAvatarUrl()
=> CDN.GetDefaultUserAvatarUrl(DiscriminatorValue);

public override string ToString() => $"{Username}#{Discriminator}";
private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})";



+ 10
- 2
src/Discord.Net.Rest/Net/Converters/OptionalConverter.cs View File

@@ -1,4 +1,4 @@
using Newtonsoft.Json;
using Newtonsoft.Json;
using System;

namespace Discord.Net.Converters
@@ -19,10 +19,18 @@ namespace Discord.Net.Converters
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
T obj;
// custom converters need to be able to safely fail; move this check in here to prevent wasteful casting when parsing primitives
if (_innerConverter != null)
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
obj = serializer.Deserialize<T>(reader);

return new Optional<T>(obj);
}



+ 11
- 6
src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using Newtonsoft.Json;

namespace Discord.Net.Converters
@@ -11,13 +11,18 @@ namespace Discord.Net.Converters
public override bool CanRead => true;
public override bool CanWrite => true;

// 1e13 unix ms = year 2286
// necessary to prevent discord.js from sending values in the e15 and overflowing a DTO
private const long MaxSaneMs = 1_000_000_000_000_0;

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// Discord doesn't validate if timestamps contain decimals or not
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);
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)
@@ -25,4 +30,4 @@ namespace Discord.Net.Converters
throw new NotImplementedException();
}
}
}
}

+ 2
- 1
src/Discord.Net.Rest/Net/DefaultRestClient.cs View File

@@ -1,4 +1,5 @@
using Newtonsoft.Json;
using Discord.Net.Converters;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Globalization;


+ 7
- 7
src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs View File

@@ -1,4 +1,4 @@
using Newtonsoft.Json;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
#if DEBUG_LIMITS
@@ -86,7 +86,7 @@ namespace Discord.Net.Queue
Debug.WriteLine($"[{id}] (!) 502");
#endif
if ((request.Options.RetryMode & RetryMode.Retry502) == 0)
throw new HttpException(HttpStatusCode.BadGateway, null);
throw new HttpException(HttpStatusCode.BadGateway, request, null);

continue; //Retry
default:
@@ -106,7 +106,7 @@ namespace Discord.Net.Queue
}
catch { }
}
throw new HttpException(response.StatusCode, code, reason);
throw new HttpException(response.StatusCode, request, code, reason);
}
}
else
@@ -163,7 +163,7 @@ namespace Discord.Net.Queue
if (!isRateLimited)
throw new TimeoutException();
else
throw new RateLimitedException();
throw new RateLimitedException(request);
}

lock (_lock)
@@ -182,12 +182,12 @@ namespace Discord.Net.Queue
}

if ((request.Options.RetryMode & RetryMode.RetryRatelimit) == 0)
throw new RateLimitedException();
throw new RateLimitedException(request);

if (resetAt.HasValue)
{
if (resetAt > timeoutAt)
throw new RateLimitedException();
throw new RateLimitedException(request);
int millis = (int)Math.Ceiling((resetAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds);
#if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Sleeping {millis} ms (Pre-emptive)");
@@ -198,7 +198,7 @@ namespace Discord.Net.Queue
else
{
if ((timeoutAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds < 500.0)
throw new RateLimitedException();
throw new RateLimitedException(request);
#if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Sleeping 500* ms (Pre-emptive)");
#endif


+ 2
- 2
src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs View File

@@ -1,11 +1,11 @@
using Discord.Net.Rest;
using Discord.Net.Rest;
using System;
using System.IO;
using System.Threading.Tasks;

namespace Discord.Net.Queue
{
public class RestRequest
public class RestRequest : IRequest
{
public IRestClient Client { get; }
public string Method { get; }


+ 2
- 2
src/Discord.Net.Rest/Net/Queue/Requests/WebSocketRequest.cs View File

@@ -1,4 +1,4 @@
using Discord.Net.WebSockets;
using Discord.Net.WebSockets;
using System;
using System.IO;
using System.Threading;
@@ -6,7 +6,7 @@ using System.Threading.Tasks;

namespace Discord.Net.Queue
{
public class WebSocketRequest
public class WebSocketRequest : IRequest
{
public IWebSocketClient Client { get; }
public string BucketId { get; }


+ 2
- 2
src/Discord.Net.WebSocket/BaseSocketClient.cs View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Discord.API;
@@ -44,7 +44,7 @@ namespace Discord.WebSocket
/// <inheritdoc />
public abstract Task StopAsync();
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 DownloadUsersAsync(IEnumerable<IGuild> guilds);



+ 7
- 7
src/Discord.Net.WebSocket/DiscordShardedClient.cs View File

@@ -75,8 +75,8 @@ namespace Discord.WebSocket
{
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;
_shards = new DiscordSocketClient[_shardIds.Length];
for (int i = 0; i < _shardIds.Length; i++)
@@ -238,13 +238,13 @@ namespace Discord.WebSocket
for (int i = 0; i < _shards.Length; i++)
await _shards[i].SetStatusAsync(status).ConfigureAwait(false);
}
public override async Task SetGameAsync(string name, string streamUrl = null, StreamType streamType = StreamType.NotStreaming)
public override async Task SetGameAsync(string name, string streamUrl = null, ActivityType type = ActivityType.Playing)
{
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);
}
public override async Task SetActivityAsync(IActivity activity)


+ 2
- 13
src/Discord.Net.WebSocket/DiscordSocketApiClient.cs View File

@@ -1,4 +1,4 @@
#pragma warning disable CS1591
#pragma warning disable CS1591
using Discord.API.Gateway;
using Discord.API.Rest;
using Discord.Net.Queue;
@@ -207,18 +207,7 @@ namespace Discord.API
await RequestQueue.SendAsync(new WebSocketRequest(WebSocketClient, null, bytes, true, options)).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)
{
options = RequestOptions.CreateOrClone(options);


+ 11
- 22
src/Discord.Net.WebSocket/DiscordSocketClient.cs View File

@@ -1,4 +1,3 @@
#pragma warning disable CS0618
using Discord.API;
using Discord.API.Gateway;
using Discord.Logging;
@@ -326,12 +325,12 @@ namespace Discord.WebSocket
_statusSince = null;
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))
Activity = new StreamingGame(name, streamUrl, streamType);
Activity = new StreamingGame(name, streamUrl);
else if (!string.IsNullOrEmpty(name))
Activity = new Game(name);
Activity = new Game(name, type);
else
Activity = null;
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
if (Activity is RichGame game)
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.StreamType = StreamType.NotStreaming;
gameModel.Type = Activity.Type;
if (Activity is StreamingGame streamGame)
gameModel.StreamUrl = streamGame.Url;
}

await ApiClient.SendStatusUpdateAsync(
@@ -418,11 +415,8 @@ namespace Discord.WebSocket

_sessionId = null;
_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;
case GatewayOpCode.Reconnect:
@@ -451,7 +445,7 @@ namespace Discord.WebSocket
{
var model = data.Guilds[i];
var guild = AddGuild(model, state);
if (!guild.IsAvailable || ApiClient.AuthTokenType == TokenType.User)
if (!guild.IsAvailable)
unavailableGuilds++;
else
await GuildAvailableAsync(guild).ConfigureAwait(false);
@@ -470,9 +464,6 @@ namespace Discord.WebSocket
return;
}

if (ApiClient.AuthTokenType == TokenType.User)
await SyncGuildsAsync().ConfigureAwait(false);

_lastGuildAvailableTime = Environment.TickCount;
_guildDownloadTask = WaitForGuildsAsync(_connection.CancelToken, _gatewayLogger)
.ContinueWith(async x =>
@@ -547,8 +538,6 @@ namespace Discord.WebSocket
var guild = AddGuild(data, State);
if (guild != null)
{
if (ApiClient.AuthTokenType == TokenType.User)
await SyncGuildsAsync().ConfigureAwait(false);
await TimedInvokeAsync(_joinedGuildEvent, nameof(JoinedGuild), guild).ConfigureAwait(false);
}
else


+ 3
- 3
src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs View File

@@ -1,4 +1,4 @@
using Discord.Rest;
using Discord.Rest;
using System.Collections.Generic;
using System.IO;
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);
#if FILESYSTEM
/// <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
/// <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);
/// <summary> Gets the last N messages from this message channel. </summary>


+ 23
- 7
src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
@@ -15,10 +15,12 @@ namespace Discord.WebSocket
public class SocketCategoryChannel : SocketGuildChannel, ICategoryChannel
{
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
=> Guild.Channels.Where(x => x.CategoryId == CategoryId).ToImmutableArray();
=> Guild.Channels.Where(x => x.CategoryId == Id).ToImmutableArray();

internal SocketCategoryChannel(DiscordSocketClient discord, ulong id, SocketGuild guild)
: base(discord, id, guild)
@@ -31,14 +33,28 @@ namespace Discord.WebSocket
return entity;
}

//Users
public override SocketGuildUser GetUser(ulong id)
{
var user = Guild.GetUser(id);
if (user != null)
{
var guildPerms = Permissions.ResolveGuild(Guild, user);
var channelPerms = Permissions.ResolveChannel(Guild, user, this, guildPerms);
if (Permissions.GetValue(channelPerms, ChannelPermission.ViewChannel))
return user;
}
return null;
}

private string DebuggerDisplay => $"{Name} ({Id}, Category)";
internal new SocketCategoryChannel Clone() => MemberwiseClone() as SocketCategoryChannel;

// IGuildChannel
IAsyncEnumerable<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)
=> throw new NotSupportedException();
=> Task.FromResult<IGuildUser>(GetUser(id));
Task<IInviteMetadata> IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options)
=> throw new NotSupportedException();
Task<IReadOnlyCollection<IInviteMetadata>> IGuildChannel.GetInvitesAsync(RequestOptions options)
@@ -46,8 +62,8 @@ namespace Discord.WebSocket

//IChannel
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)
=> throw new NotSupportedException();
=> Task.FromResult<IUser>(GetUser(id));
}
}

+ 10
- 10
src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs View File

@@ -1,4 +1,4 @@
using Discord.Rest;
using Discord.Rest;
using System;
using System.Collections.Generic;
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)
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options);
#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
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)
=> ChannelHelper.TriggerTypingAsync(this, Discord, options);
@@ -113,7 +113,7 @@ namespace Discord.WebSocket

//IPrivateChannel
IReadOnlyCollection<IUser> IPrivateChannel.Recipients => ImmutableArray.Create<IUser>(Recipient);
//IMessageChannel
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)
=> await GetPinnedMessagesAsync(options).ConfigureAwait(false);
#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
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)
=> await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false);
IDisposable IMessageChannel.EnterTypingState(RequestOptions options)


+ 10
- 10
src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs View File

@@ -1,4 +1,4 @@
using Discord.Audio;
using Discord.Audio;
using Discord.Rest;
using System;
using System.Collections.Concurrent;
@@ -61,7 +61,7 @@ namespace Discord.WebSocket
users[models[i].Id] = SocketGroupUser.Create(this, state, models[i]);
_users = users;
}
public Task LeaveAsync(RequestOptions options = null)
=> 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)
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options);
#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
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)
=> ChannelHelper.TriggerTypingAsync(this, Discord, options);
@@ -195,11 +195,11 @@ namespace Discord.WebSocket
async Task<IReadOnlyCollection<IMessage>> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options)
=> await GetPinnedMessagesAsync(options).ConfigureAwait(false);
#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
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)
=> await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false);
IDisposable IMessageChannel.EnterTypingState(RequestOptions options)


+ 13
- 13
src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs View File

@@ -1,4 +1,4 @@
using Discord.Rest;
using Discord.Rest;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -16,7 +16,7 @@ namespace Discord.WebSocket
private readonly MessageCache _messages;

public string Topic { get; private set; }
private bool _nsfw;
public bool IsNsfw => _nsfw || ChannelHelper.IsNsfw(this);

@@ -24,9 +24,9 @@ namespace Discord.WebSocket
public IReadOnlyCollection<SocketMessage> CachedMessages => _messages?.Messages ?? ImmutableArray.Create<SocketMessage>();
public override IReadOnlyCollection<SocketGuildUser> Users
=> 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();
internal SocketTextChannel(DiscordSocketClient discord, ulong id, SocketGuild 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)
=> ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options);
#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
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)
=> 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)
=> await GetPinnedMessagesAsync(options).ConfigureAwait(false);
#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
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)
=> await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false);
IDisposable IMessageChannel.EnterTypingState(RequestOptions options)
=> EnterTypingState(options);
}
}
}

+ 4
- 9
src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs View File

@@ -1,4 +1,3 @@
#pragma warning disable CS0618
using Discord.Audio;
using Discord.Rest;
using System;
@@ -64,7 +63,7 @@ namespace Discord.WebSocket
public Task DownloaderPromise => _downloaderPromise.Task;
public IAudioClient AudioClient => _audioClient;
public SocketTextChannel DefaultChannel => TextChannels
.Where(c => CurrentUser.GetPermissions(c).ReadMessages)
.Where(c => CurrentUser.GetPermissions(c).ViewChannel)
.OrderBy(c => c.Position)
.FirstOrDefault();
public SocketVoiceChannel AFKChannel
@@ -192,12 +191,9 @@ namespace Discord.WebSocket

_syncPromise = 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)
{
@@ -696,7 +692,6 @@ namespace Discord.WebSocket
=> Task.FromResult<IGuildUser>(CurrentUser);
Task<IGuildUser> IGuild.GetOwnerAsync(CacheMode mode, RequestOptions options)
=> Task.FromResult<IGuildUser>(Owner);
Task IGuild.DownloadUsersAsync() { throw new NotSupportedException(); }

async Task<IWebhook> IGuild.GetWebhookAsync(ulong id, RequestOptions options)
=> await GetWebhookAsync(id, options);


+ 1
- 1
src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs View File

@@ -1,4 +1,4 @@
using Discord.Rest;
using Discord.Rest;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;


+ 8
- 5
src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs View File

@@ -1,4 +1,4 @@
using Discord.Rest;
using Discord.Rest;
using System;
using System.Threading.Tasks;
using Model = Discord.API.User;
@@ -37,23 +37,23 @@ namespace Discord.WebSocket
{
var newVal = ushort.Parse(model.Discriminator.Value);
if (newVal != DiscriminatorValue)
{
{
DiscriminatorValue = ushort.Parse(model.Discriminator.Value);
hasChanges = true;
}
}
if (model.Bot.IsSpecified && model.Bot.Value != IsBot)
{
{
IsBot = model.Bot.Value;
hasChanges = true;
}
if (model.Username.IsSpecified && model.Username.Value != Username)
{
{
Username = model.Username.Value;
hasChanges = true;
}
return hasChanges;
}
}

public async Task<IDMChannel> GetOrCreateDMChannelAsync(RequestOptions options = null)
=> 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)
=> CDN.GetUserAvatarUrl(Id, AvatarId, size, format);

public string GetDefaultAvatarUrl()
=> CDN.GetDefaultUserAvatarUrl(DiscriminatorValue);

public override string ToString() => $"{Username}#{Discriminator}";
private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})";
internal SocketUser Clone() => MemberwiseClone() as SocketUser;


+ 26
- 6
src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs View File

@@ -1,9 +1,30 @@
namespace Discord.WebSocket
namespace Discord.WebSocket
{
internal static class EntityExtensions
{
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
if (model.ApplicationId.IsSpecified)
{
@@ -27,15 +48,14 @@
{
return new StreamingGame(
model.Name,
model.StreamUrl.Value,
model.StreamType.Value.GetValueOrDefault());
model.StreamUrl.Value);
}
// Normal Game
return new Game(model.Name);
return new Game(model.Name, model.Type.GetValueOrDefault() ?? ActivityType.Playing);
}

// (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[]
{
@@ -57,7 +77,7 @@
public static GameParty ToEntity(this API.GameParty model)
{
// 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)
{
current = model.Size[0];


+ 1
- 1
src/Discord.Net.Webhook/WebhookClientHelper.cs View File

@@ -49,7 +49,7 @@ namespace Discord.Webhook
if (username != null)
args.Username = username;
if (avatarUrl != null)
args.AvatarUrl = username;
args.AvatarUrl = avatarUrl;
if (embeds != null)
args.Embeds = embeds.Select(x => x.ToModel()).ToArray();
var msg = await client.ApiClient.UploadWebhookFileAsync(client.Webhook.Id, args, options).ConfigureAwait(false);


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save