Browse Source

Merge branch 'dev' into paginate-reactions

pull/1022/head
HelpfulStranger999 GitHub 7 years ago
parent
commit
dbba84d209
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
99 changed files with 2187 additions and 157 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. +1
    -0
      src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs
  13. +3
    -3
      src/Discord.Net.Commands/CommandParser.cs
  14. +0
    -1
      src/Discord.Net.Commands/CommandService.cs
  15. +0
    -6
      src/Discord.Net.Commands/CommandServiceConfig.cs
  16. +3
    -1
      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. +2
    -0
      src/Discord.Net.Core/CDN.cs
  24. +3
    -2
      src/Discord.Net.Core/Discord.Net.Core.csproj
  25. +4
    -3
      src/Discord.Net.Core/DiscordConfig.cs
  26. +8
    -5
      src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs
  27. +50
    -0
      src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs
  28. +14
    -0
      src/Discord.Net.Core/Entities/AuditLogs/IAuditLogData.cs
  29. +34
    -0
      src/Discord.Net.Core/Entities/AuditLogs/IAuditLogEntry.cs
  30. +24
    -2
      src/Discord.Net.Core/Entities/Guilds/IGuild.cs
  31. +6
    -6
      src/Discord.Net.Core/Entities/Guilds/IVoiceRegion.cs
  32. +5
    -4
      src/Discord.Net.Core/Entities/Invites/IInvite.cs
  33. +13
    -2
      src/Discord.Net.Core/Entities/Messages/Embed.cs
  34. +0
    -1
      src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs
  35. +1
    -1
      src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs
  36. +14
    -0
      src/Discord.Net.Core/Entities/Webhooks/WebhookType.cs
  37. +1
    -1
      src/Discord.Net.Core/IDiscordClient.cs
  38. +3
    -2
      src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj
  39. +16
    -0
      src/Discord.Net.Rest/API/Common/AuditLog.cs
  40. +17
    -0
      src/Discord.Net.Rest/API/Common/AuditLogChange.cs
  41. +26
    -0
      src/Discord.Net.Rest/API/Common/AuditLogEntry.cs
  42. +27
    -0
      src/Discord.Net.Rest/API/Common/AuditLogOptions.cs
  43. +5
    -1
      src/Discord.Net.Rest/API/Common/Invite.cs
  44. +5
    -5
      src/Discord.Net.Rest/API/Common/VoiceRegion.cs
  45. +8
    -0
      src/Discord.Net.Rest/API/Rest/GetAuditLogsParams.cs
  46. +7
    -0
      src/Discord.Net.Rest/API/Rest/GetInviteParams.cs
  47. +1
    -1
      src/Discord.Net.Rest/BaseDiscordClient.cs
  48. +8
    -4
      src/Discord.Net.Rest/ClientHelper.cs
  49. +3
    -2
      src/Discord.Net.Rest/Discord.Net.Rest.csproj
  50. +33
    -9
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  51. +4
    -4
      src/Discord.Net.Rest/DiscordRestClient.cs
  52. +58
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs
  53. +23
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs
  54. +52
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs
  55. +45
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs
  56. +18
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs
  57. +45
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelUpdateAuditLogData.cs
  58. +31
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteCreateAuditLogData.cs
  59. +28
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteDeleteAuditLogData.cs
  60. +31
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteUpdateAuditLogData.cs
  61. +32
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildInfo.cs
  62. +79
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildUpdateAuditLogData.cs
  63. +55
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs
  64. +55
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs
  65. +20
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteInfo.cs
  66. +46
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteUpdateAuditLogData.cs
  67. +23
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/KickAuditLogData.cs
  68. +50
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs
  69. +35
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs
  70. +22
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs
  71. +37
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteCreateAuditLogData.cs
  72. +42
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs
  73. +44
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteUpdateAuditLogData.cs
  74. +22
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/PruneAuditLogData.cs
  75. +47
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleCreateAuditLogData.cs
  76. +47
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleDeleteAuditLogData.cs
  77. +21
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleInfo.cs
  78. +62
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleUpdateAuditLogData.cs
  79. +23
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/UnbanAuditLogData.cs
  80. +44
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookCreateAuditLogData.cs
  81. +46
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookDeleteAuditLogData.cs
  82. +16
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookInfo.cs
  83. +52
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookUpdateAuditLogData.cs
  84. +38
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/RestAuditLogEntry.cs
  85. +35
    -1
      src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs
  86. +23
    -1
      src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
  87. +10
    -5
      src/Discord.Net.Rest/Entities/Guilds/RestVoiceRegion.cs
  88. +1
    -6
      src/Discord.Net.Rest/Entities/Invites/InviteHelper.cs
  89. +10
    -5
      src/Discord.Net.Rest/Entities/Invites/RestInvite.cs
  90. +10
    -5
      src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs
  91. +8
    -1
      src/Discord.Net.WebSocket/BaseSocketClient.Events.cs
  92. +4
    -4
      src/Discord.Net.WebSocket/BaseSocketClient.cs
  93. +3
    -2
      src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj
  94. +3
    -3
      src/Discord.Net.WebSocket/DiscordShardedClient.cs
  95. +50
    -35
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  96. +22
    -0
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
  97. +1
    -0
      src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs
  98. +21
    -0
      src/Discord.Net.WebSocket/Entities/Voice/SocketVoiceServer.cs
  99. +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
{ {


+ 1
- 0
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;


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

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -14,7 +14,7 @@ namespace Discord.Commands
QuotedParameter QuotedParameter
} }
public static async Task<ParseResult> ParseArgsAsync(CommandInfo command, ICommandContext context, bool ignoreExtraArgs, IServiceProvider services, string input, int startPos)
public static async Task<ParseResult> ParseArgsAsync(CommandInfo command, ICommandContext context, IServiceProvider services, string input, int startPos)
{ {
ParameterInfo curParam = null; ParameterInfo curParam = null;
StringBuilder argBuilder = new StringBuilder(input.Length); StringBuilder argBuilder = new StringBuilder(input.Length);
@@ -110,7 +110,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.");


+ 0
- 1
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;




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

@@ -20,11 +20,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;
} }
} }

+ 3
- 1
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,7 +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).ConfigureAwait(false);
return await CommandParser.ParseArgsAsync(this, context, services, input, 0).ConfigureAwait(false);
} }


public Task<IResult> ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) public Task<IResult> ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services)


+ 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)
{ {


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

@@ -34,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)
{ {


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

+ 4
- 3
src/Discord.Net.Core/DiscordConfig.cs View File

@@ -4,10 +4,10 @@ namespace Discord
{ {
public class DiscordConfig public class DiscordConfig
{ {
public const int APIVersion = 6;
public const int APIVersion = 6;
public static string Version { get; } = public static string Version { get; } =
typeof(DiscordConfig).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? typeof(DiscordConfig).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ??
typeof(DiscordConfig).GetTypeInfo().Assembly.GetName().Version.ToString(3) ??
typeof(DiscordConfig).GetTypeInfo().Assembly.GetName().Version.ToString(3) ??
"Unknown"; "Unknown";


public static string UserAgent { get; } = $"DiscordBot (https://github.com/RogueException/Discord.Net, v{Version})"; public static string UserAgent { get; } = $"DiscordBot (https://github.com/RogueException/Discord.Net, v{Version})";
@@ -21,10 +21,11 @@ namespace Discord
public const int MaxUsersPerBatch = 1000; public const int MaxUsersPerBatch = 1000;
public const int MaxGuildsPerBatch = 100; public const int MaxGuildsPerBatch = 100;
public const int MaxUserReactionsPerBatch = 100; public const int MaxUserReactionsPerBatch = 100;
public const int MaxAuditLogEntriesPerBatch = 100;


/// <summary> Gets or sets how a request should act in the case of an error, by default. </summary> /// <summary> Gets or sets how a request should act in the case of an error, by default. </summary>
public RetryMode DefaultRetryMode { get; set; } = RetryMode.AlwaysRetry; public RetryMode DefaultRetryMode { get; set; } = RetryMode.AlwaysRetry;
/// <summary> Gets or sets the minimum log level severity that will be sent to the Log event. </summary> /// <summary> Gets or sets the minimum log level severity that will be sent to the Log event. </summary>
public LogSeverity LogLevel { get; set; } = LogSeverity.Info; public LogSeverity LogLevel { get; set; } = LogSeverity.Info;




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

+ 50
- 0
src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs View File

@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// The action type within a <see cref="IAuditLogEntry"/>
/// </summary>
public enum ActionType
{
GuildUpdated = 1,

ChannelCreated = 10,
ChannelUpdated = 11,
ChannelDeleted = 12,

OverwriteCreated = 13,
OverwriteUpdated = 14,
OverwriteDeleted = 15,

Kick = 20,
Prune = 21,
Ban = 22,
Unban = 23,

MemberUpdated = 24,
MemberRoleUpdated = 25,

RoleCreated = 30,
RoleUpdated = 31,
RoleDeleted = 32,

InviteCreated = 40,
InviteUpdated = 41,
InviteDeleted = 42,

WebhookCreated = 50,
WebhookUpdated = 51,
WebhookDeleted = 52,

EmojiCreated = 60,
EmojiUpdated = 61,
EmojiDeleted = 62,

MessageDeleted = 72
}
}

+ 14
- 0
src/Discord.Net.Core/Entities/AuditLogs/IAuditLogData.cs View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// Represents data applied to an <see cref="IAuditLogEntry"/>
/// </summary>
public interface IAuditLogData
{ }
}

+ 34
- 0
src/Discord.Net.Core/Entities/AuditLogs/IAuditLogEntry.cs View File

@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// Represents an entry in an audit log
/// </summary>
public interface IAuditLogEntry : IEntity<ulong>
{
/// <summary>
/// The action which occured to create this entry
/// </summary>
ActionType Action { get; }

/// <summary>
/// The data for this entry. May be <see cref="null"/> if no data was available.
/// </summary>
IAuditLogData Data { get; }

/// <summary>
/// The user responsible for causing the changes
/// </summary>
IUser User { get; }

/// <summary>
/// The reason behind the change. May be <see cref="null"/> if no reason was provided.
/// </summary>
string Reason { get; }
}
}

+ 24
- 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);
@@ -121,6 +139,10 @@ namespace Discord
/// <summary> Removes all users from this guild if they have not logged on in a provided number of days or, if simulate is true, returns the number of users that would be removed. </summary> /// <summary> Removes all users from this guild if they have not logged on in a provided number of days or, if simulate is true, returns the number of users that would be removed. </summary>
Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null); Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null);


/// <summary> Gets the specified number of audit log entries for this guild. </summary>
Task<IReadOnlyCollection<IAuditLogEntry>> GetAuditLogAsync(int limit = DiscordConfig.MaxAuditLogEntriesPerBatch,
CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);

/// <summary> Gets the webhook in this guild with the provided id, or null if not found. </summary> /// <summary> Gets the webhook in this guild with the provided id, or null if not found. </summary>
Task<IWebhook> GetWebhookAsync(ulong id, RequestOptions options = null); Task<IWebhook> GetWebhookAsync(ulong id, RequestOptions options = null);
/// <summary> Gets a collection of all webhooks for this guild. </summary> /// <summary> Gets a collection of all webhooks for this guild. </summary>
@@ -135,4 +157,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})";


+ 0
- 1
src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs View File

@@ -231,7 +231,6 @@ namespace Discord
{ {
private string _name; private string _name;
private string _value; private string _value;
private EmbedField _field;
public const int MaxFieldNameLength = 256; public const int MaxFieldNameLength = 256;
public const int MaxFieldValueLength = 1024; public const int MaxFieldValueLength = 1024;




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

@@ -12,7 +12,7 @@ namespace Discord
/// <summary> Gets a ChannelPermissions that grants all permissions for text channels. </summary> /// <summary> Gets a ChannelPermissions that grants all permissions for text channels. </summary>
public static readonly ChannelPermissions Text = new ChannelPermissions(0b01100_0000000_1111111110001_010001); public static readonly ChannelPermissions Text = new ChannelPermissions(0b01100_0000000_1111111110001_010001);
/// <summary> Gets a ChannelPermissions that grants all permissions for voice channels. </summary> /// <summary> Gets a ChannelPermissions that grants all permissions for voice channels. </summary>
public static readonly ChannelPermissions Voice = new ChannelPermissions(0b00100_1111110_0000000000000_010001);
public static readonly ChannelPermissions Voice = new ChannelPermissions(0b00100_1111110_0000000010000_010001);
/// <summary> Gets a ChannelPermissions that grants all permissions for category channels. </summary> /// <summary> Gets a ChannelPermissions that grants all permissions for category channels. </summary>
public static readonly ChannelPermissions Category = new ChannelPermissions(0b01100_1111110_1111111110001_010001); public static readonly ChannelPermissions Category = new ChannelPermissions(0b01100_1111110_1111111110001_010001);
/// <summary> Gets a ChannelPermissions that grants all permissions for direct message channels. </summary> /// <summary> Gets a ChannelPermissions that grants all permissions for direct message channels. </summary>


+ 14
- 0
src/Discord.Net.Core/Entities/Webhooks/WebhookType.cs View File

@@ -0,0 +1,14 @@
namespace Discord
{
/// <summary>
/// Represents the type of a webhook.
/// </summary>
/// <remarks>
/// This type is currently unused, and is only returned in audit log responses.
/// </remarks>
public enum WebhookType
{
/// <summary> An incoming webhook </summary>
Incoming = 1
}
}

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

+ 16
- 0
src/Discord.Net.Rest/API/Common/AuditLog.cs View File

@@ -0,0 +1,16 @@
using Newtonsoft.Json;

namespace Discord.API
{
internal class AuditLog
{
[JsonProperty("webhooks")]
public Webhook[] Webhooks { get; set; }

[JsonProperty("users")]
public User[] Users { get; set; }

[JsonProperty("audit_log_entries")]
public AuditLogEntry[] Entries { get; set; }
}
}

+ 17
- 0
src/Discord.Net.Rest/API/Common/AuditLogChange.cs View File

@@ -0,0 +1,17 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Discord.API
{
internal class AuditLogChange
{
[JsonProperty("key")]
public string ChangedProperty { get; set; }

[JsonProperty("new_value")]
public JToken NewValue { get; set; }

[JsonProperty("old_value")]
public JToken OldValue { get; set; }
}
}

+ 26
- 0
src/Discord.Net.Rest/API/Common/AuditLogEntry.cs View File

@@ -0,0 +1,26 @@
using Newtonsoft.Json;

namespace Discord.API
{
internal class AuditLogEntry
{
[JsonProperty("target_id")]
public ulong? TargetId { get; set; }
[JsonProperty("user_id")]
public ulong UserId { get; set; }

[JsonProperty("changes")]
public AuditLogChange[] Changes { get; set; }
[JsonProperty("options")]
public AuditLogOptions Options { get; set; }

[JsonProperty("id")]
public ulong Id { get; set; }

[JsonProperty("action_type")]
public ActionType Action { get; set; }

[JsonProperty("reason")]
public string Reason { get; set; }
}
}

+ 27
- 0
src/Discord.Net.Rest/API/Common/AuditLogOptions.cs View File

@@ -0,0 +1,27 @@
using Newtonsoft.Json;

namespace Discord.API
{
internal class AuditLogOptions
{
//Message delete
[JsonProperty("count")]
public int? MessageDeleteCount { get; set; }
[JsonProperty("channel_id")]
public ulong? MessageDeleteChannelId { get; set; }

//Prune
[JsonProperty("delete_member_days")]
public int? PruneDeleteMemberDays { get; set; }
[JsonProperty("members_removed")]
public int? PruneMembersRemoved { get; set; }

//Overwrite Update
[JsonProperty("role_name")]
public string OverwriteRoleName { get; set; }
[JsonProperty("type")]
public string OverwriteType { get; set; }
[JsonProperty("id")]
public ulong? OverwriteTargetId { get; set; }
}
}

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

+ 8
- 0
src/Discord.Net.Rest/API/Rest/GetAuditLogsParams.cs View File

@@ -0,0 +1,8 @@
namespace Discord.API.Rest
{
class GetAuditLogsParams
{
public Optional<int> Limit { get; set; }
public Optional<ulong> BeforeEntryId { 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; }
}
}

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

+ 33
- 9
src/Discord.Net.Rest/DiscordRestApiClient.cs View File

@@ -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)
@@ -1202,6 +1206,26 @@ namespace Discord.API
return await SendAsync<IReadOnlyCollection<VoiceRegion>>("GET", () => $"guilds/{guildId}/regions", ids, options: options).ConfigureAwait(false); return await SendAsync<IReadOnlyCollection<VoiceRegion>>("GET", () => $"guilds/{guildId}/regions", ids, options: options).ConfigureAwait(false);
} }


//Audit logs
public async Task<AuditLog> GetAuditLogsAsync(ulong guildId, GetAuditLogsParams args, RequestOptions options = null)
{
Preconditions.NotEqual(guildId, 0, nameof(guildId));
Preconditions.NotNull(args, nameof(args));
options = RequestOptions.CreateOrClone(options);

int limit = args.Limit.GetValueOrDefault(int.MaxValue);

var ids = new BucketIds(guildId: guildId);
Expression<Func<string>> endpoint;

if (args.BeforeEntryId.IsSpecified)
endpoint = () => $"guilds/{guildId}/audit-logs?limit={limit}&before={args.BeforeEntryId.Value}";
else
endpoint = () => $"guilds/{guildId}/audit-logs?limit={limit}";

return await SendAsync<AuditLog>("GET", endpoint, ids, options: options).ConfigureAwait(false);
}

//Webhooks //Webhooks
public async Task<Webhook> CreateWebhookAsync(ulong channelId, CreateWebhookParams args, RequestOptions options = null) public async Task<Webhook> CreateWebhookAsync(ulong channelId, CreateWebhookParams args, 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)
{ {


+ 58
- 0
src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs View File

@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
internal static class AuditLogHelper
{
private static readonly Dictionary<ActionType, Func<BaseDiscordClient, Model, EntryModel, IAuditLogData>> CreateMapping
= new Dictionary<ActionType, Func<BaseDiscordClient, Model, EntryModel, IAuditLogData>>()
{
[ActionType.GuildUpdated] = GuildUpdateAuditLogData.Create,

[ActionType.ChannelCreated] = ChannelCreateAuditLogData.Create,
[ActionType.ChannelUpdated] = ChannelUpdateAuditLogData.Create,
[ActionType.ChannelDeleted] = ChannelDeleteAuditLogData.Create,

[ActionType.OverwriteCreated] = OverwriteCreateAuditLogData.Create,
[ActionType.OverwriteUpdated] = OverwriteUpdateAuditLogData.Create,
[ActionType.OverwriteDeleted] = OverwriteDeleteAuditLogData.Create,

[ActionType.Kick] = KickAuditLogData.Create,
[ActionType.Prune] = PruneAuditLogData.Create,
[ActionType.Ban] = BanAuditLogData.Create,
[ActionType.Unban] = UnbanAuditLogData.Create,
[ActionType.MemberUpdated] = MemberUpdateAuditLogData.Create,
[ActionType.MemberRoleUpdated] = MemberRoleAuditLogData.Create,

[ActionType.RoleCreated] = RoleCreateAuditLogData.Create,
[ActionType.RoleUpdated] = RoleUpdateAuditLogData.Create,
[ActionType.RoleDeleted] = RoleDeleteAuditLogData.Create,

[ActionType.InviteCreated] = InviteCreateAuditLogData.Create,
[ActionType.InviteUpdated] = InviteUpdateAuditLogData.Create,
[ActionType.InviteDeleted] = InviteDeleteAuditLogData.Create,

[ActionType.WebhookCreated] = WebhookCreateAuditLogData.Create,
[ActionType.WebhookUpdated] = WebhookUpdateAuditLogData.Create,
[ActionType.WebhookDeleted] = WebhookDeleteAuditLogData.Create,

[ActionType.EmojiCreated] = EmoteCreateAuditLogData.Create,
[ActionType.EmojiUpdated] = EmoteUpdateAuditLogData.Create,
[ActionType.EmojiDeleted] = EmoteDeleteAuditLogData.Create,

[ActionType.MessageDeleted] = MessageDeleteAuditLogData.Create,
};

public static IAuditLogData CreateData(BaseDiscordClient discord, Model log, EntryModel entry)
{
if (CreateMapping.TryGetValue(entry.Action, out var func))
return func(discord, log, entry);

return null;
}
}
}

+ 23
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs View File

@@ -0,0 +1,23 @@
using System.Linq;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
public class BanAuditLogData : IAuditLogData
{
private BanAuditLogData(IUser user)
{
Target = user;
}

internal static BanAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId);
return new BanAuditLogData(RestUser.Create(discord, userInfo));
}

public IUser Target { get; }
}
}

+ 52
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs View File

@@ -0,0 +1,52 @@
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using System.Linq;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
public class ChannelCreateAuditLogData : IAuditLogData
{
private ChannelCreateAuditLogData(ulong id, string name, ChannelType type, IReadOnlyCollection<Overwrite> overwrites)
{
ChannelId = id;
ChannelName = name;
ChannelType = type;
Overwrites = overwrites;
}

internal static ChannelCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var changes = entry.Changes;
var overwrites = new List<Overwrite>();

var overwritesModel = changes.FirstOrDefault(x => x.ChangedProperty == "permission_overwrites");
var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type");
var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name");

var type = typeModel.NewValue.ToObject<ChannelType>();
var name = nameModel.NewValue.ToObject<string>();

foreach (var overwrite in overwritesModel.NewValue)
{
var deny = overwrite.Value<ulong>("deny");
var _type = overwrite.Value<string>("type");
var id = overwrite.Value<ulong>("id");
var allow = overwrite.Value<ulong>("allow");

PermissionTarget permType = _type == "member" ? PermissionTarget.User : PermissionTarget.Role;

overwrites.Add(new Overwrite(id, permType, new OverwritePermissions(allow, deny)));
}

return new ChannelCreateAuditLogData(entry.TargetId.Value, name, type, overwrites.ToReadOnlyCollection());
}

public ulong ChannelId { get; }
public string ChannelName { get; }
public ChannelType ChannelType { get; }
public IReadOnlyCollection<Overwrite> Overwrites { get; }
}
}

+ 45
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs View File

@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
public class ChannelDeleteAuditLogData : IAuditLogData
{
private ChannelDeleteAuditLogData(ulong id, string name, ChannelType type, IReadOnlyCollection<Overwrite> overwrites)
{
ChannelId = id;
ChannelName = name;
ChannelType = type;
Overwrites = overwrites;
}

internal static ChannelDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var changes = entry.Changes;

var overwritesModel = changes.FirstOrDefault(x => x.ChangedProperty == "permission_overwrites");
var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type");
var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name");

var overwrites = overwritesModel.OldValue.ToObject<API.Overwrite[]>()
.Select(x => new Overwrite(x.TargetId, x.TargetType, new OverwritePermissions(x.Allow, x.Deny)))
.ToList();
var type = typeModel.OldValue.ToObject<ChannelType>();
var name = nameModel.OldValue.ToObject<string>();
var id = entry.TargetId.Value;

return new ChannelDeleteAuditLogData(id, name, type, overwrites.ToReadOnlyCollection());
}

public ulong ChannelId { get; }
public string ChannelName { get; }
public ChannelType ChannelType { get; }
public IReadOnlyCollection<Overwrite> Overwrites { get; }
}
}

+ 18
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs View File

@@ -0,0 +1,18 @@
namespace Discord.Rest
{
public struct ChannelInfo
{
internal ChannelInfo(string name, string topic, int? bitrate, int? limit)
{
Name = name;
Topic = topic;
Bitrate = bitrate;
UserLimit = limit;
}

public string Name { get; }
public string Topic { get; }
public int? Bitrate { get; }
public int? UserLimit { get; }
}
}

+ 45
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelUpdateAuditLogData.cs View File

@@ -0,0 +1,45 @@
using System.Linq;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
public class ChannelUpdateAuditLogData : IAuditLogData
{
private ChannelUpdateAuditLogData(ulong id, ChannelInfo before, ChannelInfo after)
{
ChannelId = id;
Before = before;
After = after;
}

internal static ChannelUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var changes = entry.Changes;

var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name");
var topicModel = changes.FirstOrDefault(x => x.ChangedProperty == "topic");
var bitrateModel = changes.FirstOrDefault(x => x.ChangedProperty == "bitrate");
var userLimitModel = changes.FirstOrDefault(x => x.ChangedProperty == "user_limit");

string oldName = nameModel?.OldValue?.ToObject<string>(),
newName = nameModel?.NewValue?.ToObject<string>();
string oldTopic = topicModel?.OldValue?.ToObject<string>(),
newTopic = topicModel?.NewValue?.ToObject<string>();
int? oldBitrate = bitrateModel?.OldValue?.ToObject<int>(),
newBitrate = bitrateModel?.NewValue?.ToObject<int>();
int? oldLimit = userLimitModel?.OldValue?.ToObject<int>(),
newLimit = userLimitModel?.NewValue?.ToObject<int>();

var before = new ChannelInfo(oldName, oldTopic, oldBitrate, oldLimit);
var after = new ChannelInfo(newName, newTopic, newBitrate, newLimit);

return new ChannelUpdateAuditLogData(entry.TargetId.Value, before, after);
}

public ulong ChannelId { get; }
public ChannelInfo Before { get; set; }
public ChannelInfo After { get; set; }
}
}

+ 31
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteCreateAuditLogData.cs View File

@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
public class EmoteCreateAuditLogData : IAuditLogData
{
private EmoteCreateAuditLogData(ulong id, string name)
{
EmoteId = id;
Name = name;
}

internal static EmoteCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var change = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "name");

var emoteName = change.NewValue?.ToObject<string>();
return new EmoteCreateAuditLogData(entry.TargetId.Value, emoteName);
}

public ulong EmoteId { get; }
public string Name { get; }
}
}

+ 28
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteDeleteAuditLogData.cs View File

@@ -0,0 +1,28 @@
using System.Linq;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
public class EmoteDeleteAuditLogData : IAuditLogData
{
private EmoteDeleteAuditLogData(ulong id, string name)
{
EmoteId = id;
Name = name;
}

internal static EmoteDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var change = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "name");

var emoteName = change.OldValue?.ToObject<string>();

return new EmoteDeleteAuditLogData(entry.TargetId.Value, emoteName);
}

public ulong EmoteId { get; }
public string Name { get; }
}
}

+ 31
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteUpdateAuditLogData.cs View File

@@ -0,0 +1,31 @@
using System.Linq;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
public class EmoteUpdateAuditLogData : IAuditLogData
{
private EmoteUpdateAuditLogData(ulong id, string oldName, string newName)
{
EmoteId = id;
OldName = oldName;
NewName = newName;
}

internal static EmoteUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var change = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "name");

var newName = change.NewValue?.ToObject<string>();
var oldName = change.OldValue?.ToObject<string>();

return new EmoteUpdateAuditLogData(entry.TargetId.Value, oldName, newName);
}

public ulong EmoteId { get; }
public string NewName { get; }
public string OldName { get; }
}
}

+ 32
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildInfo.cs View File

@@ -0,0 +1,32 @@
namespace Discord.Rest
{
public struct GuildInfo
{
internal GuildInfo(int? afkTimeout, DefaultMessageNotifications? defaultNotifs,
ulong? afkChannel, string name, string region, string icon,
VerificationLevel? verification, IUser owner, MfaLevel? mfa, int? filter)
{
AfkTimeout = afkTimeout;
DefaultMessageNotifications = defaultNotifs;
AfkChannelId = afkChannel;
Name = name;
RegionId = region;
IconHash = icon;
VerificationLevel = verification;
Owner = owner;
MfaLevel = mfa;
ContentFilterLevel = filter;
}

public int? AfkTimeout { get; }
public DefaultMessageNotifications? DefaultMessageNotifications { get; }
public ulong? AfkChannelId { get; }
public string Name { get; }
public string RegionId { get; }
public string IconHash { get; }
public VerificationLevel? VerificationLevel { get; }
public IUser Owner { get; }
public MfaLevel? MfaLevel { get; }
public int? ContentFilterLevel { get; }
}
}

+ 79
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildUpdateAuditLogData.cs View File

@@ -0,0 +1,79 @@
using System.Linq;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
public class GuildUpdateAuditLogData : IAuditLogData
{
private GuildUpdateAuditLogData(GuildInfo before, GuildInfo after)
{
Before = before;
After = after;
}

internal static GuildUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var changes = entry.Changes;

var afkTimeoutModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_timeout");
var defaultMessageNotificationsModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_timeout");
var afkChannelModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_timeout");
var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_timeout");
var regionIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_timeout");
var iconHashModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_timeout");
var verificationLevelModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_timeout");
var ownerIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_timeout");
var mfaLevelModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_timeout");
var contentFilterModel = changes.FirstOrDefault(x => x.ChangedProperty == "afk_timeout");

int? oldAfkTimeout = afkTimeoutModel?.OldValue?.ToObject<int>(),
newAfkTimeout = afkTimeoutModel?.NewValue?.ToObject<int>();
DefaultMessageNotifications? oldDefaultMessageNotifications = defaultMessageNotificationsModel?.OldValue?.ToObject<DefaultMessageNotifications>(),
newDefaultMessageNotifications = defaultMessageNotificationsModel?.NewValue?.ToObject<DefaultMessageNotifications>();
ulong? oldAfkChannelId = afkChannelModel?.OldValue?.ToObject<ulong>(),
newAfkChannelId = afkChannelModel?.NewValue?.ToObject<ulong>();
string oldName = nameModel?.OldValue?.ToObject<string>(),
newName = nameModel?.NewValue?.ToObject<string>();
string oldRegionId = regionIdModel?.OldValue?.ToObject<string>(),
newRegionId = regionIdModel?.NewValue?.ToObject<string>();
string oldIconHash = iconHashModel?.OldValue?.ToObject<string>(),
newIconHash = iconHashModel?.NewValue?.ToObject<string>();
VerificationLevel? oldVerificationLevel = verificationLevelModel?.OldValue?.ToObject<VerificationLevel>(),
newVerificationLevel = verificationLevelModel?.NewValue?.ToObject<VerificationLevel>();
ulong? oldOwnerId = ownerIdModel?.OldValue?.ToObject<ulong>(),
newOwnerId = ownerIdModel?.NewValue?.ToObject<ulong>();
MfaLevel? oldMfaLevel = mfaLevelModel?.OldValue?.ToObject<MfaLevel>(),
newMfaLevel = mfaLevelModel?.NewValue?.ToObject<MfaLevel>();
int? oldContentFilter = contentFilterModel?.OldValue?.ToObject<int>(),
newContentFilter = contentFilterModel?.NewValue?.ToObject<int>();

IUser oldOwner = null;
if (oldOwnerId != null)
{
var oldOwnerInfo = log.Users.FirstOrDefault(x => x.Id == oldOwnerId.Value);
oldOwner = RestUser.Create(discord, oldOwnerInfo);
}

IUser newOwner = null;
if (newOwnerId != null)
{
var newOwnerInfo = log.Users.FirstOrDefault(x => x.Id == newOwnerId.Value);
newOwner = RestUser.Create(discord, newOwnerInfo);
}

var before = new GuildInfo(oldAfkTimeout, oldDefaultMessageNotifications,
oldAfkChannelId, oldName, oldRegionId, oldIconHash, oldVerificationLevel, oldOwner,
oldMfaLevel, oldContentFilter);
var after = new GuildInfo(newAfkTimeout, newDefaultMessageNotifications,
newAfkChannelId, newName, newRegionId, newIconHash, newVerificationLevel, newOwner,
newMfaLevel, newContentFilter);

return new GuildUpdateAuditLogData(before, after);
}

public GuildInfo Before { get; }
public GuildInfo After { get; }
}
}

+ 55
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs View File

@@ -0,0 +1,55 @@
using System.Linq;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
public class InviteCreateAuditLogData : IAuditLogData
{
private InviteCreateAuditLogData(int maxAge, string code, bool temporary, IUser inviter, ulong channelId, int uses, int maxUses)
{
MaxAge = maxAge;
Code = code;
Temporary = temporary;
Creator = inviter;
ChannelId = channelId;
Uses = uses;
MaxUses = maxUses;
}

internal static InviteCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var changes = entry.Changes;

var maxAgeModel = changes.FirstOrDefault(x => x.ChangedProperty == "max_age");
var codeModel = changes.FirstOrDefault(x => x.ChangedProperty == "code");
var temporaryModel = changes.FirstOrDefault(x => x.ChangedProperty == "temporary");
var inviterIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "inviter_id");
var channelIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "channel_id");
var usesModel = changes.FirstOrDefault(x => x.ChangedProperty == "uses");
var maxUsesModel = changes.FirstOrDefault(x => x.ChangedProperty == "max_uses");

var maxAge = maxAgeModel.NewValue.ToObject<int>();
var code = codeModel.NewValue.ToObject<string>();
var temporary = temporaryModel.NewValue.ToObject<bool>();
var inviterId = inviterIdModel.NewValue.ToObject<ulong>();
var channelId = channelIdModel.NewValue.ToObject<ulong>();
var uses = usesModel.NewValue.ToObject<int>();
var maxUses = maxUsesModel.NewValue.ToObject<int>();

var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId);
var inviter = RestUser.Create(discord, inviterInfo);

return new InviteCreateAuditLogData(maxAge, code, temporary, inviter, channelId, uses, maxUses);
}

public int MaxAge { get; }
public string Code { get; }
public bool Temporary { get; }
public IUser Creator { get; }
public ulong ChannelId { get; }
public int Uses { get; }
public int MaxUses { get; }
}
}

+ 55
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs View File

@@ -0,0 +1,55 @@
using System.Linq;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
public class InviteDeleteAuditLogData : IAuditLogData
{
private InviteDeleteAuditLogData(int maxAge, string code, bool temporary, IUser inviter, ulong channelId, int uses, int maxUses)
{
MaxAge = maxAge;
Code = code;
Temporary = temporary;
Creator = inviter;
ChannelId = channelId;
Uses = uses;
MaxUses = maxUses;
}

internal static InviteDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var changes = entry.Changes;

var maxAgeModel = changes.FirstOrDefault(x => x.ChangedProperty == "max_age");
var codeModel = changes.FirstOrDefault(x => x.ChangedProperty == "code");
var temporaryModel = changes.FirstOrDefault(x => x.ChangedProperty == "temporary");
var inviterIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "inviter_id");
var channelIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "channel_id");
var usesModel = changes.FirstOrDefault(x => x.ChangedProperty == "uses");
var maxUsesModel = changes.FirstOrDefault(x => x.ChangedProperty == "max_uses");

var maxAge = maxAgeModel.OldValue.ToObject<int>();
var code = codeModel.OldValue.ToObject<string>();
var temporary = temporaryModel.OldValue.ToObject<bool>();
var inviterId = inviterIdModel.OldValue.ToObject<ulong>();
var channelId = channelIdModel.OldValue.ToObject<ulong>();
var uses = usesModel.OldValue.ToObject<int>();
var maxUses = maxUsesModel.OldValue.ToObject<int>();

var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId);
var inviter = RestUser.Create(discord, inviterInfo);

return new InviteDeleteAuditLogData(maxAge, code, temporary, inviter, channelId, uses, maxUses);
}

public int MaxAge { get; }
public string Code { get; }
public bool Temporary { get; }
public IUser Creator { get; }
public ulong ChannelId { get; }
public int Uses { get; }
public int MaxUses { get; }
}
}

+ 20
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteInfo.cs View File

@@ -0,0 +1,20 @@
namespace Discord.Rest
{
public struct InviteInfo
{
internal InviteInfo(int? maxAge, string code, bool? temporary, ulong? channelId, int? maxUses)
{
MaxAge = maxAge;
Code = code;
Temporary = temporary;
ChannelId = channelId;
MaxUses = maxUses;
}

public int? MaxAge { get; }
public string Code { get; }
public bool? Temporary { get; }
public ulong? ChannelId { get; }
public int? MaxUses { get; }
}
}

+ 46
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteUpdateAuditLogData.cs View File

@@ -0,0 +1,46 @@
using System.Linq;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
public class InviteUpdateAuditLogData : IAuditLogData
{
private InviteUpdateAuditLogData(InviteInfo before, InviteInfo after)
{
Before = before;
After = after;
}

internal static InviteUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var changes = entry.Changes;

var maxAgeModel = changes.FirstOrDefault(x => x.ChangedProperty == "max_age");
var codeModel = changes.FirstOrDefault(x => x.ChangedProperty == "code");
var temporaryModel = changes.FirstOrDefault(x => x.ChangedProperty == "temporary");
var channelIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "channel_id");
var maxUsesModel = changes.FirstOrDefault(x => x.ChangedProperty == "max_uses");

int? oldMaxAge = maxAgeModel?.OldValue?.ToObject<int>(),
newMaxAge = maxAgeModel?.NewValue?.ToObject<int>();
string oldCode = codeModel?.OldValue?.ToObject<string>(),
newCode = codeModel?.NewValue?.ToObject<string>();
bool? oldTemporary = temporaryModel?.OldValue?.ToObject<bool>(),
newTemporary = temporaryModel?.NewValue?.ToObject<bool>();
ulong? oldChannelId = channelIdModel?.OldValue?.ToObject<ulong>(),
newChannelId = channelIdModel?.NewValue?.ToObject<ulong>();
int? oldMaxUses = maxUsesModel?.OldValue?.ToObject<int>(),
newMaxUses = maxUsesModel?.NewValue?.ToObject<int>();

var before = new InviteInfo(oldMaxAge, oldCode, oldTemporary, oldChannelId, oldMaxUses);
var after = new InviteInfo(newMaxAge, newCode, newTemporary, newChannelId, newMaxUses);

return new InviteUpdateAuditLogData(before, after);
}

public InviteInfo Before { get; }
public InviteInfo After { get; }
}
}

+ 23
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/KickAuditLogData.cs View File

@@ -0,0 +1,23 @@
using System.Linq;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
public class KickAuditLogData : IAuditLogData
{
private KickAuditLogData(RestUser user)
{
Target = user;
}

internal static KickAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId);
return new KickAuditLogData(RestUser.Create(discord, userInfo));
}

public IUser Target { get; }
}
}

+ 50
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs View File

@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
public class MemberRoleAuditLogData : IAuditLogData
{
private MemberRoleAuditLogData(IReadOnlyCollection<RoleInfo> roles, IUser target)
{
Roles = roles;
Target = target;
}

internal static MemberRoleAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var changes = entry.Changes;

var roleInfos = changes.SelectMany(x => x.NewValue.ToObject<API.Role[]>(),
(model, role) => new { model.ChangedProperty, Role = role })
.Select(x => new RoleInfo(x.Role.Name, x.Role.Id, x.ChangedProperty == "$add"))
.ToList();

var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId);
var user = RestUser.Create(discord, userInfo);

return new MemberRoleAuditLogData(roleInfos.ToReadOnlyCollection(), user);
}

public IReadOnlyCollection<RoleInfo> Roles { get; }
public IUser Target { get; }

public struct RoleInfo
{
internal RoleInfo(string name, ulong roleId, bool added)
{
Name = name;
RoleId = roleId;
Added = added;
}

public string Name { get; }
public ulong RoleId { get; }
public bool Added { get; }
}
}
}

+ 35
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs View File

@@ -0,0 +1,35 @@
using System.Linq;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;
using ChangeModel = Discord.API.AuditLogChange;

namespace Discord.Rest
{
public class MemberUpdateAuditLogData : IAuditLogData
{
private MemberUpdateAuditLogData(IUser target, string newNick, string oldNick)
{
Target = target;
NewNick = newNick;
OldNick = oldNick;
}

internal static MemberUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var changes = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "nick");

var newNick = changes.NewValue?.ToObject<string>();
var oldNick = changes.OldValue?.ToObject<string>();

var targetInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId);
var user = RestUser.Create(discord, targetInfo);

return new MemberUpdateAuditLogData(user, newNick, oldNick);
}

public IUser Target { get; }
public string NewNick { get; }
public string OldNick { get; }
}
}

+ 22
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs View File

@@ -0,0 +1,22 @@
using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
public class MessageDeleteAuditLogData : IAuditLogData
{
private MessageDeleteAuditLogData(ulong channelId, int count)
{
ChannelId = channelId;
MessageCount = count;
}

internal static MessageDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
return new MessageDeleteAuditLogData(entry.Options.MessageDeleteChannelId.Value, entry.Options.MessageDeleteCount.Value);
}

public int MessageCount { get; }
public ulong ChannelId { get; }
}
}

+ 37
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteCreateAuditLogData.cs View File

@@ -0,0 +1,37 @@
using System.Linq;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
public class OverwriteCreateAuditLogData : IAuditLogData
{
private OverwriteCreateAuditLogData(Overwrite overwrite)
{
Overwrite = overwrite;
}

internal static OverwriteCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var changes = entry.Changes;

var denyModel = changes.FirstOrDefault(x => x.ChangedProperty == "deny");
var allowModel = changes.FirstOrDefault(x => x.ChangedProperty == "allow");

var deny = denyModel.NewValue.ToObject<ulong>();
var allow = allowModel.NewValue.ToObject<ulong>();

var permissions = new OverwritePermissions(allow, deny);

var id = entry.Options.OverwriteTargetId.Value;
var type = entry.Options.OverwriteType;

PermissionTarget target = type == "member" ? PermissionTarget.User : PermissionTarget.Role;

return new OverwriteCreateAuditLogData(new Overwrite(id, target, permissions));
}

public Overwrite Overwrite { get; }
}
}

+ 42
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs View File

@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;
using ChangeModel = Discord.API.AuditLogChange;
using OptionModel = Discord.API.AuditLogOptions;

namespace Discord.Rest
{
public class OverwriteDeleteAuditLogData : IAuditLogData
{
private OverwriteDeleteAuditLogData(Overwrite deletedOverwrite)
{
Overwrite = deletedOverwrite;
}

internal static OverwriteDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var changes = entry.Changes;

var denyModel = changes.FirstOrDefault(x => x.ChangedProperty == "deny");
var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type");
var idModel = changes.FirstOrDefault(x => x.ChangedProperty == "id");
var allowModel = changes.FirstOrDefault(x => x.ChangedProperty == "allow");

var deny = denyModel.OldValue.ToObject<ulong>();
var type = typeModel.OldValue.ToObject<string>();
var id = idModel.OldValue.ToObject<ulong>();
var allow = allowModel.OldValue.ToObject<ulong>();

PermissionTarget target = type == "member" ? PermissionTarget.User : PermissionTarget.Role;

return new OverwriteDeleteAuditLogData(new Overwrite(id, target, new OverwritePermissions(allow, deny)));
}

public Overwrite Overwrite { get; }
}
}

+ 44
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteUpdateAuditLogData.cs View File

@@ -0,0 +1,44 @@
using System.Linq;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
public class OverwriteUpdateAuditLogData : IAuditLogData
{
private OverwriteUpdateAuditLogData(OverwritePermissions before, OverwritePermissions after, ulong targetId, PermissionTarget targetType)
{
OldPermissions = before;
NewPermissions = after;
OverwriteTargetId = targetId;
OverwriteType = targetType;
}

internal static OverwriteUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var changes = entry.Changes;

var denyModel = changes.FirstOrDefault(x => x.ChangedProperty == "deny");
var allowModel = changes.FirstOrDefault(x => x.ChangedProperty == "allow");

var beforeAllow = allowModel?.OldValue?.ToObject<ulong>();
var afterAllow = allowModel?.NewValue?.ToObject<ulong>();
var beforeDeny = denyModel?.OldValue?.ToObject<ulong>();
var afterDeny = denyModel?.OldValue?.ToObject<ulong>();

var beforePermissions = new OverwritePermissions(beforeAllow ?? 0, beforeDeny ?? 0);
var afterPermissions = new OverwritePermissions(afterAllow ?? 0, afterDeny ?? 0);

PermissionTarget target = entry.Options.OverwriteType == "member" ? PermissionTarget.User : PermissionTarget.Role;

return new OverwriteUpdateAuditLogData(beforePermissions, afterPermissions, entry.Options.OverwriteTargetId.Value, target);
}

public OverwritePermissions OldPermissions { get; }
public OverwritePermissions NewPermissions { get; }

public ulong OverwriteTargetId { get; }
public PermissionTarget OverwriteType { get; }
}
}

+ 22
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/PruneAuditLogData.cs View File

@@ -0,0 +1,22 @@
using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
public class PruneAuditLogData : IAuditLogData
{
private PruneAuditLogData(int pruneDays, int membersRemoved)
{
PruneDays = pruneDays;
MembersRemoved = membersRemoved;
}

internal static PruneAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
return new PruneAuditLogData(entry.Options.PruneDeleteMemberDays.Value, entry.Options.PruneMembersRemoved.Value);
}

public int PruneDays { get; }
public int MembersRemoved { get; }
}
}

+ 47
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleCreateAuditLogData.cs View File

@@ -0,0 +1,47 @@
using System.Linq;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
public class RoleCreateAuditLogData : IAuditLogData
{
private RoleCreateAuditLogData(ulong id, RoleInfo props)
{
RoleId = id;
Properties = props;
}

internal static RoleCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var changes = entry.Changes;

var colorModel = changes.FirstOrDefault(x => x.ChangedProperty == "color");
var mentionableModel = changes.FirstOrDefault(x => x.ChangedProperty == "mentionable");
var hoistModel = changes.FirstOrDefault(x => x.ChangedProperty == "hoist");
var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name");
var permissionsModel = changes.FirstOrDefault(x => x.ChangedProperty == "permissions");

uint? colorRaw = colorModel?.NewValue?.ToObject<uint>();
bool? mentionable = mentionableModel?.NewValue?.ToObject<bool>();
bool? hoist = hoistModel?.NewValue?.ToObject<bool>();
string name = nameModel?.NewValue?.ToObject<string>();
ulong? permissionsRaw = permissionsModel?.NewValue?.ToObject<ulong>();

Color? color = null;
GuildPermissions? permissions = null;

if (colorRaw.HasValue)
color = new Color(colorRaw.Value);
if (permissionsRaw.HasValue)
permissions = new GuildPermissions(permissionsRaw.Value);

return new RoleCreateAuditLogData(entry.TargetId.Value,
new RoleInfo(color, mentionable, hoist, name, permissions));
}

public ulong RoleId { get; }
public RoleInfo Properties { get; }
}
}

+ 47
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleDeleteAuditLogData.cs View File

@@ -0,0 +1,47 @@
using System.Linq;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
public class RoleDeleteAuditLogData : IAuditLogData
{
private RoleDeleteAuditLogData(ulong id, RoleInfo props)
{
RoleId = id;
Properties = props;
}

internal static RoleDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var changes = entry.Changes;

var colorModel = changes.FirstOrDefault(x => x.ChangedProperty == "color");
var mentionableModel = changes.FirstOrDefault(x => x.ChangedProperty == "mentionable");
var hoistModel = changes.FirstOrDefault(x => x.ChangedProperty == "hoist");
var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name");
var permissionsModel = changes.FirstOrDefault(x => x.ChangedProperty == "permissions");

uint? colorRaw = colorModel?.OldValue?.ToObject<uint>();
bool? mentionable = mentionableModel?.OldValue?.ToObject<bool>();
bool? hoist = hoistModel?.OldValue?.ToObject<bool>();
string name = nameModel?.OldValue?.ToObject<string>();
ulong? permissionsRaw = permissionsModel?.OldValue?.ToObject<ulong>();

Color? color = null;
GuildPermissions? permissions = null;

if (colorRaw.HasValue)
color = new Color(colorRaw.Value);
if (permissionsRaw.HasValue)
permissions = new GuildPermissions(permissionsRaw.Value);

return new RoleDeleteAuditLogData(entry.TargetId.Value,
new RoleInfo(color, mentionable, hoist, name, permissions));
}

public ulong RoleId { get; }
public RoleInfo Properties { get; }
}
}

+ 21
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleInfo.cs View File

@@ -0,0 +1,21 @@
namespace Discord.Rest
{
public struct RoleInfo
{
internal RoleInfo(Color? color, bool? mentionable, bool? hoist, string name,
GuildPermissions? permissions)
{
Color = color;
Mentionable = mentionable;
Hoist = hoist;
Name = name;
Permissions = permissions;
}

public Color? Color { get; }
public bool? Mentionable { get; }
public bool? Hoist { get; }
public string Name { get; }
public GuildPermissions? Permissions { get; }
}
}

+ 62
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleUpdateAuditLogData.cs View File

@@ -0,0 +1,62 @@
using System.Linq;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
public class RoleUpdateAuditLogData : IAuditLogData
{
private RoleUpdateAuditLogData(ulong id, RoleInfo oldProps, RoleInfo newProps)
{
RoleId = id;
Before = oldProps;
After = newProps;
}

internal static RoleUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var changes = entry.Changes;

var colorModel = changes.FirstOrDefault(x => x.ChangedProperty == "color");
var mentionableModel = changes.FirstOrDefault(x => x.ChangedProperty == "mentionable");
var hoistModel = changes.FirstOrDefault(x => x.ChangedProperty == "hoist");
var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name");
var permissionsModel = changes.FirstOrDefault(x => x.ChangedProperty == "permissions");

uint? oldColorRaw = colorModel?.OldValue?.ToObject<uint>(),
newColorRaw = colorModel?.NewValue?.ToObject<uint>();
bool? oldMentionable = mentionableModel?.OldValue?.ToObject<bool>(),
newMentionable = mentionableModel?.NewValue?.ToObject<bool>();
bool? oldHoist = hoistModel?.OldValue?.ToObject<bool>(),
newHoist = hoistModel?.NewValue?.ToObject<bool>();
string oldName = nameModel?.OldValue?.ToObject<string>(),
newName = nameModel?.NewValue?.ToObject<string>();
ulong? oldPermissionsRaw = permissionsModel?.OldValue?.ToObject<ulong>(),
newPermissionsRaw = permissionsModel?.OldValue?.ToObject<ulong>();

Color? oldColor = null,
newColor = null;
GuildPermissions? oldPermissions = null,
newPermissions = null;

if (oldColorRaw.HasValue)
oldColor = new Color(oldColorRaw.Value);
if (newColorRaw.HasValue)
newColor = new Color(newColorRaw.Value);
if (oldPermissionsRaw.HasValue)
oldPermissions = new GuildPermissions(oldPermissionsRaw.Value);
if (newPermissionsRaw.HasValue)
newPermissions = new GuildPermissions(newPermissionsRaw.Value);

var oldProps = new RoleInfo(oldColor, oldMentionable, oldHoist, oldName, oldPermissions);
var newProps = new RoleInfo(newColor, newMentionable, newHoist, newName, newPermissions);

return new RoleUpdateAuditLogData(entry.TargetId.Value, oldProps, newProps);
}

public ulong RoleId { get; }
public RoleInfo Before { get; }
public RoleInfo After { get; }
}
}

+ 23
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/UnbanAuditLogData.cs View File

@@ -0,0 +1,23 @@
using System.Linq;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
public class UnbanAuditLogData : IAuditLogData
{
private UnbanAuditLogData(IUser user)
{
Target = user;
}

internal static UnbanAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId);
return new UnbanAuditLogData(RestUser.Create(discord, userInfo));
}

public IUser Target { get; }
}
}

+ 44
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookCreateAuditLogData.cs View File

@@ -0,0 +1,44 @@
using System.Linq;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
public class WebhookCreateAuditLogData : IAuditLogData
{
private WebhookCreateAuditLogData(IWebhook webhook, WebhookType type, string name, ulong channelId)
{
Webhook = webhook;
Name = name;
Type = type;
ChannelId = channelId;
}

internal static WebhookCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var changes = entry.Changes;

var channelIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "channel_id");
var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type");
var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name");

var channelId = channelIdModel.NewValue.ToObject<ulong>();
var type = typeModel.NewValue.ToObject<WebhookType>();
var name = nameModel.NewValue.ToObject<string>();

var webhookInfo = log.Webhooks?.FirstOrDefault(x => x.Id == entry.TargetId);
var webhook = RestWebhook.Create(discord, (IGuild)null, webhookInfo);

return new WebhookCreateAuditLogData(webhook, type, name, channelId);
}

//Corresponds to the *current* data
public IWebhook Webhook { get; }

//Corresponds to the *audit log* data
public WebhookType Type { get; }
public string Name { get; }
public ulong ChannelId { get; }
}
}

+ 46
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookDeleteAuditLogData.cs View File

@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
public class WebhookDeleteAuditLogData : IAuditLogData
{
private WebhookDeleteAuditLogData(ulong id, ulong channel, WebhookType type, string name, string avatar)
{
WebhookId = id;
ChannelId = channel;
Name = name;
Type = type;
Avatar = avatar;
}

internal static WebhookDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var changes = entry.Changes;

var channelIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "channel_id");
var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type");
var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name");
var avatarHashModel = changes.FirstOrDefault(x => x.ChangedProperty == "avatar_hash");

var channelId = channelIdModel.OldValue.ToObject<ulong>();
var type = typeModel.OldValue.ToObject<WebhookType>();
var name = nameModel.OldValue.ToObject<string>();
var avatarHash = avatarHashModel?.OldValue?.ToObject<string>();

return new WebhookDeleteAuditLogData(entry.TargetId.Value, channelId, type, name, avatarHash);
}

public ulong WebhookId { get; }
public ulong ChannelId { get; }
public WebhookType Type { get; }
public string Name { get; }
public string Avatar { get; }
}
}

+ 16
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookInfo.cs View File

@@ -0,0 +1,16 @@
namespace Discord.Rest
{
public struct WebhookInfo
{
internal WebhookInfo(string name, ulong? channelId, string avatar)
{
Name = name;
ChannelId = channelId;
Avatar = avatar;
}

public string Name { get; }
public ulong? ChannelId { get; }
public string Avatar { get; }
}
}

+ 52
- 0
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookUpdateAuditLogData.cs View File

@@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
public class WebhookUpdateAuditLogData : IAuditLogData
{
private WebhookUpdateAuditLogData(IWebhook webhook, WebhookInfo before, WebhookInfo after)
{
Webhook = webhook;
Before = before;
After = after;
}

internal static WebhookUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
var changes = entry.Changes;

var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name");
var channelIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "channel_id");
var avatarHashModel = changes.FirstOrDefault(x => x.ChangedProperty == "avatar_hash");

var oldName = nameModel?.OldValue?.ToObject<string>();
var oldChannelId = channelIdModel?.OldValue?.ToObject<ulong>();
var oldAvatar = avatarHashModel?.OldValue?.ToObject<string>();
var before = new WebhookInfo(oldName, oldChannelId, oldAvatar);

var newName = nameModel?.NewValue?.ToObject<string>();
var newChannelId = channelIdModel?.NewValue?.ToObject<ulong>();
var newAvatar = avatarHashModel?.NewValue?.ToObject<string>();
var after = new WebhookInfo(newName, newChannelId, newAvatar);

var webhookInfo = log.Webhooks?.FirstOrDefault(x => x.Id == entry.TargetId);
var webhook = RestWebhook.Create(discord, (IGuild)null, webhookInfo);

return new WebhookUpdateAuditLogData(webhook, before, after);
}

//Again, the *current* data
public IWebhook Webhook { get; }

//And the *audit log* data
public WebhookInfo Before { get; }
public WebhookInfo After { get; }
}
}

+ 38
- 0
src/Discord.Net.Rest/Entities/AuditLogs/RestAuditLogEntry.cs View File

@@ -0,0 +1,38 @@
using System.Linq;

using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
{
public class RestAuditLogEntry : RestEntity<ulong>, IAuditLogEntry
{
private RestAuditLogEntry(BaseDiscordClient discord, Model fullLog, EntryModel model, IUser user)
: base(discord, model.Id)
{
Action = model.Action;
Data = AuditLogHelper.CreateData(discord, fullLog, model);
User = user;
Reason = model.Reason;
}

internal static RestAuditLogEntry Create(BaseDiscordClient discord, Model fullLog, EntryModel model)
{
var userInfo = fullLog.Users.FirstOrDefault(x => x.Id == model.UserId);
IUser user = null;
if (userInfo != null)
user = RestUser.Create(discord, userInfo);

return new RestAuditLogEntry(discord, fullLog, model, user);
}

/// <inheritdoc/>
public ActionType Action { get; }
/// <inheritdoc/>
public IAuditLogData Data { get; }
/// <inheritdoc/>
public IUser User { get; }
/// <inheritdoc/>
public string Reason { get; }
}
}

+ 35
- 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)
@@ -263,6 +268,35 @@ namespace Discord.Rest
return model.Pruned; return model.Pruned;
} }


// Audit logs
public static IAsyncEnumerable<IReadOnlyCollection<RestAuditLogEntry>> GetAuditLogsAsync(IGuild guild, BaseDiscordClient client,
ulong? from, int? limit, RequestOptions options)
{
return new PagedAsyncEnumerable<RestAuditLogEntry>(
DiscordConfig.MaxAuditLogEntriesPerBatch,
async (info, ct) =>
{
var args = new GetAuditLogsParams
{
Limit = info.PageSize
};
if (info.Position != null)
args.BeforeEntryId = info.Position.Value;
var model = await client.ApiClient.GetAuditLogsAsync(guild.Id, args, options);
return model.Entries.Select((x) => RestAuditLogEntry.Create(client, model, x)).ToImmutableArray();
},
nextPage: (info, lastPage) =>
{
if (lastPage.Count != DiscordConfig.MaxAuditLogEntriesPerBatch)
return false;
info.Position = lastPage.Min(x => x.Id);
return true;
},
start: from,
count: limit
);
}

//Webhooks //Webhooks
public static async Task<RestWebhook> GetWebhookAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options) public static async Task<RestWebhook> GetWebhookAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options)
{ {


+ 23
- 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);
@@ -264,6 +268,10 @@ namespace Discord.Rest
public Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null) public Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null)
=> GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options); => GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options);


//Audit logs
public IAsyncEnumerable<IReadOnlyCollection<RestAuditLogEntry>> GetAuditLogsAsync(int limit, RequestOptions options = null)
=> GuildHelper.GetAuditLogsAsync(this, Discord, null, limit, options);

//Webhooks //Webhooks
public Task<RestWebhook> GetWebhookAsync(ulong id, RequestOptions options = null) public Task<RestWebhook> GetWebhookAsync(ulong id, RequestOptions options = null)
=> GuildHelper.GetWebhookAsync(this, Discord, id, options); => GuildHelper.GetWebhookAsync(this, Discord, id, options);
@@ -291,6 +299,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)
{ {
@@ -419,6 +433,14 @@ namespace Discord.Rest
} }
Task IGuild.DownloadUsersAsync() { throw new NotSupportedException(); } Task IGuild.DownloadUsersAsync() { throw new NotSupportedException(); }


async Task<IReadOnlyCollection<IAuditLogEntry>> IGuild.GetAuditLogAsync(int limit, CacheMode cacheMode, RequestOptions options)
{
if (cacheMode == CacheMode.AllowDownload)
return (await GetAuditLogsAsync(limit, options).FlattenAsync().ConfigureAwait(false)).ToImmutableArray();
else
return ImmutableArray.Create<IAuditLogEntry>();
}

async Task<IWebhook> IGuild.GetWebhookAsync(ulong id, RequestOptions options) async Task<IWebhook> IGuild.GetWebhookAsync(ulong id, RequestOptions options)
=> await GetWebhookAsync(id, options); => await GetWebhookAsync(id, options);
async Task<IReadOnlyCollection<IWebhook>> IGuild.GetWebhooksAsync(RequestOptions options) async Task<IReadOnlyCollection<IWebhook>> IGuild.GetWebhooksAsync(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})";


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


+ 22
- 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);
@@ -434,6 +438,10 @@ namespace Discord.WebSocket
_downloaderPromise.TrySetResultAsync(true); _downloaderPromise.TrySetResultAsync(true);
} }


//Audit logs
public IAsyncEnumerable<IReadOnlyCollection<RestAuditLogEntry>> GetAuditLogsAsync(int limit, RequestOptions options = null)
=> GuildHelper.GetAuditLogsAsync(this, Discord, null, limit, options);

//Webhooks //Webhooks
public Task<RestWebhook> GetWebhookAsync(ulong id, RequestOptions options = null) public Task<RestWebhook> GetWebhookAsync(ulong id, RequestOptions options = null)
=> GuildHelper.GetWebhookAsync(this, Discord, id, options); => GuildHelper.GetWebhookAsync(this, Discord, id, options);
@@ -641,6 +649,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);
@@ -693,6 +707,14 @@ namespace Discord.WebSocket
Task<IGuildUser> IGuild.GetOwnerAsync(CacheMode mode, RequestOptions options) Task<IGuildUser> IGuild.GetOwnerAsync(CacheMode mode, RequestOptions options)
=> Task.FromResult<IGuildUser>(Owner); => Task.FromResult<IGuildUser>(Owner);


async Task<IReadOnlyCollection<IAuditLogEntry>> IGuild.GetAuditLogAsync(int limit, CacheMode cacheMode, RequestOptions options)
{
if (cacheMode == CacheMode.AllowDownload)
return (await GetAuditLogsAsync(limit, options).FlattenAsync().ConfigureAwait(false)).ToImmutableArray();
else
return ImmutableArray.Create<IAuditLogEntry>();
}

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


+ 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