Browse Source

Merge branch 'dev' into quotationMarks

pull/943/head
Chris Johnston GitHub 7 years ago
parent
commit
0704eab7be
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 766 additions and 243 deletions
  1. +34
    -2
      Discord.Net.sln
  2. +12
    -0
      samples/01_basic_ping_bot/01_basic_ping_bot.csproj
  3. +69
    -0
      samples/01_basic_ping_bot/Program.cs
  4. +17
    -0
      samples/02_commands_framework/02_commands_framework.csproj
  5. +63
    -0
      samples/02_commands_framework/Modules/PublicModule.cs
  6. +60
    -0
      samples/02_commands_framework/Program.cs
  7. +49
    -0
      samples/02_commands_framework/Services/CommandHandlingService.cs
  8. +20
    -0
      samples/02_commands_framework/Services/PictureService.cs
  9. +1
    -0
      src/Discord.Net.Commands/Attributes/CommandAttribute.cs
  10. +3
    -3
      src/Discord.Net.Commands/Builders/CommandBuilder.cs
  11. +0
    -1
      src/Discord.Net.Commands/Builders/ModuleBuilder.cs
  12. +2
    -1
      src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs
  13. +2
    -3
      src/Discord.Net.Commands/CommandParser.cs
  14. +44
    -7
      src/Discord.Net.Commands/CommandService.cs
  15. +0
    -6
      src/Discord.Net.Commands/CommandServiceConfig.cs
  16. +6
    -3
      src/Discord.Net.Commands/Info/CommandInfo.cs
  17. +0
    -5
      src/Discord.Net.Commands/Info/ModuleInfo.cs
  18. +2
    -2
      src/Discord.Net.Commands/Readers/ChannelTypeReader.cs
  19. +2
    -2
      src/Discord.Net.Commands/Readers/MessageTypeReader.cs
  20. +2
    -2
      src/Discord.Net.Commands/Readers/RoleTypeReader.cs
  21. +2
    -2
      src/Discord.Net.Commands/Readers/UserTypeReader.cs
  22. +5
    -1
      src/Discord.Net.Commands/Results/TypeReaderResult.cs
  23. +16
    -5
      src/Discord.Net.Core/CDN.cs
  24. +3
    -2
      src/Discord.Net.Core/Discord.Net.Core.csproj
  25. +8
    -5
      src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs
  26. +20
    -2
      src/Discord.Net.Core/Entities/Guilds/IGuild.cs
  27. +6
    -6
      src/Discord.Net.Core/Entities/Guilds/IVoiceRegion.cs
  28. +5
    -4
      src/Discord.Net.Core/Entities/Invites/IInvite.cs
  29. +13
    -2
      src/Discord.Net.Core/Entities/Messages/Embed.cs
  30. +68
    -70
      src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs
  31. +2
    -0
      src/Discord.Net.Core/Entities/Users/IUser.cs
  32. +1
    -1
      src/Discord.Net.Core/IDiscordClient.cs
  33. +3
    -2
      src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj
  34. +5
    -1
      src/Discord.Net.Rest/API/Common/Invite.cs
  35. +5
    -5
      src/Discord.Net.Rest/API/Common/VoiceRegion.cs
  36. +7
    -0
      src/Discord.Net.Rest/API/Rest/GetInviteParams.cs
  37. +12
    -0
      src/Discord.Net.Rest/AssemblyInfo.cs
  38. +1
    -1
      src/Discord.Net.Rest/BaseDiscordClient.cs
  39. +8
    -4
      src/Discord.Net.Rest/ClientHelper.cs
  40. +3
    -2
      src/Discord.Net.Rest/Discord.Net.Rest.csproj
  41. +14
    -10
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  42. +4
    -4
      src/Discord.Net.Rest/DiscordRestClient.cs
  43. +6
    -1
      src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs
  44. +11
    -1
      src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
  45. +10
    -5
      src/Discord.Net.Rest/Entities/Guilds/RestVoiceRegion.cs
  46. +1
    -6
      src/Discord.Net.Rest/Entities/Invites/InviteHelper.cs
  47. +10
    -5
      src/Discord.Net.Rest/Entities/Invites/RestInvite.cs
  48. +4
    -1
      src/Discord.Net.Rest/Entities/Users/RestUser.cs
  49. +10
    -5
      src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs
  50. +8
    -1
      src/Discord.Net.WebSocket/BaseSocketClient.Events.cs
  51. +4
    -4
      src/Discord.Net.WebSocket/BaseSocketClient.cs
  52. +3
    -2
      src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj
  53. +3
    -3
      src/Discord.Net.WebSocket/DiscordShardedClient.cs
  54. +50
    -35
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  55. +10
    -0
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
  56. +8
    -5
      src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs
  57. +1
    -0
      src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs
  58. +21
    -0
      src/Discord.Net.WebSocket/Entities/Voice/SocketVoiceServer.cs
  59. +7
    -3
      src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs

+ 34
- 2
Discord.Net.sln View File

@@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15 # Visual Studio 15
VisualStudioVersion = 15.0.27130.0
VisualStudioVersion = 15.0.27004.2009
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Core", "src\Discord.Net.Core\Discord.Net.Core.csproj", "{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Core", "src\Discord.Net.Core\Discord.Net.Core.csproj", "{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}"
EndProject EndProject
@@ -22,7 +22,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Tests", "test\D
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Webhook", "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj", "{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Webhook", "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj", "{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Analyzers", "src\Discord.Net.Analyzers\Discord.Net.Analyzers.csproj", "{BBA8E7FB-C834-40DC-822F-B112CB7F0140}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Analyzers", "src\Discord.Net.Analyzers\Discord.Net.Analyzers.csproj", "{BBA8E7FB-C834-40DC-822F-B112CB7F0140}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "01_basic_ping_bot", "samples\01_basic_ping_bot\01_basic_ping_bot.csproj", "{F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "02_commands_framework", "samples\02_commands_framework\02_commands_framework.csproj", "{4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -130,6 +136,30 @@ Global
{BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|x64.Build.0 = 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.ActiveCfg = Release|Any CPU
{BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|x86.Build.0 = Release|Any CPU {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|x86.Build.0 = Release|Any CPU
{F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Debug|x64.ActiveCfg = Debug|Any CPU
{F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Debug|x64.Build.0 = Debug|Any CPU
{F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Debug|x86.ActiveCfg = Debug|Any CPU
{F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Debug|x86.Build.0 = Debug|Any CPU
{F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Release|Any CPU.Build.0 = Release|Any CPU
{F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Release|x64.ActiveCfg = Release|Any CPU
{F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Release|x64.Build.0 = Release|Any CPU
{F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Release|x86.ActiveCfg = Release|Any CPU
{F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Release|x86.Build.0 = Release|Any CPU
{4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Debug|x64.ActiveCfg = Debug|Any CPU
{4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Debug|x64.Build.0 = Debug|Any CPU
{4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Debug|x86.ActiveCfg = Debug|Any CPU
{4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Debug|x86.Build.0 = Debug|Any CPU
{4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Release|Any CPU.Build.0 = Release|Any CPU
{4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Release|x64.ActiveCfg = Release|Any CPU
{4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Release|x64.Build.0 = Release|Any CPU
{4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Release|x86.ActiveCfg = Release|Any CPU
{4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -141,6 +171,8 @@ Global
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012} {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012}
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A}
{BBA8E7FB-C834-40DC-822F-B112CB7F0140} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} {BBA8E7FB-C834-40DC-822F-B112CB7F0140} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A}
{F2FF84FB-F6AD-47E5-9EE5-18206CAE136E} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}
{4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D2404771-EEC8-45F2-9D71-F3373F6C1495} SolutionGuid = {D2404771-EEC8-45F2-9D71-F3373F6C1495}


+ 12
- 0
samples/01_basic_ping_bot/01_basic_ping_bot.csproj View File

@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" />
</ItemGroup>

</Project>

+ 69
- 0
samples/01_basic_ping_bot/Program.cs View File

@@ -0,0 +1,69 @@
using System;
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;

namespace _01_basic_ping_bot
{
// This is a minimal, barebones example of using Discord.Net
//
// If writing a bot with commands, we recommend using the Discord.Net.Commands
// framework, rather than handling commands yourself, like we do in this sample.
//
// You can find samples of using the command framework:
// - Here, under the 02_commands_framework sample
// - https://github.com/foxbot/DiscordBotBase - a barebones bot template
// - https://github.com/foxbot/patek - a more feature-filled bot, utilizing more aspects of the library
class Program
{
private DiscordSocketClient _client;

// Discord.Net heavily utilizes TAP for async, so we create
// an asynchronous context from the beginning.
static void Main(string[] args)
=> new Program().MainAsync().GetAwaiter().GetResult();

public async Task MainAsync()
{
_client = new DiscordSocketClient();

_client.Log += LogAsync;
_client.Ready += ReadyAsync;
_client.MessageReceived += MessageReceivedAsync;

// Tokens should be considered secret data, and never hard-coded.
await _client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token"));
await _client.StartAsync();

// Block the program until it is closed.
await Task.Delay(-1);
}

private Task LogAsync(LogMessage log)
{
Console.WriteLine(log.ToString());
return Task.CompletedTask;
}

// The Ready event indicates that the client has opened a
// connection and it is now safe to access the cache.
private Task ReadyAsync()
{
Console.WriteLine($"{_client.CurrentUser} is connected!");

return Task.CompletedTask;
}

// This is not the recommmended way to write a bot - consider
// reading over the Commands Framework sample.
private async Task MessageReceivedAsync(SocketMessage message)
{
// The bot should never respond to itself.
if (message.Author.Id == _client.CurrentUser.Id)
return;

if (message.Content == "!ping")
await message.Channel.SendMessageAsync("pong!");
}
}
}

+ 17
- 0
samples/02_commands_framework/02_commands_framework.csproj View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.1.0-preview1-final" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Discord.Net.Commands\Discord.Net.Commands.csproj" />
<ProjectReference Include="..\..\src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" />
</ItemGroup>

</Project>

+ 63
- 0
samples/02_commands_framework/Modules/PublicModule.cs View File

@@ -0,0 +1,63 @@
using System.IO;
using System.Threading.Tasks;
using Discord;
using Discord.Commands;
using _02_commands_framework.Services;

namespace _02_commands_framework.Modules
{
// Modules must be public and inherit from an IModuleBase
public class PublicModule : ModuleBase<SocketCommandContext>
{
// Dependency Injection will fill this value in for us
public PictureService PictureService { get; set; }

[Command("ping")]
[Alias("pong", "hello")]
public Task PingAsync()
=> ReplyAsync("pong!");

[Command("cat")]
public async Task CatAsync()
{
// Get a stream containing an image of a cat
var stream = await PictureService.GetCatPictureAsync();
// Streams must be seeked to their beginning before being uploaded!
stream.Seek(0, SeekOrigin.Begin);
await Context.Channel.SendFileAsync(stream, "cat.png");
}

// Get info on a user, or the user who invoked the command if one is not specified
[Command("userinfo")]
public async Task UserInfoAsync(IUser user = null)
{
user = user ?? Context.User;

await ReplyAsync(user.ToString());
}

// Ban a user
[Command("ban")]
[RequireContext(ContextType.Guild)]
// make sure the user invoking the command can ban
[RequireUserPermission(GuildPermission.BanMembers)]
// make sure the bot itself can ban
[RequireBotPermission(GuildPermission.BanMembers)]
public async Task BanUserAsync(IGuildUser user, [Remainder] string reason = null)
{
await user.Guild.AddBanAsync(user, reason: reason);
await ReplyAsync("ok!");
}

// [Remainder] takes the rest of the command's arguments as one argument, rather than splitting every space
[Command("echo")]
public Task EchoAsync([Remainder] string text)
// Insert a ZWSP before the text to prevent triggering other bots!
=> ReplyAsync('\u200B' + text);

// 'params' will parse space-separated elements into a list
[Command("list")]
public Task ListAsync(params string[] objects)
=> ReplyAsync("You listed: " + string.Join("; ", objects));
}
}

+ 60
- 0
samples/02_commands_framework/Program.cs View File

@@ -0,0 +1,60 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Discord;
using Discord.WebSocket;
using Discord.Commands;
using _02_commands_framework.Services;

namespace _02_commands_framework
{
// This is a minimal example of using Discord.Net's command
// framework - by no means does it show everything the framework
// is capable of.
//
// You can find samples of using the command framework:
// - Here, under the 02_commands_framework sample
// - https://github.com/foxbot/DiscordBotBase - a barebones bot template
// - https://github.com/foxbot/patek - a more feature-filled bot, utilizing more aspects of the library
class Program
{
static void Main(string[] args)
=> new Program().MainAsync().GetAwaiter().GetResult();

public async Task MainAsync()
{
var services = ConfigureServices();

var client = services.GetRequiredService<DiscordSocketClient>();

client.Log += LogAsync;
services.GetRequiredService<CommandService>().Log += LogAsync;

await client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token"));
await client.StartAsync();

await services.GetRequiredService<CommandHandlingService>().InitializeAsync();

await Task.Delay(-1);
}

private Task LogAsync(LogMessage log)
{
Console.WriteLine(log.ToString());

return Task.CompletedTask;
}

private IServiceProvider ConfigureServices()
{
return new ServiceCollection()
.AddSingleton<DiscordSocketClient>()
.AddSingleton<CommandService>()
.AddSingleton<CommandHandlingService>()
.AddSingleton<HttpClient>()
.AddSingleton<PictureService>()
.BuildServiceProvider();
}
}
}

+ 49
- 0
samples/02_commands_framework/Services/CommandHandlingService.cs View File

@@ -0,0 +1,49 @@
using System;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Discord;
using Discord.Commands;
using Discord.WebSocket;

namespace _02_commands_framework.Services
{
public class CommandHandlingService
{
private readonly CommandService _commands;
private readonly DiscordSocketClient _discord;
private readonly IServiceProvider _services;

public CommandHandlingService(IServiceProvider services)
{
_commands = services.GetRequiredService<CommandService>();
_discord = services.GetRequiredService<DiscordSocketClient>();
_services = services;

_discord.MessageReceived += MessageReceivedAsync;
}

public async Task InitializeAsync()
{
await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services);
}

public async Task MessageReceivedAsync(SocketMessage rawMessage)
{
// Ignore system messages, or messages from other bots
if (!(rawMessage is SocketUserMessage message)) return;
if (message.Source != MessageSource.User) return;

// This value holds the offset where the prefix ends
var argPos = 0;
if (!message.HasMentionPrefix(_discord.CurrentUser, ref argPos)) return;

var context = new SocketCommandContext(_discord, message);
var result = await _commands.ExecuteAsync(context, argPos, _services);

if (result.Error.HasValue &&
result.Error.Value != CommandError.UnknownCommand) // it's bad practice to send 'unknown command' errors
await context.Channel.SendMessageAsync(result.ToString());
}
}
}

+ 20
- 0
samples/02_commands_framework/Services/PictureService.cs View File

@@ -0,0 +1,20 @@
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;

namespace _02_commands_framework.Services
{
public class PictureService
{
private readonly HttpClient _http;

public PictureService(HttpClient http)
=> _http = http;

public async Task<Stream> GetCatPictureAsync()
{
var resp = await _http.GetAsync("https://cataas.com/cat");
return await resp.Content.ReadAsStreamAsync();
}
}
}

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

@@ -7,6 +7,7 @@ namespace Discord.Commands
{ {
public string Text { get; } public string Text { get; }
public RunMode RunMode { get; set; } = RunMode.Default; public RunMode RunMode { get; set; } = RunMode.Default;
public bool? IgnoreExtraArgs { get; set; }


public CommandAttribute() public CommandAttribute()
{ {


+ 3
- 3
src/Discord.Net.Commands/Builders/CommandBuilder.cs View File

@@ -1,8 +1,7 @@
using System;
using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;


namespace Discord.Commands.Builders namespace Discord.Commands.Builders
{ {
@@ -22,6 +21,7 @@ namespace Discord.Commands.Builders
public string PrimaryAlias { get; set; } public string PrimaryAlias { get; set; }
public RunMode RunMode { get; set; } public RunMode RunMode { get; set; }
public int Priority { get; set; } public int Priority { get; set; }
public bool IgnoreExtraArgs { get; set; }


public IReadOnlyList<PreconditionAttribute> Preconditions => _preconditions; public IReadOnlyList<PreconditionAttribute> Preconditions => _preconditions;
public IReadOnlyList<ParameterBuilder> Parameters => _parameters; public IReadOnlyList<ParameterBuilder> Parameters => _parameters;
@@ -140,4 +140,4 @@ namespace Discord.Commands.Builders
return new CommandInfo(this, info, service); return new CommandInfo(this, info, service);
} }
} }
}
}

+ 0
- 1
src/Discord.Net.Commands/Builders/ModuleBuilder.cs View File

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


namespace Discord.Commands.Builders namespace Discord.Commands.Builders
{ {


+ 2
- 1
src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs View File

@@ -158,6 +158,7 @@ namespace Discord.Commands
builder.AddAliases(command.Text); builder.AddAliases(command.Text);
builder.RunMode = command.RunMode; builder.RunMode = command.RunMode;
builder.Name = builder.Name ?? command.Text; builder.Name = builder.Name ?? command.Text;
builder.IgnoreExtraArgs = command.IgnoreExtraArgs ?? service._ignoreExtraArgs;
break; break;
case NameAttribute name: case NameAttribute name:
builder.Name = name.Text; builder.Name = name.Text;
@@ -291,7 +292,7 @@ namespace Discord.Commands


//We dont have a cached type reader, create one //We dont have a cached type reader, create one
reader = ReflectionUtils.CreateObject<TypeReader>(typeReaderType.GetTypeInfo(), service, services); reader = ReflectionUtils.CreateObject<TypeReader>(typeReaderType.GetTypeInfo(), service, services);
service.AddTypeReader(paramType, reader);
service.AddTypeReader(paramType, reader, false);


return reader; return reader;
} }


+ 2
- 3
src/Discord.Net.Commands/CommandParser.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Text; using System.Text;
@@ -14,7 +14,6 @@ namespace Discord.Commands
Parameter, Parameter,
QuotedParameter QuotedParameter
} }

public static async Task<ParseResult> ParseArgsAsync(CommandInfo command, ICommandContext context, bool ignoreExtraArgs, IServiceProvider services, string input, int startPos, IReadOnlyDictionary<char, char> aliasMap) public static async Task<ParseResult> ParseArgsAsync(CommandInfo command, ICommandContext context, bool ignoreExtraArgs, IServiceProvider services, string input, int startPos, IReadOnlyDictionary<char, char> aliasMap)
{ {
ParameterInfo curParam = null; ParameterInfo curParam = null;
@@ -132,7 +131,7 @@ namespace Discord.Commands
{ {
if (curParam == null) if (curParam == null)
{ {
if (ignoreExtraArgs)
if (command.IgnoreExtraArgs)
break; break;
else else
return ParseResult.FromError(CommandError.BadArgCount, "The input text has too many parameters."); return ParseResult.FromError(CommandError.BadArgCount, "The input text has too many parameters.");


+ 44
- 7
src/Discord.Net.Commands/CommandService.cs View File

@@ -6,7 +6,6 @@ using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Discord.Commands.Builders; using Discord.Commands.Builders;
using Discord.Logging; using Discord.Logging;


@@ -217,10 +216,11 @@ namespace Discord.Commands
return true; return true;
} }


//Type Readers
//Type Readers
/// <summary> /// <summary>
/// Adds a custom <see cref="TypeReader"/> to this <see cref="CommandService"/> for the supplied object type. /// Adds a custom <see cref="TypeReader"/> to this <see cref="CommandService"/> for the supplied object type.
/// If <typeparamref name="T"/> is a <see cref="ValueType"/>, a <see cref="NullableTypeReader{T}"/> will also be added. /// If <typeparamref name="T"/> is a <see cref="ValueType"/>, a <see cref="NullableTypeReader{T}"/> will also be added.
/// If a default <see cref="TypeReader"/> exists for <typeparamref name="T"/>, a warning will be logged and the default <see cref="TypeReader"/> will be replaced.
/// </summary> /// </summary>
/// <typeparam name="T">The object type to be read by the <see cref="TypeReader"/>.</typeparam> /// <typeparam name="T">The object type to be read by the <see cref="TypeReader"/>.</typeparam>
/// <param name="reader">An instance of the <see cref="TypeReader"/> to be added.</param> /// <param name="reader">An instance of the <see cref="TypeReader"/> to be added.</param>
@@ -228,17 +228,54 @@ namespace Discord.Commands
=> AddTypeReader(typeof(T), reader); => AddTypeReader(typeof(T), reader);
/// <summary> /// <summary>
/// Adds a custom <see cref="TypeReader"/> to this <see cref="CommandService"/> for the supplied object type. /// Adds a custom <see cref="TypeReader"/> to this <see cref="CommandService"/> for the supplied object type.
/// If <paramref name="type"/> is a <see cref="ValueType"/>, a <see cref="NullableTypeReader{T}"/> for the value type will also be added.
/// If <paramref name="type"/> is a <see cref="ValueType"/>, a <see cref="NullableTypeReader{T}"/> for the value type will also be added.
/// If a default <see cref="TypeReader"/> exists for <paramref name="type"/>, a warning will be logged and the default <see cref="TypeReader"/> will be replaced.
/// </summary> /// </summary>
/// <param name="type">A <see cref="Type"/> instance for the type to be read.</param> /// <param name="type">A <see cref="Type"/> instance for the type to be read.</param>
/// <param name="reader">An instance of the <see cref="TypeReader"/> to be added.</param> /// <param name="reader">An instance of the <see cref="TypeReader"/> to be added.</param>
public void AddTypeReader(Type type, TypeReader reader) public void AddTypeReader(Type type, TypeReader reader)
{ {
var readers = _typeReaders.GetOrAdd(type, x => new ConcurrentDictionary<Type, TypeReader>());
readers[reader.GetType()] = reader;
if (_defaultTypeReaders.ContainsKey(type))
_ = _cmdLogger.WarningAsync($"The default TypeReader for {type.FullName} was replaced by {reader.GetType().FullName}." +
$"To suppress this message, use AddTypeReader<T>(reader, true).");
AddTypeReader(type, reader, true);
}
/// <summary>
/// Adds a custom <see cref="TypeReader"/> to this <see cref="CommandService"/> for the supplied object type.
/// If <typeparamref name="T"/> is a <see cref="ValueType"/>, a <see cref="NullableTypeReader{T}"/> will also be added.
/// </summary>
/// <typeparam name="T">The object type to be read by the <see cref="TypeReader"/>.</typeparam>
/// <param name="reader">An instance of the <see cref="TypeReader"/> to be added.</param>
/// <param name="replaceDefault">If <paramref name="reader"/> should replace the default <see cref="TypeReader"/> for <typeparamref name="T"/> if one exists.</param>
public void AddTypeReader<T>(TypeReader reader, bool replaceDefault)
=> AddTypeReader(typeof(T), reader, replaceDefault);
/// <summary>
/// Adds a custom <see cref="TypeReader"/> to this <see cref="CommandService"/> for the supplied object type.
/// If <paramref name="type"/> is a <see cref="ValueType"/>, a <see cref="NullableTypeReader{T}"/> for the value type will also be added.
/// </summary>
/// <param name="type">A <see cref="Type"/> instance for the type to be read.</param>
/// <param name="reader">An instance of the <see cref="TypeReader"/> to be added.</param>
/// <param name="replaceDefault">If <paramref name="reader"/> should replace the default <see cref="TypeReader"/> for <paramref name="type"/> if one exists.</param>
public void AddTypeReader(Type type, TypeReader reader, bool replaceDefault)
{
if (replaceDefault && _defaultTypeReaders.ContainsKey(type))
{
_defaultTypeReaders.AddOrUpdate(type, reader, (k, v) => reader);
if (type.GetTypeInfo().IsValueType)
{
var nullableType = typeof(Nullable<>).MakeGenericType(type);
var nullableReader = NullableTypeReader.Create(type, reader);
_defaultTypeReaders.AddOrUpdate(nullableType, nullableReader, (k, v) => nullableReader);
}
}
else
{
var readers = _typeReaders.GetOrAdd(type, x => new ConcurrentDictionary<Type, TypeReader>());
readers[reader.GetType()] = reader;


if (type.GetTypeInfo().IsValueType)
AddNullableTypeReader(type, reader);
if (type.GetTypeInfo().IsValueType)
AddNullableTypeReader(type, reader);
}
} }
internal void AddNullableTypeReader(Type valueType, TypeReader valueTypeReader) internal void AddNullableTypeReader(Type valueType, TypeReader valueTypeReader)
{ {


+ 0
- 6
src/Discord.Net.Commands/CommandServiceConfig.cs View File

@@ -25,11 +25,5 @@ namespace Discord.Commands


/// <summary> Determines whether extra parameters should be ignored. </summary> /// <summary> Determines whether extra parameters should be ignored. </summary>
public bool IgnoreExtraArgs { get; set; } = false; public bool IgnoreExtraArgs { get; set; } = false;

///// <summary> Gets or sets the <see cref="IServiceProvider"/> to use. </summary>
//public IServiceProvider ServiceProvider { get; set; } = null;

///// <summary> Gets or sets a factory function for the <see cref="IServiceProvider"/> to use. </summary>
//public Func<CommandService, IServiceProvider> ServiceProviderFactory { get; set; } = null;
} }
} }

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

@@ -27,6 +27,7 @@ namespace Discord.Commands
public string Remarks { get; } public string Remarks { get; }
public int Priority { get; } public int Priority { get; }
public bool HasVarArgs { get; } public bool HasVarArgs { get; }
public bool IgnoreExtraArgs { get; }
public RunMode RunMode { get; } public RunMode RunMode { get; }


public IReadOnlyList<string> Aliases { get; } public IReadOnlyList<string> Aliases { get; }
@@ -63,6 +64,7 @@ namespace Discord.Commands


Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray(); Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray();
HasVarArgs = builder.Parameters.Count > 0 ? builder.Parameters[builder.Parameters.Count - 1].IsMultiple : false; HasVarArgs = builder.Parameters.Count > 0 ? builder.Parameters[builder.Parameters.Count - 1].IsMultiple : false;
IgnoreExtraArgs = builder.IgnoreExtraArgs;


_action = builder.Callback; _action = builder.Callback;
_commandService = service; _commandService = service;
@@ -119,6 +121,7 @@ namespace Discord.Commands
return ParseResult.FromError(preconditionResult); return ParseResult.FromError(preconditionResult);


string input = searchResult.Text.Substring(startIndex); string input = searchResult.Text.Substring(startIndex);

return await CommandParser.ParseArgsAsync(this, context, _commandService._ignoreExtraArgs, services, input, 0, _commandService._quotationMarkAliasMap).ConfigureAwait(false); return await CommandParser.ParseArgsAsync(this, context, _commandService._ignoreExtraArgs, services, input, 0, _commandService._quotationMarkAliasMap).ConfigureAwait(false);
} }


@@ -165,11 +168,11 @@ namespace Discord.Commands
switch (RunMode) switch (RunMode)
{ {
case RunMode.Sync: //Always sync case RunMode.Sync: //Always sync
return await ExecuteAsyncInternalAsync(context, args, services).ConfigureAwait(false);
return await ExecuteInternalAsync(context, args, services).ConfigureAwait(false);
case RunMode.Async: //Always async case RunMode.Async: //Always async
var t2 = Task.Run(async () => var t2 = Task.Run(async () =>
{ {
await ExecuteAsyncInternalAsync(context, args, services).ConfigureAwait(false);
await ExecuteInternalAsync(context, args, services).ConfigureAwait(false);
}); });
break; break;
} }
@@ -181,7 +184,7 @@ namespace Discord.Commands
} }
} }


private async Task<IResult> ExecuteAsyncInternalAsync(ICommandContext context, object[] args, IServiceProvider services)
private async Task<IResult> ExecuteInternalAsync(ICommandContext context, object[] args, IServiceProvider services)
{ {
await Module.Service._cmdLogger.DebugAsync($"Executing {GetLogText(context)}").ConfigureAwait(false); await Module.Service._cmdLogger.DebugAsync($"Executing {GetLogText(context)}").ConfigureAwait(false);
try try


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

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


namespace Discord.Commands namespace Discord.Commands
@@ -23,8 +22,6 @@ namespace Discord.Commands
public ModuleInfo Parent { get; } public ModuleInfo Parent { get; }
public bool IsSubmodule => Parent != null; public bool IsSubmodule => Parent != null;


//public TypeInfo TypeInfo { get; }

internal ModuleInfo(ModuleBuilder builder, CommandService service, IServiceProvider services, ModuleInfo parent = null) internal ModuleInfo(ModuleBuilder builder, CommandService service, IServiceProvider services, ModuleInfo parent = null)
{ {
Service = service; Service = service;
@@ -35,8 +32,6 @@ namespace Discord.Commands
Group = builder.Group; Group = builder.Group;
Parent = parent; Parent = parent;


//TypeInfo = builder.TypeInfo;

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


+ 2
- 2
src/Discord.Net.Commands/Readers/ChannelTypeReader.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
@@ -6,7 +6,7 @@ using System.Threading.Tasks;


namespace Discord.Commands namespace Discord.Commands
{ {
internal class ChannelTypeReader<T> : TypeReader
public class ChannelTypeReader<T> : TypeReader
where T : class, IChannel where T : class, IChannel
{ {
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services) public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)


+ 2
- 2
src/Discord.Net.Commands/Readers/MessageTypeReader.cs View File

@@ -1,10 +1,10 @@
using System;
using System;
using System.Globalization; using System.Globalization;
using System.Threading.Tasks; using System.Threading.Tasks;


namespace Discord.Commands namespace Discord.Commands
{ {
internal class MessageTypeReader<T> : TypeReader
public class MessageTypeReader<T> : TypeReader
where T : class, IMessage where T : class, IMessage
{ {
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services) public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)


+ 2
- 2
src/Discord.Net.Commands/Readers/RoleTypeReader.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
@@ -6,7 +6,7 @@ using System.Threading.Tasks;


namespace Discord.Commands namespace Discord.Commands
{ {
internal class RoleTypeReader<T> : TypeReader
public class RoleTypeReader<T> : TypeReader
where T : class, IRole where T : class, IRole
{ {
public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services) public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)


+ 2
- 2
src/Discord.Net.Commands/Readers/UserTypeReader.cs View File

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


namespace Discord.Commands namespace Discord.Commands
{ {
internal class UserTypeReader<T> : TypeReader
public class UserTypeReader<T> : TypeReader
where T : class, IUser where T : class, IUser
{ {
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services) public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)


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

@@ -1,7 +1,8 @@
using System;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;


namespace Discord.Commands namespace Discord.Commands
{ {
@@ -30,6 +31,9 @@ namespace Discord.Commands
public string ErrorReason { get; } public string ErrorReason { get; }


public bool IsSuccess => !Error.HasValue; public bool IsSuccess => !Error.HasValue;
public object BestMatch => IsSuccess
? (Values.Count == 1 ? Values.Single().Value : Values.OrderByDescending(v => v.Score).First().Value)
: throw new InvalidOperationException("TypeReaderResult was not successful.");


private TypeReaderResult(IReadOnlyCollection<TypeReaderValue> values, CommandError? error, string errorReason) private TypeReaderResult(IReadOnlyCollection<TypeReaderValue> values, CommandError? error, string errorReason)
{ {


+ 16
- 5
src/Discord.Net.Core/CDN.cs View File

@@ -13,6 +13,10 @@ namespace Discord
string extension = FormatToExtension(format, avatarId); string extension = FormatToExtension(format, avatarId);
return $"{DiscordConfig.CDNUrl}avatars/{userId}/{avatarId}.{extension}?size={size}"; return $"{DiscordConfig.CDNUrl}avatars/{userId}/{avatarId}.{extension}?size={size}";
} }
public static string GetDefaultUserAvatarUrl(ushort discriminator)
{
return $"{DiscordConfig.CDNUrl}embed/avatars/{discriminator % 5}.png";
}
public static string GetGuildIconUrl(ulong guildId, string iconId) public static string GetGuildIconUrl(ulong guildId, string iconId)
=> iconId != null ? $"{DiscordConfig.CDNUrl}icons/{guildId}/{iconId}.jpg" : null; => iconId != null ? $"{DiscordConfig.CDNUrl}icons/{guildId}/{iconId}.jpg" : null;
public static string GetGuildSplashUrl(ulong guildId, string splashId) public static string GetGuildSplashUrl(ulong guildId, string splashId)
@@ -30,6 +34,8 @@ namespace Discord


public static string GetSpotifyAlbumArtUrl(string albumArtId) public static string GetSpotifyAlbumArtUrl(string albumArtId)
=> $"https://i.scdn.co/image/{albumArtId}"; => $"https://i.scdn.co/image/{albumArtId}";
public static string GetSpotifyDirectUrl(string trackId)
=> $"https://open.spotify.com/track/{trackId}";


private static string FormatToExtension(ImageFormat format, string imageId) private static string FormatToExtension(ImageFormat format, string imageId)
{ {
@@ -37,11 +43,16 @@ namespace Discord
format = imageId.StartsWith("a_") ? ImageFormat.Gif : ImageFormat.Png; format = imageId.StartsWith("a_") ? ImageFormat.Gif : ImageFormat.Png;
switch (format) switch (format)
{ {
case ImageFormat.Gif: return "gif";
case ImageFormat.Jpeg: return "jpeg";
case ImageFormat.Png: return "png";
case ImageFormat.WebP: return "webp";
default: throw new ArgumentException(nameof(format));
case ImageFormat.Gif:
return "gif";
case ImageFormat.Jpeg:
return "jpeg";
case ImageFormat.Png:
return "png";
case ImageFormat.WebP:
return "webp";
default:
throw new ArgumentException(nameof(format));
} }
} }
} }


+ 3
- 2
src/Discord.Net.Core/Discord.Net.Core.csproj View File

@@ -4,11 +4,12 @@
<AssemblyName>Discord.Net.Core</AssemblyName> <AssemblyName>Discord.Net.Core</AssemblyName>
<RootNamespace>Discord</RootNamespace> <RootNamespace>Discord</RootNamespace>
<Description>The core components for the Discord.Net library.</Description> <Description>The core components for the Discord.Net library.</Description>
<TargetFrameworks>net45;netstandard1.1;netstandard1.3</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">net45;netstandard1.1;netstandard1.3</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">netstandard1.1;netstandard1.3</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="10.0.2" /> <PackageReference Include="Newtonsoft.Json" Version="10.0.2" />
<PackageReference Include="System.Collections.Immutable" Version="1.3.1" /> <PackageReference Include="System.Collections.Immutable" Version="1.3.1" />
<PackageReference Include="System.Interactive.Async" Version="3.1.1" /> <PackageReference Include="System.Interactive.Async" Version="3.1.1" />
</ItemGroup> </ItemGroup>
</Project>
</Project>

+ 8
- 5
src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs View File

@@ -7,17 +7,20 @@ namespace Discord
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] [DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SpotifyGame : Game public class SpotifyGame : Game
{ {
public IEnumerable<string> Artists { get; internal set; }
public string AlbumArt { get; internal set; }
public IReadOnlyCollection<string> Artists { get; internal set; }
public string AlbumTitle { get; internal set; } public string AlbumTitle { get; internal set; }
public string TrackTitle { 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; } public TimeSpan? Duration { get; internal set; }


public string TrackId { get; internal set; }
public string SessionId { get; internal set; }

public string AlbumArtUrl { get; internal set; }
public string TrackUrl { get; internal set; }

internal SpotifyGame() { } internal SpotifyGame() { }


public override string ToString() => Name;
public override string ToString() => $"{string.Join(", ", Artists)} - {TrackTitle} ({Duration})";
private string DebuggerDisplay => $"{Name} (Spotify)"; private string DebuggerDisplay => $"{Name} (Spotify)";
} }
} }

+ 20
- 2
src/Discord.Net.Core/Entities/Guilds/IGuild.cs View File

@@ -1,4 +1,4 @@
using Discord.Audio;
using Discord.Audio;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -66,6 +66,24 @@ namespace Discord


/// <summary> Gets a collection of all users banned on this guild. </summary> /// <summary> Gets a collection of all users banned on this guild. </summary>
Task<IReadOnlyCollection<IBan>> GetBansAsync(RequestOptions options = null); Task<IReadOnlyCollection<IBan>> GetBansAsync(RequestOptions options = null);
/// <summary>
/// Gets a ban object for a banned user.
/// </summary>
/// <param name="user">The banned user.</param>
/// <returns>
/// An awaitable <see cref="Task"/> containing the ban object, which contains the user information and the
/// reason for the ban; <see langword="null"/> if the ban entry cannot be found.
/// </returns>
Task<IBan> GetBanAsync(IUser user, RequestOptions options = null);
/// <summary>
/// Gets a ban object for a banned user.
/// </summary>
/// <param name="userId">The snowflake identifier for the banned user.</param>
/// <returns>
/// An awaitable <see cref="Task"/> containing the ban object, which contains the user information and the
/// reason for the ban; <see langword="null"/> if the ban entry cannot be found.
/// </returns>
Task<IBan> GetBanAsync(ulong userId, RequestOptions options = null);
/// <summary> Bans the provided user from this guild and optionally prunes their recent messages. </summary> /// <summary> Bans the provided user from this guild and optionally prunes their recent messages. </summary>
/// <param name="pruneDays">The number of days to remove messages from this user for - must be between [0, 7]</param> /// <param name="pruneDays">The number of days to remove messages from this user for - must be between [0, 7]</param>
Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null); Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null);
@@ -135,4 +153,4 @@ namespace Discord
/// <summary> Deletes an existing emote from this guild. </summary> /// <summary> Deletes an existing emote from this guild. </summary>
Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null); Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null);
} }
}
}

+ 6
- 6
src/Discord.Net.Core/Entities/Guilds/IVoiceRegion.cs View File

@@ -1,4 +1,4 @@
namespace Discord
namespace Discord
{ {
public interface IVoiceRegion public interface IVoiceRegion
{ {
@@ -10,9 +10,9 @@
bool IsVip { get; } bool IsVip { get; }
/// <summary> Returns true if this voice region is the closest to your machine. </summary> /// <summary> Returns true if this voice region is the closest to your machine. </summary>
bool IsOptimal { get; } bool IsOptimal { get; }
/// <summary> Gets an example hostname for this voice region. </summary>
string SampleHostname { get; }
/// <summary> Gets an example port for this voice region. </summary>
int SamplePort { get; }
/// <summary> Returns true if this is a deprecated voice region (avoid switching to these). </summary>
bool IsDeprecated { get; }
/// <summary> Returns true if this is a custom voice region (used for events/etc) </summary>
bool IsCustom { get; }
} }
}
}

+ 5
- 4
src/Discord.Net.Core/Entities/Invites/IInvite.cs View File

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


namespace Discord namespace Discord
{ {
@@ -22,8 +22,9 @@ namespace Discord
ulong GuildId { get; } ulong GuildId { get; }
/// <summary> Gets the name of the guild this invite is linked to. </summary> /// <summary> Gets the name of the guild this invite is linked to. </summary>
string GuildName { get; } string GuildName { get; }

/// <summary> Accepts this invite and joins the target guild. This will fail on bot accounts. </summary>
Task AcceptAsync(RequestOptions options = null);
/// <summary> Gets the approximated count of online members in the guild. </summary>
int? PresenceCount { get; }
/// <summary> Gets the approximated count of total members in the guild. </summary>
int? MemberCount { get; }
} }
} }

+ 13
- 2
src/Discord.Net.Core/Entities/Messages/Embed.cs View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
@@ -57,7 +57,18 @@ namespace Discord
Fields = fields; Fields = fields;
} }


public int Length => Title?.Length + Author?.Name?.Length + Description?.Length + Footer?.Text?.Length + Fields.Sum(f => f.Name.Length + f.Value.ToString().Length) ?? 0;
public int Length
{
get
{
int titleLength = Title?.Length ?? 0;
int authorLength = Author?.Name?.Length ?? 0;
int descriptionLength = Description?.Length ?? 0;
int footerLength = Footer?.Text?.Length ?? 0;
int fieldSum = Fields.Sum(f => f.Name?.Length + f.Value?.ToString().Length) ?? 0;
return titleLength + authorLength + descriptionLength + footerLength + fieldSum;
}
}


public override string ToString() => Title; public override string ToString() => Title;
private string DebuggerDisplay => $"{Title} ({Type})"; private string DebuggerDisplay => $"{Title} ({Type})";


+ 68
- 70
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.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq;


namespace Discord namespace Discord
{ {
public class EmbedBuilder public class EmbedBuilder
{ {
private readonly Embed _embed;
private string _title;
private string _description;
private string _url;
private EmbedImage? _image;
private EmbedThumbnail? _thumbnail;
private List<EmbedFieldBuilder> _fields;


public const int MaxFieldCount = 25; public const int MaxFieldCount = 25;
public const int MaxTitleLength = 256; public const int MaxTitleLength = 256;
public const int MaxDescriptionLength = 2048; public const int MaxDescriptionLength = 2048;
public const int MaxEmbedLength = 6000; // user bot limit is 2000, but we don't validate that here.
public const int MaxEmbedLength = 6000;


public EmbedBuilder() public EmbedBuilder()
{ {
_embed = new Embed(EmbedType.Rich);
Fields = new List<EmbedFieldBuilder>(); Fields = new List<EmbedFieldBuilder>();
} }


public string Title public string Title
{ {
get => _embed.Title;
get => _title;
set set
{ {
if (value?.Length > MaxTitleLength) throw new ArgumentException($"Title length must be less than or equal to {MaxTitleLength}.", nameof(Title)); if (value?.Length > MaxTitleLength) throw new ArgumentException($"Title length must be less than or equal to {MaxTitleLength}.", nameof(Title));
_embed.Title = value;
_title = value;
} }
} }

public string Description public string Description
{ {
get => _embed.Description;
get => _description;
set set
{ {
if (value?.Length > MaxDescriptionLength) throw new ArgumentException($"Description length must be less than or equal to {MaxDescriptionLength}.", nameof(Description)); if (value?.Length > MaxDescriptionLength) throw new ArgumentException($"Description length must be less than or equal to {MaxDescriptionLength}.", nameof(Description));
_embed.Description = value;
_description = value;
} }
} }


public string Url public string Url
{ {
get => _embed.Url;
get => _url;
set set
{ {
if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(Url)); if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(Url));
_embed.Url = value;
_url = value;
} }
} }
public string ThumbnailUrl public string ThumbnailUrl
{ {
get => _embed.Thumbnail?.Url;
get => _thumbnail?.Url;
set set
{ {
if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(ThumbnailUrl)); if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(ThumbnailUrl));
_embed.Thumbnail = new EmbedThumbnail(value, null, null, null);
_thumbnail = new EmbedThumbnail(value, null, null, null);
} }
} }
public string ImageUrl public string ImageUrl
{ {
get => _embed.Image?.Url;
get => _image?.Url;
set set
{ {
if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(ImageUrl)); if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(ImageUrl));
_embed.Image = new EmbedImage(value, null, null, null);
_image = new EmbedImage(value, null, null, null);
} }
} }
public DateTimeOffset? Timestamp { get => _embed.Timestamp; set { _embed.Timestamp = value; } }
public Color? Color { get => _embed.Color; set { _embed.Color = value; } }

public EmbedAuthorBuilder Author { get; set; }
public EmbedFooterBuilder Footer { get; set; }
private List<EmbedFieldBuilder> _fields;
public List<EmbedFieldBuilder> Fields public List<EmbedFieldBuilder> Fields
{ {
get => _fields; get => _fields;
set set
{ {

if (value == null) throw new ArgumentNullException("Cannot set an embed builder's fields collection to null", nameof(Fields)); if (value == null) throw new ArgumentNullException("Cannot set an embed builder's fields collection to null", nameof(Fields));
if (value.Count > MaxFieldCount) throw new ArgumentException($"Field count must be less than or equal to {MaxFieldCount}.", nameof(Fields)); if (value.Count > MaxFieldCount) throw new ArgumentException($"Field count must be less than or equal to {MaxFieldCount}.", nameof(Fields));
_fields = value; _fields = value;
} }
} }


public DateTimeOffset? Timestamp { get; set; }
public Color? Color { get; set; }
public EmbedAuthorBuilder Author { get; set; }
public EmbedFooterBuilder Footer { get; set; }

public int Length
{
get
{
int titleLength = Title?.Length ?? 0;
int authorLength = Author?.Name?.Length ?? 0;
int descriptionLength = Description?.Length ?? 0;
int footerLength = Footer?.Text?.Length ?? 0;
int fieldSum = Fields.Sum(f => f.Name.Length + f.Value.ToString().Length);

return titleLength + authorLength + descriptionLength + footerLength + fieldSum;
}
}

public EmbedBuilder WithTitle(string title) public EmbedBuilder WithTitle(string title)
{ {
Title = title; Title = title;
@@ -180,7 +196,6 @@ namespace Discord
AddField(field); AddField(field);
return this; return this;
} }

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


public Embed Build() public Embed Build()
{ {
_embed.Footer = Footer?.Build();
_embed.Author = Author?.Build();
if (Length > MaxEmbedLength)
throw new InvalidOperationException($"Total embed length must be less than or equal to {MaxEmbedLength}");

var fields = ImmutableArray.CreateBuilder<EmbedField>(Fields.Count); var fields = ImmutableArray.CreateBuilder<EmbedField>(Fields.Count);
for (int i = 0; i < Fields.Count; i++) for (int i = 0; i < Fields.Count; i++)
fields.Add(Fields[i].Build()); fields.Add(Fields[i].Build());
_embed.Fields = fields.ToImmutable();


if (_embed.Length > MaxEmbedLength)
{
throw new InvalidOperationException($"Total embed length must be less than or equal to {MaxEmbedLength}");
}

return _embed;
return new Embed(EmbedType.Rich, Title, Description, Url, Timestamp, Color, _image, null, Author?.Build(), Footer?.Build(), null, _thumbnail, fields.ToImmutable());
} }
} }


public class EmbedFieldBuilder public class EmbedFieldBuilder
{ {
private string _name;
private string _value;
private EmbedField _field; private EmbedField _field;

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


public string Name public string Name
{ {
get => _field.Name;
get => _name;
set set
{ {
if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException($"Field name must not be null, empty or entirely whitespace.", nameof(Name)); if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException($"Field name must not be null, empty or entirely whitespace.", nameof(Name));
if (value.Length > MaxFieldNameLength) throw new ArgumentException($"Field name length must be less than or equal to {MaxFieldNameLength}.", nameof(Name)); if (value.Length > MaxFieldNameLength) throw new ArgumentException($"Field name length must be less than or equal to {MaxFieldNameLength}.", nameof(Name));
_field.Name = value;
_name = value;
} }
} }


public object Value public object Value
{ {
get => _field.Value;
get => _value;
set set
{ {
var stringValue = value?.ToString(); var stringValue = value?.ToString();
if (string.IsNullOrEmpty(stringValue)) throw new ArgumentException($"Field value must not be null or empty.", nameof(Value)); if (string.IsNullOrEmpty(stringValue)) throw new ArgumentException($"Field value must not be null or empty.", nameof(Value));
if (stringValue.Length > MaxFieldValueLength) throw new ArgumentException($"Field value length must be less than or equal to {MaxFieldValueLength}.", nameof(Value)); if (stringValue.Length > MaxFieldValueLength) throw new ArgumentException($"Field value length must be less than or equal to {MaxFieldValueLength}.", nameof(Value));
_field.Value = stringValue;
_value = stringValue;
} }
} }
public bool IsInline { get => _field.Inline; set { _field.Inline = value; } }

public EmbedFieldBuilder()
{
_field = new EmbedField();
}
public bool IsInline { get; set; }


public EmbedFieldBuilder WithName(string name) public EmbedFieldBuilder WithName(string name)
{ {
@@ -270,48 +276,44 @@ namespace Discord
} }


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


public class EmbedAuthorBuilder public class EmbedAuthorBuilder
{ {
private EmbedAuthor _author;

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


public string Name public string Name
{ {
get => _author.Name;
get => _name;
set set
{ {
if (value?.Length > MaxAuthorNameLength) throw new ArgumentException($"Author name length must be less than or equal to {MaxAuthorNameLength}.", nameof(Name)); if (value?.Length > MaxAuthorNameLength) throw new ArgumentException($"Author name length must be less than or equal to {MaxAuthorNameLength}.", nameof(Name));
_author.Name = value;
_name = value;
} }
} }
public string Url public string Url
{ {
get => _author.Url;
get => _url;
set set
{ {
if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(Url)); if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(Url));
_author.Url = value;
_url = value;
} }
} }
public string IconUrl public string IconUrl
{ {
get => _author.IconUrl;
get => _iconUrl;
set set
{ {
if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(IconUrl)); if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(IconUrl));
_author.IconUrl = value;
_iconUrl = value;
} }
} }


public EmbedAuthorBuilder()
{
_author = new EmbedAuthor();
}

public EmbedAuthorBuilder WithName(string name) public EmbedAuthorBuilder WithName(string name)
{ {
Name = name; Name = name;
@@ -329,39 +331,35 @@ namespace Discord
} }


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


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


public const int MaxFooterTextLength = 2048; public const int MaxFooterTextLength = 2048;


public string Text public string Text
{ {
get => _footer.Text;
get => _text;
set set
{ {
if (value?.Length > MaxFooterTextLength) throw new ArgumentException($"Footer text length must be less than or equal to {MaxFooterTextLength}.", nameof(Text)); if (value?.Length > MaxFooterTextLength) throw new ArgumentException($"Footer text length must be less than or equal to {MaxFooterTextLength}.", nameof(Text));
_footer.Text = value;
_text = value;
} }
} }
public string IconUrl public string IconUrl
{ {
get => _footer.IconUrl;
get => _iconUrl;
set set
{ {
if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(IconUrl)); if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(IconUrl));
_footer.IconUrl = value;
_iconUrl = value;
} }
} }


public EmbedFooterBuilder()
{
_footer = new EmbedFooter();
}

public EmbedFooterBuilder WithText(string text) public EmbedFooterBuilder WithText(string text)
{ {
Text = text; Text = text;
@@ -374,6 +372,6 @@ namespace Discord
} }


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

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

@@ -8,6 +8,8 @@ namespace Discord
string AvatarId { get; } string AvatarId { get; }
/// <summary> Gets the url to this user's avatar. </summary> /// <summary> Gets the url to this user's avatar. </summary>
string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128); string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128);
/// <summary> Gets the url to this user's default avatar. </summary>
string GetDefaultAvatarUrl();
/// <summary> Gets the per-username unique id for this user. </summary> /// <summary> Gets the per-username unique id for this user. </summary>
string Discriminator { get; } string Discriminator { get; }
/// <summary> Gets the per-username unique id for this user. </summary> /// <summary> Gets the per-username unique id for this user. </summary>


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

@@ -27,7 +27,7 @@ namespace Discord
Task<IReadOnlyCollection<IGuild>> GetGuildsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); Task<IReadOnlyCollection<IGuild>> GetGuildsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
Task<IGuild> CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null, RequestOptions options = null); Task<IGuild> CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null, RequestOptions options = null);
Task<IInvite> GetInviteAsync(string inviteId, RequestOptions options = null);
Task<IInvite> GetInviteAsync(string inviteId, bool withCount = false, RequestOptions options = null);


Task<IUser> GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); Task<IUser> GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
Task<IUser> GetUserAsync(string username, string discriminator, RequestOptions options = null); Task<IUser> GetUserAsync(string username, string discriminator, RequestOptions options = null);


+ 3
- 2
src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj View File

@@ -4,9 +4,10 @@
<AssemblyName>Discord.Net.DebugTools</AssemblyName> <AssemblyName>Discord.Net.DebugTools</AssemblyName>
<RootNamespace>Discord</RootNamespace> <RootNamespace>Discord</RootNamespace>
<Description>A Discord.Net extension adding some helper classes for diagnosing issues.</Description> <Description>A Discord.Net extension adding some helper classes for diagnosing issues.</Description>
<TargetFrameworks>net45;netstandard1.3</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">net45;netstandard1.3</TargetFrameworks>
<TargetFramework Condition=" '$(OS)' != 'Windows_NT' ">netstandard1.3</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" /> <ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" />
</ItemGroup> </ItemGroup>
</Project>
</Project>

+ 5
- 1
src/Discord.Net.Rest/API/Common/Invite.cs View File

@@ -1,4 +1,4 @@
#pragma warning disable CS1591
#pragma warning disable CS1591
using Newtonsoft.Json; using Newtonsoft.Json;


namespace Discord.API namespace Discord.API
@@ -11,5 +11,9 @@ namespace Discord.API
public InviteGuild Guild { get; set; } public InviteGuild Guild { get; set; }
[JsonProperty("channel")] [JsonProperty("channel")]
public InviteChannel Channel { get; set; } public InviteChannel Channel { get; set; }
[JsonProperty("approximate_presence_count")]
public Optional<int?> PresenceCount { get; set; }
[JsonProperty("approximate_member_count")]
public Optional<int?> MemberCount { get; set; }
} }
} }

+ 5
- 5
src/Discord.Net.Rest/API/Common/VoiceRegion.cs View File

@@ -1,4 +1,4 @@
#pragma warning disable CS1591
#pragma warning disable CS1591
using Newtonsoft.Json; using Newtonsoft.Json;


namespace Discord.API namespace Discord.API
@@ -13,9 +13,9 @@ namespace Discord.API
public bool IsVip { get; set; } public bool IsVip { get; set; }
[JsonProperty("optimal")] [JsonProperty("optimal")]
public bool IsOptimal { get; set; } public bool IsOptimal { get; set; }
[JsonProperty("sample_hostname")]
public string SampleHostname { get; set; }
[JsonProperty("sample_port")]
public int SamplePort { get; set; }
[JsonProperty("deprecated")]
public bool IsDeprecated { get; set; }
[JsonProperty("custom")]
public bool IsCustom { get; set; }
} }
} }

+ 7
- 0
src/Discord.Net.Rest/API/Rest/GetInviteParams.cs View File

@@ -0,0 +1,7 @@
namespace Discord.API.Rest
{
internal class GetInviteParams
{
public Optional<bool?> WithCounts { get; set; }
}
}

+ 12
- 0
src/Discord.Net.Rest/AssemblyInfo.cs View File

@@ -6,5 +6,17 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Discord.Net.Commands")] [assembly: InternalsVisibleTo("Discord.Net.Commands")]
[assembly: InternalsVisibleTo("Discord.Net.Tests")] [assembly: InternalsVisibleTo("Discord.Net.Tests")]


[assembly: TypeForwardedTo(typeof(Discord.Embed))]
[assembly: TypeForwardedTo(typeof(Discord.EmbedBuilder))] [assembly: TypeForwardedTo(typeof(Discord.EmbedBuilder))]
[assembly: TypeForwardedTo(typeof(Discord.EmbedBuilderExtensions))] [assembly: TypeForwardedTo(typeof(Discord.EmbedBuilderExtensions))]
[assembly: TypeForwardedTo(typeof(Discord.EmbedAuthor))]
[assembly: TypeForwardedTo(typeof(Discord.EmbedAuthorBuilder))]
[assembly: TypeForwardedTo(typeof(Discord.EmbedField))]
[assembly: TypeForwardedTo(typeof(Discord.EmbedFieldBuilder))]
[assembly: TypeForwardedTo(typeof(Discord.EmbedFooter))]
[assembly: TypeForwardedTo(typeof(Discord.EmbedFooterBuilder))]
[assembly: TypeForwardedTo(typeof(Discord.EmbedImage))]
[assembly: TypeForwardedTo(typeof(Discord.EmbedProvider))]
[assembly: TypeForwardedTo(typeof(Discord.EmbedThumbnail))]
[assembly: TypeForwardedTo(typeof(Discord.EmbedType))]
[assembly: TypeForwardedTo(typeof(Discord.EmbedVideo))]

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

@@ -148,7 +148,7 @@ namespace Discord.Rest
Task<IReadOnlyCollection<IConnection>> IDiscordClient.GetConnectionsAsync(RequestOptions options) Task<IReadOnlyCollection<IConnection>> IDiscordClient.GetConnectionsAsync(RequestOptions options)
=> Task.FromResult<IReadOnlyCollection<IConnection>>(ImmutableArray.Create<IConnection>()); => Task.FromResult<IReadOnlyCollection<IConnection>>(ImmutableArray.Create<IConnection>());


Task<IInvite> IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options)
Task<IInvite> IDiscordClient.GetInviteAsync(string inviteId, bool withCount, RequestOptions options)
=> Task.FromResult<IInvite>(null); => Task.FromResult<IInvite>(null);


Task<IGuild> IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) Task<IGuild> IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options)


+ 8
- 4
src/Discord.Net.Rest/ClientHelper.cs View File

@@ -50,12 +50,16 @@ namespace Discord.Rest
return models.Select(x => RestConnection.Create(x)).ToImmutableArray(); return models.Select(x => RestConnection.Create(x)).ToImmutableArray();
} }
public static async Task<RestInvite> GetInviteAsync(BaseDiscordClient client,
string inviteId, RequestOptions options)
public static async Task<RestInviteMetadata> GetInviteAsync(BaseDiscordClient client,
string inviteId, bool withCount, RequestOptions options)
{ {
var model = await client.ApiClient.GetInviteAsync(inviteId, options).ConfigureAwait(false);
var args = new GetInviteParams
{
WithCounts = withCount
};
var model = await client.ApiClient.GetInviteAsync(inviteId, args, options).ConfigureAwait(false);
if (model != null) if (model != null)
return RestInvite.Create(client, null, null, model);
return RestInviteMetadata.Create(client, null, null, model);
return null; return null;
} }


+ 3
- 2
src/Discord.Net.Rest/Discord.Net.Rest.csproj View File

@@ -4,7 +4,8 @@
<AssemblyName>Discord.Net.Rest</AssemblyName> <AssemblyName>Discord.Net.Rest</AssemblyName>
<RootNamespace>Discord.Rest</RootNamespace> <RootNamespace>Discord.Rest</RootNamespace>
<Description>A core Discord.Net library containing the REST client and models.</Description> <Description>A core Discord.Net library containing the REST client and models.</Description>
<TargetFrameworks>net45;netstandard1.1;netstandard1.3</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">net45;netstandard1.1;netstandard1.3</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">netstandard1.1;netstandard1.3</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" /> <ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" />
@@ -16,4 +17,4 @@
<ItemGroup Condition=" '$(TargetFramework)' == 'net45' "> <ItemGroup Condition=" '$(TargetFramework)' == 'net45' ">
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
</ItemGroup> </ItemGroup>
</Project>
</Project>

+ 14
- 10
src/Discord.Net.Rest/DiscordRestApiClient.cs View File

@@ -626,7 +626,7 @@ namespace Discord.API
ulong afterUserId = args.AfterUserId.GetValueOrDefault(0); ulong afterUserId = args.AfterUserId.GetValueOrDefault(0);


var ids = new BucketIds(channelId: channelId); var ids = new BucketIds(channelId: channelId);
Expression<Func<string>> endpoint = () => $"channels/{channelId}/messages/{messageId}/reactions/{emoji}";
Expression<Func<string>> endpoint = () => $"channels/{channelId}/messages/{messageId}/reactions/{emoji}?limit={limit}&after={afterUserId}";
return await SendAsync<IReadOnlyCollection<User>>("GET", endpoint, ids, options: options).ConfigureAwait(false); return await SendAsync<IReadOnlyCollection<User>>("GET", endpoint, ids, options: options).ConfigureAwait(false);
} }
public async Task AckMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) public async Task AckMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null)
@@ -800,6 +800,15 @@ namespace Discord.API
var ids = new BucketIds(guildId: guildId); var ids = new BucketIds(guildId: guildId);
return await SendAsync<IReadOnlyCollection<Ban>>("GET", () => $"guilds/{guildId}/bans", ids, options: options).ConfigureAwait(false); return await SendAsync<IReadOnlyCollection<Ban>>("GET", () => $"guilds/{guildId}/bans", ids, options: options).ConfigureAwait(false);
} }
public async Task<Ban> GetGuildBanAsync(ulong guildId, ulong userId, RequestOptions options)
{
Preconditions.NotEqual(userId, 0, nameof(userId));
Preconditions.NotEqual(guildId, 0, nameof(guildId));
options = RequestOptions.CreateOrClone(options);

var ids = new BucketIds(guildId: guildId);
return await SendAsync<Ban>("GET", () => $"guilds/{guildId}/bans/{userId}", ids, options: options).ConfigureAwait(false);
}
public async Task CreateGuildBanAsync(ulong guildId, ulong userId, CreateGuildBanParams args, RequestOptions options = null) public async Task CreateGuildBanAsync(ulong guildId, ulong userId, CreateGuildBanParams args, RequestOptions options = null)
{ {
Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(guildId, 0, nameof(guildId));
@@ -897,7 +906,7 @@ namespace Discord.API
} }


//Guild Invites //Guild Invites
public async Task<Invite> GetInviteAsync(string inviteId, RequestOptions options = null)
public async Task<InviteMetadata> GetInviteAsync(string inviteId, GetInviteParams args, RequestOptions options = null)
{ {
Preconditions.NotNullOrEmpty(inviteId, nameof(inviteId)); Preconditions.NotNullOrEmpty(inviteId, nameof(inviteId));
options = RequestOptions.CreateOrClone(options); options = RequestOptions.CreateOrClone(options);
@@ -910,9 +919,11 @@ namespace Discord.API
if (index >= 0) if (index >= 0)
inviteId = inviteId.Substring(index + 1); inviteId = inviteId.Substring(index + 1);


var withCounts = args.WithCounts.GetValueOrDefault(false);

try try
{ {
return await SendAsync<Invite>("GET", () => $"invites/{inviteId}", new BucketIds(), options: options).ConfigureAwait(false);
return await SendAsync<InviteMetadata>("GET", () => $"invites/{inviteId}?with_counts={withCounts}", new BucketIds(), options: options).ConfigureAwait(false);
} }
catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; } catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; }
} }
@@ -950,13 +961,6 @@ namespace Discord.API
return await SendAsync<Invite>("DELETE", () => $"invites/{inviteId}", new BucketIds(), options: options).ConfigureAwait(false); return await SendAsync<Invite>("DELETE", () => $"invites/{inviteId}", new BucketIds(), options: options).ConfigureAwait(false);
} }
public async Task AcceptInviteAsync(string inviteId, RequestOptions options = null)
{
Preconditions.NotNullOrEmpty(inviteId, nameof(inviteId));
options = RequestOptions.CreateOrClone(options);
await SendAsync("POST", () => $"invites/{inviteId}", new BucketIds(), options: options).ConfigureAwait(false);
}


//Guild Members //Guild Members
public async Task<GuildMember> GetGuildMemberAsync(ulong guildId, ulong userId, RequestOptions options = null) public async Task<GuildMember> GetGuildMemberAsync(ulong guildId, ulong userId, RequestOptions options = null)


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

@@ -56,8 +56,8 @@ namespace Discord.Rest
=> ClientHelper.GetConnectionsAsync(this, options); => ClientHelper.GetConnectionsAsync(this, options);


/// <inheritdoc /> /// <inheritdoc />
public Task<RestInvite> GetInviteAsync(string inviteId, RequestOptions options = null)
=> ClientHelper.GetInviteAsync(this, inviteId, options);
public Task<RestInviteMetadata> GetInviteAsync(string inviteId, bool withCount = false, RequestOptions options = null)
=> ClientHelper.GetInviteAsync(this, inviteId, withCount, options);


/// <inheritdoc /> /// <inheritdoc />
public Task<RestGuild> GetGuildAsync(ulong id, RequestOptions options = null) public Task<RestGuild> GetGuildAsync(ulong id, RequestOptions options = null)
@@ -131,8 +131,8 @@ namespace Discord.Rest
async Task<IReadOnlyCollection<IConnection>> IDiscordClient.GetConnectionsAsync(RequestOptions options) async Task<IReadOnlyCollection<IConnection>> IDiscordClient.GetConnectionsAsync(RequestOptions options)
=> await GetConnectionsAsync(options).ConfigureAwait(false); => await GetConnectionsAsync(options).ConfigureAwait(false);


async Task<IInvite> IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options)
=> await GetInviteAsync(inviteId, options).ConfigureAwait(false);
async Task<IInvite> IDiscordClient.GetInviteAsync(string inviteId, bool withCount, RequestOptions options)
=> await GetInviteAsync(inviteId, withCount, options).ConfigureAwait(false);


async Task<IGuild> IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) async Task<IGuild> IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options)
{ {


+ 6
- 1
src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs View File

@@ -1,4 +1,4 @@
using Discord.API.Rest;
using Discord.API.Rest;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
@@ -111,6 +111,11 @@ namespace Discord.Rest
var models = await client.ApiClient.GetGuildBansAsync(guild.Id, options).ConfigureAwait(false); var models = await client.ApiClient.GetGuildBansAsync(guild.Id, options).ConfigureAwait(false);
return models.Select(x => RestBan.Create(client, x)).ToImmutableArray(); return models.Select(x => RestBan.Create(client, x)).ToImmutableArray();
} }
public static async Task<RestBan> GetBanAsync(IGuild guild, BaseDiscordClient client, ulong userId, RequestOptions options)
{
var model = await client.ApiClient.GetGuildBanAsync(guild.Id, userId, options).ConfigureAwait(false);
return RestBan.Create(client, model);
}


public static async Task AddBanAsync(IGuild guild, BaseDiscordClient client, public static async Task AddBanAsync(IGuild guild, BaseDiscordClient client,
ulong userId, int pruneDays, string reason, RequestOptions options) ulong userId, int pruneDays, string reason, RequestOptions options)


+ 11
- 1
src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs View File

@@ -1,4 +1,4 @@
using Discord.Audio;
using Discord.Audio;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
@@ -140,6 +140,10 @@ namespace Discord.Rest
//Bans //Bans
public Task<IReadOnlyCollection<RestBan>> GetBansAsync(RequestOptions options = null) public Task<IReadOnlyCollection<RestBan>> GetBansAsync(RequestOptions options = null)
=> GuildHelper.GetBansAsync(this, Discord, options); => GuildHelper.GetBansAsync(this, Discord, options);
public Task<RestBan> GetBanAsync(IUser user, RequestOptions options = null)
=> GuildHelper.GetBanAsync(this, Discord, user.Id, options);
public Task<RestBan> GetBanAsync(ulong userId, RequestOptions options = null)
=> GuildHelper.GetBanAsync(this, Discord, userId, options);


public Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null) public Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null)
=> GuildHelper.AddBanAsync(this, Discord, user.Id, pruneDays, reason, options); => GuildHelper.AddBanAsync(this, Discord, user.Id, pruneDays, reason, options);
@@ -291,6 +295,12 @@ namespace Discord.Rest


async Task<IReadOnlyCollection<IBan>> IGuild.GetBansAsync(RequestOptions options) async Task<IReadOnlyCollection<IBan>> IGuild.GetBansAsync(RequestOptions options)
=> await GetBansAsync(options).ConfigureAwait(false); => await GetBansAsync(options).ConfigureAwait(false);
/// <inheritdoc/>
async Task<IBan> IGuild.GetBanAsync(IUser user, RequestOptions options)
=> await GetBanAsync(user, options).ConfigureAwait(false);
/// <inheritdoc/>
async Task<IBan> IGuild.GetBanAsync(ulong userId, RequestOptions options)
=> await GetBanAsync(userId, options).ConfigureAwait(false);


async Task<IReadOnlyCollection<IGuildChannel>> IGuild.GetChannelsAsync(CacheMode mode, RequestOptions options) async Task<IReadOnlyCollection<IGuildChannel>> IGuild.GetChannelsAsync(CacheMode mode, RequestOptions options)
{ {


+ 10
- 5
src/Discord.Net.Rest/Entities/Guilds/RestVoiceRegion.cs View File

@@ -1,4 +1,4 @@
using Discord.Rest;
using Discord.Rest;
using System.Diagnostics; using System.Diagnostics;
using Model = Discord.API.VoiceRegion; using Model = Discord.API.VoiceRegion;


@@ -7,11 +7,16 @@ namespace Discord
[DebuggerDisplay("{DebuggerDisplay,nq}")] [DebuggerDisplay("{DebuggerDisplay,nq}")]
public class RestVoiceRegion : RestEntity<string>, IVoiceRegion public class RestVoiceRegion : RestEntity<string>, IVoiceRegion
{ {
/// <inheritdoc />
public string Name { get; private set; } public string Name { get; private set; }
/// <inheritdoc />
public bool IsVip { get; private set; } public bool IsVip { get; private set; }
/// <inheritdoc />
public bool IsOptimal { get; private set; } public bool IsOptimal { get; private set; }
public string SampleHostname { get; private set; }
public int SamplePort { get; private set; }
/// <inheritdoc />
public bool IsDeprecated { get; private set; }
/// <inheritdoc />
public bool IsCustom { get; private set; }


internal RestVoiceRegion(BaseDiscordClient client, string id) internal RestVoiceRegion(BaseDiscordClient client, string id)
: base(client, id) : base(client, id)
@@ -28,8 +33,8 @@ namespace Discord
Name = model.Name; Name = model.Name;
IsVip = model.IsVip; IsVip = model.IsVip;
IsOptimal = model.IsOptimal; IsOptimal = model.IsOptimal;
SampleHostname = model.SampleHostname;
SamplePort = model.SamplePort;
IsDeprecated = model.IsDeprecated;
IsCustom = model.IsCustom;
} }


public override string ToString() => Name; public override string ToString() => Name;


+ 1
- 6
src/Discord.Net.Rest/Entities/Invites/InviteHelper.cs View File

@@ -1,14 +1,9 @@
using System.Threading.Tasks;
using System.Threading.Tasks;


namespace Discord.Rest namespace Discord.Rest
{ {
internal static class InviteHelper internal static class InviteHelper
{ {
public static async Task AcceptAsync(IInvite invite, BaseDiscordClient client,
RequestOptions options)
{
await client.ApiClient.AcceptInviteAsync(invite.Code, options).ConfigureAwait(false);
}
public static async Task DeleteAsync(IInvite invite, BaseDiscordClient client, public static async Task DeleteAsync(IInvite invite, BaseDiscordClient client,
RequestOptions options) RequestOptions options)
{ {


+ 10
- 5
src/Discord.Net.Rest/Entities/Invites/RestInvite.cs View File

@@ -1,6 +1,7 @@
using System;
using System;
using System.Diagnostics; using System.Diagnostics;
using System.Threading.Tasks; using System.Threading.Tasks;
using Discord.API.Rest;
using Model = Discord.API.Invite; using Model = Discord.API.Invite;


namespace Discord.Rest namespace Discord.Rest
@@ -10,6 +11,8 @@ namespace Discord.Rest
{ {
public string ChannelName { get; private set; } public string ChannelName { get; private set; }
public string GuildName { get; private set; } public string GuildName { get; private set; }
public int? PresenceCount { get; private set; }
public int? MemberCount { get; private set; }
public ulong ChannelId { get; private set; } public ulong ChannelId { get; private set; }
public ulong GuildId { get; private set; } public ulong GuildId { get; private set; }
internal IChannel Channel { get; private set; } internal IChannel Channel { get; private set; }
@@ -36,19 +39,21 @@ namespace Discord.Rest
ChannelId = model.Channel.Id; ChannelId = model.Channel.Id;
GuildName = model.Guild.Name; GuildName = model.Guild.Name;
ChannelName = model.Channel.Name; ChannelName = model.Channel.Name;
MemberCount = model.MemberCount.IsSpecified ? model.MemberCount.Value : null;
PresenceCount = model.PresenceCount.IsSpecified ? model.PresenceCount.Value : null;
} }
public async Task UpdateAsync(RequestOptions options = null) public async Task UpdateAsync(RequestOptions options = null)
{ {
var model = await Discord.ApiClient.GetInviteAsync(Code, options).ConfigureAwait(false);
var args = new GetInviteParams();
if (MemberCount != null || PresenceCount != null)
args.WithCounts = true;
var model = await Discord.ApiClient.GetInviteAsync(Code, args, options).ConfigureAwait(false);
Update(model); Update(model);
} }
public Task DeleteAsync(RequestOptions options = null) public Task DeleteAsync(RequestOptions options = null)
=> InviteHelper.DeleteAsync(this, Discord, options); => InviteHelper.DeleteAsync(this, Discord, options);


public Task AcceptAsync(RequestOptions options = null)
=> InviteHelper.AcceptAsync(this, Discord, options);

public override string ToString() => Url; public override string ToString() => Url;
private string DebuggerDisplay => $"{Url} ({GuildName} / {ChannelName})"; private string DebuggerDisplay => $"{Url} ({GuildName} / {ChannelName})";


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


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

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




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

@@ -163,7 +163,7 @@ namespace Discord.Net.Queue
if (!isRateLimited) if (!isRateLimited)
throw new TimeoutException(); throw new TimeoutException();
else else
throw new RateLimitedException(request);
ThrowRetryLimit(request);
} }


lock (_lock) lock (_lock)
@@ -181,13 +181,12 @@ namespace Discord.Net.Queue
await _queue.RaiseRateLimitTriggered(Id, null).ConfigureAwait(false); await _queue.RaiseRateLimitTriggered(Id, null).ConfigureAwait(false);
} }


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


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

private void ThrowRetryLimit(RestRequest request)
{
if ((request.Options.RetryMode & RetryMode.RetryRatelimit) == 0)
throw new RateLimitedException(request);
}
} }
} }

+ 8
- 1
src/Discord.Net.WebSocket/BaseSocketClient.Events.cs View File

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


namespace Discord.WebSocket namespace Discord.WebSocket
@@ -165,6 +165,13 @@ namespace Discord.WebSocket
remove { _userVoiceStateUpdatedEvent.Remove(value); } remove { _userVoiceStateUpdatedEvent.Remove(value); }
} }
internal readonly AsyncEvent<Func<SocketUser, SocketVoiceState, SocketVoiceState, Task>> _userVoiceStateUpdatedEvent = new AsyncEvent<Func<SocketUser, SocketVoiceState, SocketVoiceState, Task>>(); internal readonly AsyncEvent<Func<SocketUser, SocketVoiceState, SocketVoiceState, Task>> _userVoiceStateUpdatedEvent = new AsyncEvent<Func<SocketUser, SocketVoiceState, SocketVoiceState, Task>>();
/// <summary> Fired when the bot connects to a Discord voice server. </summary>
public event Func<SocketVoiceServer, Task> VoiceServerUpdated
{
add { _voiceServerUpdatedEvent.Add(value); }
remove { _voiceServerUpdatedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketVoiceServer, Task>> _voiceServerUpdatedEvent = new AsyncEvent<Func<SocketVoiceServer, Task>>();
/// <summary> Fired when the connected account is updated. </summary> /// <summary> Fired when the connected account is updated. </summary>
public event Func<SocketSelfUser, SocketSelfUser, Task> CurrentUserUpdated { public event Func<SocketSelfUser, SocketSelfUser, Task> CurrentUserUpdated {
add { _selfUpdatedEvent.Add(value); } add { _selfUpdatedEvent.Add(value); }


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

@@ -55,8 +55,8 @@ namespace Discord.WebSocket
public Task<IReadOnlyCollection<RestConnection>> GetConnectionsAsync(RequestOptions options = null) public Task<IReadOnlyCollection<RestConnection>> GetConnectionsAsync(RequestOptions options = null)
=> ClientHelper.GetConnectionsAsync(this, options ?? RequestOptions.Default); => ClientHelper.GetConnectionsAsync(this, options ?? RequestOptions.Default);
/// <inheritdoc /> /// <inheritdoc />
public Task<RestInvite> GetInviteAsync(string inviteId, RequestOptions options = null)
=> ClientHelper.GetInviteAsync(this, inviteId, options ?? RequestOptions.Default);
public Task<RestInviteMetadata> GetInviteAsync(string inviteId, bool withCount = false, RequestOptions options = null)
=> ClientHelper.GetInviteAsync(this, inviteId, withCount, options ?? RequestOptions.Default);
// IDiscordClient // IDiscordClient
async Task<IApplication> IDiscordClient.GetApplicationInfoAsync(RequestOptions options) async Task<IApplication> IDiscordClient.GetApplicationInfoAsync(RequestOptions options)
@@ -70,8 +70,8 @@ namespace Discord.WebSocket
async Task<IReadOnlyCollection<IConnection>> IDiscordClient.GetConnectionsAsync(RequestOptions options) async Task<IReadOnlyCollection<IConnection>> IDiscordClient.GetConnectionsAsync(RequestOptions options)
=> await GetConnectionsAsync(options).ConfigureAwait(false); => await GetConnectionsAsync(options).ConfigureAwait(false);


async Task<IInvite> IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options)
=> await GetInviteAsync(inviteId, options).ConfigureAwait(false);
async Task<IInvite> IDiscordClient.GetInviteAsync(string inviteId, bool withCount, RequestOptions options)
=> await GetInviteAsync(inviteId, withCount, options).ConfigureAwait(false);


Task<IGuild> IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) Task<IGuild> IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options)
=> Task.FromResult<IGuild>(GetGuild(id)); => Task.FromResult<IGuild>(GetGuild(id));


+ 3
- 2
src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj View File

@@ -4,7 +4,8 @@
<AssemblyName>Discord.Net.WebSocket</AssemblyName> <AssemblyName>Discord.Net.WebSocket</AssemblyName>
<RootNamespace>Discord.WebSocket</RootNamespace> <RootNamespace>Discord.WebSocket</RootNamespace>
<Description>A core Discord.Net library containing the WebSocket client and models.</Description> <Description>A core Discord.Net library containing the WebSocket client and models.</Description>
<TargetFrameworks>net45;netstandard1.1;netstandard1.3</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">net45;netstandard1.1;netstandard1.3</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">netstandard1.1;netstandard1.3</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -14,4 +15,4 @@
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard1.3' "> <ItemGroup Condition=" '$(TargetFramework)' == 'netstandard1.3' ">
<PackageReference Include="System.Net.WebSockets.Client" Version="4.3.1" /> <PackageReference Include="System.Net.WebSockets.Client" Version="4.3.1" />
</ItemGroup> </ItemGroup>
</Project>
</Project>

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

@@ -129,7 +129,7 @@ namespace Discord.WebSocket
private int GetShardIdFor(ulong guildId) private int GetShardIdFor(ulong guildId)
=> (int)((guildId >> 22) % (uint)_totalShards); => (int)((guildId >> 22) % (uint)_totalShards);
public int GetShardIdFor(IGuild guild) public int GetShardIdFor(IGuild guild)
=> GetShardIdFor(guild.Id);
=> GetShardIdFor(guild?.Id ?? 0);
private DiscordSocketClient GetShardFor(ulong guildId) private DiscordSocketClient GetShardFor(ulong guildId)
=> GetShard(GetShardIdFor(guildId)); => GetShard(GetShardIdFor(guildId));
public DiscordSocketClient GetShardFor(IGuild guild) public DiscordSocketClient GetShardFor(IGuild guild)
@@ -327,8 +327,8 @@ namespace Discord.WebSocket
async Task<IReadOnlyCollection<IConnection>> IDiscordClient.GetConnectionsAsync(RequestOptions options) async Task<IReadOnlyCollection<IConnection>> IDiscordClient.GetConnectionsAsync(RequestOptions options)
=> await GetConnectionsAsync().ConfigureAwait(false); => await GetConnectionsAsync().ConfigureAwait(false);


async Task<IInvite> IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options)
=> await GetInviteAsync(inviteId).ConfigureAwait(false);
async Task<IInvite> IDiscordClient.GetInviteAsync(string inviteId, bool withCount, RequestOptions options)
=> await GetInviteAsync(inviteId, withCount, options).ConfigureAwait(false);


Task<IGuild> IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) Task<IGuild> IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options)
=> Task.FromResult<IGuild>(GetGuild(id)); => Task.FromResult<IGuild>(GetGuild(id));


+ 50
- 35
src/Discord.Net.WebSocket/DiscordSocketClient.cs View File

@@ -62,9 +62,9 @@ namespace Discord.WebSocket
internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient;
public override IReadOnlyCollection<SocketGuild> Guilds => State.Guilds; public override IReadOnlyCollection<SocketGuild> Guilds => State.Guilds;
public override IReadOnlyCollection<ISocketPrivateChannel> PrivateChannels => State.PrivateChannels; public override IReadOnlyCollection<ISocketPrivateChannel> PrivateChannels => State.PrivateChannels;
public IReadOnlyCollection<SocketDMChannel> DMChannels
public IReadOnlyCollection<SocketDMChannel> DMChannels
=> State.PrivateChannels.Select(x => x as SocketDMChannel).Where(x => x != null).ToImmutableArray(); => State.PrivateChannels.Select(x => x as SocketDMChannel).Where(x => x != null).ToImmutableArray();
public IReadOnlyCollection<SocketGroupChannel> GroupChannels
public IReadOnlyCollection<SocketGroupChannel> GroupChannels
=> State.PrivateChannels.Select(x => x as SocketGroupChannel).Where(x => x != null).ToImmutableArray(); => State.PrivateChannels.Select(x => x as SocketGroupChannel).Where(x => x != null).ToImmutableArray();
public override IReadOnlyCollection<RestVoiceRegion> VoiceRegions => _voiceRegions.ToReadOnlyCollection(); public override IReadOnlyCollection<RestVoiceRegion> VoiceRegions => _voiceRegions.ToReadOnlyCollection();


@@ -89,11 +89,11 @@ namespace Discord.WebSocket


_stateLock = new SemaphoreSlim(1, 1); _stateLock = new SemaphoreSlim(1, 1);
_gatewayLogger = LogManager.CreateLogger(ShardId == 0 && TotalShards == 1 ? "Gateway" : $"Shard #{ShardId}"); _gatewayLogger = LogManager.CreateLogger(ShardId == 0 && TotalShards == 1 ? "Gateway" : $"Shard #{ShardId}");
_connection = new ConnectionManager(_stateLock, _gatewayLogger, config.ConnectionTimeout,
_connection = new ConnectionManager(_stateLock, _gatewayLogger, config.ConnectionTimeout,
OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x); OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x);
_connection.Connected += () => TimedInvokeAsync(_connectedEvent, nameof(Connected)); _connection.Connected += () => TimedInvokeAsync(_connectedEvent, nameof(Connected));
_connection.Disconnected += (ex, recon) => TimedInvokeAsync(_disconnectedEvent, nameof(Disconnected), ex); _connection.Disconnected += (ex, recon) => TimedInvokeAsync(_disconnectedEvent, nameof(Disconnected), ex);
_nextAudioId = 1; _nextAudioId = 1;
_connectionGroupLock = groupLock; _connectionGroupLock = groupLock;
_parentClient = parentClient; _parentClient = parentClient;
@@ -104,7 +104,7 @@ namespace Discord.WebSocket
_gatewayLogger.WarningAsync("Serializer Error", e.ErrorContext.Error).GetAwaiter().GetResult(); _gatewayLogger.WarningAsync("Serializer Error", e.ErrorContext.Error).GetAwaiter().GetResult();
e.ErrorContext.Handled = true; e.ErrorContext.Handled = true;
}; };
ApiClient.SentGatewayMessage += async opCode => await _gatewayLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false); ApiClient.SentGatewayMessage += async opCode => await _gatewayLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false);
ApiClient.ReceivedGatewayEvent += ProcessMessageAsync; ApiClient.ReceivedGatewayEvent += ProcessMessageAsync;


@@ -136,7 +136,7 @@ namespace Discord.WebSocket
ApiClient.Dispose(); ApiClient.Dispose();
} }
} }
internal override async Task OnLoginAsync(TokenType tokenType, string token) internal override async Task OnLoginAsync(TokenType tokenType, string token)
{ {
if (_parentClient == null) if (_parentClient == null)
@@ -154,11 +154,11 @@ namespace Discord.WebSocket
_voiceRegions = ImmutableDictionary.Create<string, RestVoiceRegion>(); _voiceRegions = ImmutableDictionary.Create<string, RestVoiceRegion>();
} }


public override async Task StartAsync()
public override async Task StartAsync()
=> await _connection.StartAsync().ConfigureAwait(false); => await _connection.StartAsync().ConfigureAwait(false);
public override async Task StopAsync()
public override async Task StopAsync()
=> await _connection.StopAsync().ConfigureAwait(false); => await _connection.StopAsync().ConfigureAwait(false);
private async Task OnConnectingAsync() private async Task OnConnectingAsync()
{ {
if (_connectionGroupLock != null) if (_connectionGroupLock != null)
@@ -181,11 +181,11 @@ namespace Discord.WebSocket


//Wait for READY //Wait for READY
await _connection.WaitAsync().ConfigureAwait(false); await _connection.WaitAsync().ConfigureAwait(false);
await _gatewayLogger.DebugAsync("Sending Status").ConfigureAwait(false); await _gatewayLogger.DebugAsync("Sending Status").ConfigureAwait(false);
await SendStatusAsync().ConfigureAwait(false); await SendStatusAsync().ConfigureAwait(false);
} }
finally
finally
{ {
if (_connectionGroupLock != null) if (_connectionGroupLock != null)
{ {
@@ -230,22 +230,22 @@ namespace Discord.WebSocket
} }


/// <inheritdoc /> /// <inheritdoc />
public override async Task<RestApplication> GetApplicationInfoAsync(RequestOptions options = null)
public override async Task<RestApplication> GetApplicationInfoAsync(RequestOptions options = null)
=> _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this, options ?? RequestOptions.Default).ConfigureAwait(false)); => _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this, options ?? RequestOptions.Default).ConfigureAwait(false));


/// <inheritdoc /> /// <inheritdoc />
public override SocketGuild GetGuild(ulong id)
=> State.GetGuild(id);
public override SocketGuild GetGuild(ulong id)
=> State.GetGuild(id);


/// <inheritdoc /> /// <inheritdoc />
public override SocketChannel GetChannel(ulong id)
public override SocketChannel GetChannel(ulong id)
=> State.GetChannel(id); => State.GetChannel(id);
/// <inheritdoc /> /// <inheritdoc />
public override SocketUser GetUser(ulong id)
public override SocketUser GetUser(ulong id)
=> State.GetUser(id); => State.GetUser(id);
/// <inheritdoc /> /// <inheritdoc />
public override SocketUser GetUser(string username, string discriminator)
public override SocketUser GetUser(string username, string discriminator)
=> State.Users.FirstOrDefault(x => x.Discriminator == discriminator && x.Username == username); => State.Users.FirstOrDefault(x => x.Discriminator == discriminator && x.Username == username);
internal SocketGlobalUser GetOrCreateUser(ClientState state, Discord.API.User model) internal SocketGlobalUser GetOrCreateUser(ClientState state, Discord.API.User model)
{ {
@@ -266,7 +266,7 @@ namespace Discord.WebSocket
return user; return user;
}); });
} }
internal void RemoveUser(ulong id)
internal void RemoveUser(ulong id)
=> State.RemoveUser(id); => State.RemoveUser(id);


/// <inheritdoc /> /// <inheritdoc />
@@ -340,7 +340,7 @@ namespace Discord.WebSocket
Activity = activity; Activity = activity;
await SendStatusAsync().ConfigureAwait(false); await SendStatusAsync().ConfigureAwait(false);
} }
private async Task SendStatusAsync() private async Task SendStatusAsync()
{ {
if (CurrentUser == null) if (CurrentUser == null)
@@ -374,7 +374,7 @@ namespace Discord.WebSocket
if (seq != null) if (seq != null)
_lastSeq = seq.Value; _lastSeq = seq.Value;
_lastMessageTime = Environment.TickCount; _lastMessageTime = Environment.TickCount;
try try
{ {
switch (opCode) switch (opCode)
@@ -390,7 +390,7 @@ namespace Discord.WebSocket
case GatewayOpCode.Heartbeat: case GatewayOpCode.Heartbeat:
{ {
await _gatewayLogger.DebugAsync("Received Heartbeat").ConfigureAwait(false); await _gatewayLogger.DebugAsync("Received Heartbeat").ConfigureAwait(false);
await ApiClient.SendHeartbeatAsync(_lastSeq).ConfigureAwait(false); await ApiClient.SendHeartbeatAsync(_lastSeq).ConfigureAwait(false);
} }
break; break;
@@ -415,7 +415,7 @@ namespace Discord.WebSocket


_sessionId = null; _sessionId = null;
_lastSeq = 0; _lastSeq = 0;
await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards).ConfigureAwait(false); await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards).ConfigureAwait(false);
} }
break; break;
@@ -475,18 +475,18 @@ namespace Discord.WebSocket
} }
else if (_connection.CancelToken.IsCancellationRequested) else if (_connection.CancelToken.IsCancellationRequested)
return; return;
await TimedInvokeAsync(_readyEvent, nameof(Ready)).ConfigureAwait(false); await TimedInvokeAsync(_readyEvent, nameof(Ready)).ConfigureAwait(false);
await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false); await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false);
}); });
var _ = _connection.CompleteAsync();
_ = _connection.CompleteAsync();
} }
break; break;
case "RESUMED": case "RESUMED":
{ {
await _gatewayLogger.DebugAsync("Received Dispatch (RESUMED)").ConfigureAwait(false); await _gatewayLogger.DebugAsync("Received Dispatch (RESUMED)").ConfigureAwait(false);


var _ = _connection.CompleteAsync();
_ = _connection.CompleteAsync();


//Notify the client that these guilds are available again //Notify the client that these guilds are available again
foreach (var guild in State.Guilds) foreach (var guild in State.Guilds)
@@ -514,7 +514,7 @@ namespace Discord.WebSocket
if (guild != null) if (guild != null)
{ {
guild.Update(State, data); guild.Update(State, data);
if (_unavailableGuildCount != 0) if (_unavailableGuildCount != 0)
_unavailableGuildCount--; _unavailableGuildCount--;
await GuildAvailableAsync(guild).ConfigureAwait(false); await GuildAvailableAsync(guild).ConfigureAwait(false);
@@ -1025,7 +1025,7 @@ namespace Discord.WebSocket


SocketUser user = guild.GetUser(data.User.Id); SocketUser user = guild.GetUser(data.User.Id);
if (user == null) if (user == null)
user = SocketUnknownUser.Create(this, State, data.User);
user = SocketUnknownUser.Create(this, State, data.User);
await TimedInvokeAsync(_userBannedEvent, nameof(UserBanned), user, guild).ConfigureAwait(false); await TimedInvokeAsync(_userBannedEvent, nameof(UserBanned), user, guild).ConfigureAwait(false);
} }
else else
@@ -1095,8 +1095,10 @@ namespace Discord.WebSocket
else if (channel is SocketGroupChannel) else if (channel is SocketGroupChannel)
author = (channel as SocketGroupChannel).GetOrAddUser(data.Author.Value); author = (channel as SocketGroupChannel).GetOrAddUser(data.Author.Value);
else else
{
await UnknownChannelUserAsync(type, data.Author.Value.Id, channel.Id).ConfigureAwait(false); await UnknownChannelUserAsync(type, data.Author.Value.Id, channel.Id).ConfigureAwait(false);
return;
return;
}
} }


var msg = SocketMessage.Create(this, State, author, channel, data); var msg = SocketMessage.Create(this, State, author, channel, data);
@@ -1323,7 +1325,7 @@ namespace Discord.WebSocket
await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false); await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false);
} }
} }
var before = user.Clone(); var before = user.Clone();
user.Update(State, data, true); user.Update(State, data, true);
await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), before, user).ConfigureAwait(false); await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), before, user).ConfigureAwait(false);
@@ -1464,16 +1466,29 @@ namespace Discord.WebSocket


var data = (payload as JToken).ToObject<VoiceServerUpdateEvent>(_serializer); var data = (payload as JToken).ToObject<VoiceServerUpdateEvent>(_serializer);
var guild = State.GetGuild(data.GuildId); var guild = State.GetGuild(data.GuildId);
if (guild != null)
var isCached = guild != null;
var cachedGuild = new Cacheable<IGuild, ulong>(guild, data.GuildId, isCached,
() => Task.FromResult(State.GetGuild(data.GuildId) as IGuild));

var voiceServer = new SocketVoiceServer(cachedGuild, data.Endpoint, data.Token);
await TimedInvokeAsync(_voiceServerUpdatedEvent, nameof(UserVoiceStateUpdated), voiceServer).ConfigureAwait(false);

if (isCached)
{ {
string endpoint = data.Endpoint.Substring(0, data.Endpoint.LastIndexOf(':'));
var endpoint = data.Endpoint;

//Only strip out the port if the endpoint contains it
var portBegin = endpoint.LastIndexOf(':');
if (portBegin > 0)
endpoint = endpoint.Substring(0, portBegin);

var _ = guild.FinishConnectAudio(endpoint, data.Token).ConfigureAwait(false); var _ = guild.FinishConnectAudio(endpoint, data.Token).ConfigureAwait(false);
} }
else else
{ {
await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false);
return;
} }

} }
break; break;


@@ -1796,8 +1811,8 @@ namespace Discord.WebSocket
async Task<IReadOnlyCollection<IConnection>> IDiscordClient.GetConnectionsAsync(RequestOptions options) async Task<IReadOnlyCollection<IConnection>> IDiscordClient.GetConnectionsAsync(RequestOptions options)
=> await GetConnectionsAsync().ConfigureAwait(false); => await GetConnectionsAsync().ConfigureAwait(false);


async Task<IInvite> IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options)
=> await GetInviteAsync(inviteId).ConfigureAwait(false);
async Task<IInvite> IDiscordClient.GetInviteAsync(string inviteId, bool withCount, RequestOptions options)
=> await GetInviteAsync(inviteId, withCount, options).ConfigureAwait(false);


Task<IGuild> IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) Task<IGuild> IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options)
=> Task.FromResult<IGuild>(GetGuild(id)); => Task.FromResult<IGuild>(GetGuild(id));


+ 10
- 0
src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs View File

@@ -288,6 +288,10 @@ namespace Discord.WebSocket
//Bans //Bans
public Task<IReadOnlyCollection<RestBan>> GetBansAsync(RequestOptions options = null) public Task<IReadOnlyCollection<RestBan>> GetBansAsync(RequestOptions options = null)
=> GuildHelper.GetBansAsync(this, Discord, options); => GuildHelper.GetBansAsync(this, Discord, options);
public Task<RestBan> GetBanAsync(IUser user, RequestOptions options = null)
=> GuildHelper.GetBanAsync(this, Discord, user.Id, options);
public Task<RestBan> GetBanAsync(ulong userId, RequestOptions options = null)
=> GuildHelper.GetBanAsync(this, Discord, userId, options);


public Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null) public Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null)
=> GuildHelper.AddBanAsync(this, Discord, user.Id, pruneDays, reason, options); => GuildHelper.AddBanAsync(this, Discord, user.Id, pruneDays, reason, options);
@@ -641,6 +645,12 @@ namespace Discord.WebSocket


async Task<IReadOnlyCollection<IBan>> IGuild.GetBansAsync(RequestOptions options) async Task<IReadOnlyCollection<IBan>> IGuild.GetBansAsync(RequestOptions options)
=> await GetBansAsync(options).ConfigureAwait(false); => await GetBansAsync(options).ConfigureAwait(false);
/// <inheritdoc/>
async Task<IBan> IGuild.GetBanAsync(IUser user, RequestOptions options)
=> await GetBanAsync(user, options).ConfigureAwait(false);
/// <inheritdoc/>
async Task<IBan> IGuild.GetBanAsync(ulong userId, RequestOptions options)
=> await GetBanAsync(userId, options).ConfigureAwait(false);


Task<IReadOnlyCollection<IGuildChannel>> IGuild.GetChannelsAsync(CacheMode mode, RequestOptions options) Task<IReadOnlyCollection<IGuildChannel>> IGuild.GetChannelsAsync(CacheMode mode, RequestOptions options)
=> Task.FromResult<IReadOnlyCollection<IGuildChannel>>(Channels); => Task.FromResult<IReadOnlyCollection<IGuildChannel>>(Channels);


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


public async Task<IDMChannel> GetOrCreateDMChannelAsync(RequestOptions options = null) public async Task<IDMChannel> GetOrCreateDMChannelAsync(RequestOptions options = null)
=> GlobalUser.DMChannel ?? await UserHelper.CreateDMChannelAsync(this, Discord, options) as IDMChannel; => GlobalUser.DMChannel ?? await UserHelper.CreateDMChannelAsync(this, Discord, options) as IDMChannel;
@@ -61,6 +61,9 @@ namespace Discord.WebSocket
public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128)
=> CDN.GetUserAvatarUrl(Id, AvatarId, size, format); => CDN.GetUserAvatarUrl(Id, AvatarId, size, format);


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

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


+ 1
- 0
src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs View File

@@ -26,6 +26,7 @@ namespace Discord.WebSocket
internal SocketWebhookUser(SocketGuild guild, ulong id, ulong webhookId) internal SocketWebhookUser(SocketGuild guild, ulong id, ulong webhookId)
: base(guild.Discord, id) : base(guild.Discord, id)
{ {
Guild = guild;
WebhookId = webhookId; WebhookId = webhookId;
} }
internal static SocketWebhookUser Create(SocketGuild guild, ClientState state, Model model, ulong webhookId) internal static SocketWebhookUser Create(SocketGuild guild, ClientState state, Model model, ulong webhookId)


+ 21
- 0
src/Discord.Net.WebSocket/Entities/Voice/SocketVoiceServer.cs View File

@@ -0,0 +1,21 @@
using System.Diagnostics;

namespace Discord.WebSocket
{
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketVoiceServer
{
public Cacheable<IGuild, ulong> Guild { get; private set; }
public string Endpoint { get; private set; }
public string Token { get; private set; }

internal SocketVoiceServer(Cacheable<IGuild, ulong> guild, string endpoint, string token)
{
Guild = guild;
Endpoint = endpoint;
Token = token;
}

private string DebuggerDisplay => $"SocketVoiceServer ({Guild.Id})";
}
}

+ 7
- 3
src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs View File

@@ -1,3 +1,6 @@
using System.Collections.Immutable;
using System.Linq;

namespace Discord.WebSocket namespace Discord.WebSocket
{ {
internal static class EntityExtensions internal static class EntityExtensions
@@ -15,12 +18,13 @@ namespace Discord.WebSocket
{ {
Name = model.Name, Name = model.Name,
SessionId = model.SessionId.GetValueOrDefault(), SessionId = model.SessionId.GetValueOrDefault(),
SyncId = model.SyncId.Value,
TrackId = model.SyncId.Value,
TrackUrl = CDN.GetSpotifyDirectUrl(model.SyncId.Value),
AlbumTitle = albumText, AlbumTitle = albumText,
TrackTitle = model.Details.GetValueOrDefault(), TrackTitle = model.Details.GetValueOrDefault(),
Artists = model.State.GetValueOrDefault()?.Split(';'),
Artists = model.State.GetValueOrDefault()?.Split(';').Select(x=>x?.Trim()).ToImmutableArray(),
Duration = timestamps?.End - timestamps?.Start, Duration = timestamps?.End - timestamps?.Start,
AlbumArt = albumArtId != null ? CDN.GetSpotifyAlbumArtUrl(albumArtId) : null,
AlbumArtUrl = albumArtId != null ? CDN.GetSpotifyAlbumArtUrl(albumArtId) : null,
Type = ActivityType.Listening Type = ActivityType.Listening
}; };
} }


Loading…
Cancel
Save