Browse Source

Merge branch 'discord-net:dev' into customId-segments-context

pull/2136/head
Cenk Ergen GitHub 3 years ago
parent
commit
4a245ac307
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
80 changed files with 1609 additions and 703 deletions
  1. +3
    -0
      .gitmodules
  2. +17
    -3
      CHANGELOG.md
  3. +16
    -1
      Discord.Net.sln
  4. +1
    -1
      Discord.Net.targets
  5. +1
    -0
      azure/deploy.yml
  6. +1
    -1
      docs/docfx.json
  7. +41
    -0
      docs/faq/build_overrides/what-are-they.md
  8. +2
    -0
      docs/faq/toc.yml
  9. +1
    -1
      docs/guides/int_framework/samples/intro/modal.cs
  10. +1
    -1
      docs/guides/int_framework/samples/postexecution/error_review.cs
  11. +2
    -2
      docs/guides/other_libs/samples/ModifyLogMethod.cs
  12. +278
    -0
      experiment/Discord.Net.BuildOverrides/BuildOverrides.cs
  13. +20
    -0
      experiment/Discord.Net.BuildOverrides/Discord.Net.BuildOverrides.csproj
  14. +34
    -0
      experiment/Discord.Net.BuildOverrides/IOverride.cs
  15. +30
    -0
      experiment/Discord.Net.BuildOverrides/OverrideContext.cs
  16. +1
    -0
      overrides/Discord.Net.BuildOverrides
  17. +0
    -152
      samples/InteractionFramework/CommandHandler.cs
  18. +0
    -0
      samples/InteractionFramework/Enums/ExampleEnum.cs
  19. +81
    -0
      samples/InteractionFramework/InteractionHandler.cs
  20. +0
    -18
      samples/InteractionFramework/Modules/ComponentModule.cs
  21. +48
    -36
      samples/InteractionFramework/Modules/ExampleModule.cs
  22. +0
    -30
      samples/InteractionFramework/Modules/MessageCommandModule.cs
  23. +0
    -51
      samples/InteractionFramework/Modules/SlashCommandModule.cs
  24. +0
    -17
      samples/InteractionFramework/Modules/UserCommandModule.cs
  25. +33
    -42
      samples/InteractionFramework/Program.cs
  26. +1
    -1
      samples/ShardedClient/Modules/InteractionModule.cs
  27. +5
    -2
      samples/ShardedClient/Program.cs
  28. +2
    -2
      samples/ShardedClient/Services/InteractionHandlingService.cs
  29. +35
    -0
      src/Discord.Net.Commands/CommandService.cs
  30. +10
    -0
      src/Discord.Net.Core/DiscordConfig.cs
  31. +0
    -21
      src/Discord.Net.Core/Entities/Guilds/GuildIntegrationProperties.cs
  32. +19
    -2
      src/Discord.Net.Core/Entities/Guilds/IGuild.cs
  33. +0
    -18
      src/Discord.Net.Core/Entities/Guilds/IntegrationAccount.cs
  34. +35
    -12
      src/Discord.Net.Core/Entities/Integrations/IIntegration.cs
  35. +23
    -0
      src/Discord.Net.Core/Entities/Integrations/IIntegrationAccount.cs
  36. +33
    -0
      src/Discord.Net.Core/Entities/Integrations/IIntegrationApplication.cs
  37. +17
    -0
      src/Discord.Net.Core/Entities/Integrations/IntegrationExpireBehavior.cs
  38. +20
    -1
      src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteraction.cs
  39. +16
    -0
      src/Discord.Net.Core/Entities/Messages/FileAttachment.cs
  40. +8
    -0
      src/Discord.Net.Core/Entities/Messages/IAttachment.cs
  41. +1
    -1
      src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs
  42. +17
    -0
      src/Discord.Net.Core/Entities/Users/ConnectionVisibility.cs
  43. +44
    -15
      src/Discord.Net.Core/Entities/Users/IConnection.cs
  44. +6
    -3
      src/Discord.Net.Core/Format.cs
  45. +5
    -1
      src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs
  46. +25
    -0
      src/Discord.Net.Interactions/Extensions/RestExtensions.cs
  47. +1
    -1
      src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs
  48. +75
    -0
      src/Discord.Net.Interactions/InteractionService.cs
  49. +12
    -0
      src/Discord.Net.Interactions/Map/TypeMap.cs
  50. +34
    -0
      src/Discord.Net.Interactions/RestInteractionModuleBase.cs
  51. +21
    -0
      src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs
  52. +13
    -5
      src/Discord.Net.Rest/API/Common/Connection.cs
  53. +18
    -7
      src/Discord.Net.Rest/API/Common/Integration.cs
  54. +1
    -1
      src/Discord.Net.Rest/API/Common/IntegrationAccount.cs
  55. +20
    -0
      src/Discord.Net.Rest/API/Common/IntegrationApplication.cs
  56. +1
    -1
      src/Discord.Net.Rest/API/Common/ThreadMetadata.cs
  57. +2
    -0
      src/Discord.Net.Rest/BaseDiscordClient.cs
  58. +1
    -1
      src/Discord.Net.Rest/ClientHelper.cs
  59. +3
    -36
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  60. +6
    -10
      src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs
  61. +6
    -11
      src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
  62. +0
    -104
      src/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs
  63. +102
    -0
      src/Discord.Net.Rest/Entities/Integrations/RestIntegration.cs
  64. +29
    -0
      src/Discord.Net.Rest/Entities/Integrations/RestIntegrationAccount.cs
  65. +39
    -0
      src/Discord.Net.Rest/Entities/Integrations/RestIntegrationApplication.cs
  66. +8
    -0
      src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs
  67. +10
    -2
      src/Discord.Net.Rest/Entities/Messages/Attachment.cs
  68. +38
    -15
      src/Discord.Net.Rest/Entities/Users/RestConnection.cs
  69. +4
    -2
      src/Discord.Net.Rest/Entities/Users/RestUser.cs
  70. +14
    -0
      src/Discord.Net.WebSocket/API/Gateway/IntegrationDeletedEvent.cs
  71. +27
    -1
      src/Discord.Net.WebSocket/BaseSocketClient.Events.cs
  72. +1
    -1
      src/Discord.Net.WebSocket/DiscordShardedClient.cs
  73. +94
    -4
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  74. +28
    -3
      src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs
  75. +6
    -10
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
  76. +2
    -14
      src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs
  77. +16
    -6
      src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs
  78. +11
    -0
      src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs
  79. +2
    -2
      src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs
  80. +31
    -31
      src/Discord.Net/Discord.Net.nuspec

+ 3
- 0
.gitmodules View File

@@ -0,0 +1,3 @@
[submodule "overrides/Discord.Net.BuildOverrides"]
path = overrides/Discord.Net.BuildOverrides
url = https://github.com/discord-net/Discord.Net.BuildOverrides

+ 17
- 3
CHANGELOG.md View File

@@ -1,8 +1,22 @@
# Changelog

## [3.4.1] - 2022-03-9

### Added
- #2169 Component TypeConverters and CustomID TypeReaders (fb4250b)
- #2180 Attachment description and content type (765c0c5)
- #2162 Add configuration toggle to suppress Unknown dispatch warnings (1ba96d6)
- #2178 Add 10065 Error code (cc6918d)

### Fixed
- #2179 Logging out sharded client throws (24b7bb5)
- #2182 Thread owner always returns null (25aaa49)
- #2165 Fix error with flag params when uploading files. (a5d3add)
- #2181 Fix ambiguous reference for creating roles (f8ec3c7)

## [3.4.0] - 2022-3-2

## Added
### Added
- #2146 Add FromDateTimeOffset in TimestampTag (553055b)
- #2062 Add return statement to precondition handling (3e52fab)
- #2131 Add support for sending Message Flags (1fb62de)
@@ -13,13 +27,13 @@
- #2155 Add Interaction Service Complex Parameters (9ba64f6)
- #2156 Add Display name support for enum type converter (c800674)

## Fixed
### Fixed
- #2117 Fix stream access exception when ratelimited (a1cfa41)
- #2128 Fix context menu comand message type (f601e9b)
- #2135 Fix NRE when ratelimmited requests don't return a body (b95b942)
- #2154 Fix usage of CacheMode.AllowDownload in channels (b3370c3)

## Misc
### Misc
- #2149 Clarify Users property on SocketGuildChannel (5594739)
- #2157 Enforce valid button styles (507a18d)



+ 16
- 1
Discord.Net.sln View File

@@ -34,7 +34,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_InteractionFramework", "sa
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_WebhookClient", "samples\WebhookClient\_WebhookClient.csproj", "{B61AAE66-15CC-40E4-873A-C23E697C3411}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IDN", "samples\idn\idn.csproj", "{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "idn", "samples\idn\idn.csproj", "{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{C7CF5621-7D36-433B-B337-5A2E3C101A71}"
EndProject
@@ -44,6 +44,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions",
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.BuildOverrides", "experiment\Discord.Net.BuildOverrides\Discord.Net.BuildOverrides.csproj", "{115F4921-B44D-4F69-996B-69796959C99D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -258,6 +260,18 @@ Global
{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x64.Build.0 = Release|Any CPU
{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x86.ActiveCfg = Release|Any CPU
{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x86.Build.0 = Release|Any CPU
{115F4921-B44D-4F69-996B-69796959C99D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{115F4921-B44D-4F69-996B-69796959C99D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{115F4921-B44D-4F69-996B-69796959C99D}.Debug|x64.ActiveCfg = Debug|Any CPU
{115F4921-B44D-4F69-996B-69796959C99D}.Debug|x64.Build.0 = Debug|Any CPU
{115F4921-B44D-4F69-996B-69796959C99D}.Debug|x86.ActiveCfg = Debug|Any CPU
{115F4921-B44D-4F69-996B-69796959C99D}.Debug|x86.Build.0 = Debug|Any CPU
{115F4921-B44D-4F69-996B-69796959C99D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{115F4921-B44D-4F69-996B-69796959C99D}.Release|Any CPU.Build.0 = Release|Any CPU
{115F4921-B44D-4F69-996B-69796959C99D}.Release|x64.ActiveCfg = Release|Any CPU
{115F4921-B44D-4F69-996B-69796959C99D}.Release|x64.Build.0 = Release|Any CPU
{115F4921-B44D-4F69-996B-69796959C99D}.Release|x86.ActiveCfg = Release|Any CPU
{115F4921-B44D-4F69-996B-69796959C99D}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -279,6 +293,7 @@ Global
{A23E46D2-1610-4AE5-820F-422D34810887} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}
{B61AAE66-15CC-40E4-873A-C23E697C3411} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}
{4A03840B-9EBE-47E3-89AB-E0914DF21AFB} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}
{115F4921-B44D-4F69-996B-69796959C99D} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D2404771-EEC8-45F2-9D71-F3373F6C1495}


+ 1
- 1
Discord.Net.targets View File

@@ -1,6 +1,6 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VersionPrefix>3.4.0</VersionPrefix>
<VersionPrefix>3.4.1</VersionPrefix>
<LangVersion>latest</LangVersion>
<Authors>Discord.Net Contributors</Authors>
<PackageTags>discord;discordapp</PackageTags>


+ 1
- 0
azure/deploy.yml View File

@@ -8,6 +8,7 @@ steps:
dotnet pack "src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "$(Build.ArtifactStagingDirectory)" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag)
dotnet pack "src\Discord.Net.Analyzers\Discord.Net.Analyzers.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "$(Build.ArtifactStagingDirectory)" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag)
dotnet pack "src\Discord.Net.Interactions\Discord.Net.Interactions.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "$(Build.ArtifactStagingDirectory)" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag)
dotnet pack "experiment\Discord.Net.BuildOverrides\Discord.Net.BuildOverrides.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "$(Build.ArtifactStagingDirectory)" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag)
displayName: Pack projects

- task: NuGetCommand@2


+ 1
- 1
docs/docfx.json View File

@@ -60,7 +60,7 @@
"overwrite": "_overwrites/**/**.md",
"globalMetadata": {
"_appTitle": "Discord.Net Documentation",
"_appFooter": "Discord.Net (c) 2015-2022 3.4.0",
"_appFooter": "Discord.Net (c) 2015-2022 3.4.1",
"_enableSearch": true,
"_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg",
"_appFaviconPath": "favicon.ico"


+ 41
- 0
docs/faq/build_overrides/what-are-they.md View File

@@ -0,0 +1,41 @@
---
uid: FAQ.BuildOverrides.WhatAreThey
title: Build Overrides, What are they?
---

# Build Overrides

Build overrides are a way for library developers to override the default behavior of the library on the fly. Adding them to your code is really simple.

## Installing the package

The build override package can be installed on nuget [here](TODO) or by using the package manager

```
PM> Install-Package Discord.Net.BuildOverrides
```

## Adding an override

```cs
public async Task MainAsync()
{
// hook into the log function
BuildOverrides.Log += (buildOverride, message) =>
{
Console.WriteLine($"{buildOverride.Name}: {message}");
return Task.CompletedTask;
};

// add your overrides
await BuildOverrides.AddOverrideAsync("example-override-name");
}

```

Overrides are normally built for specific problems, for example if someone is having an issue and we think we might have a fix then we can create a build override for them to test out the fix.

## Security and Transparency

Overrides can only be created and updated by library developers, you should only apply an override if a library developer askes you to.
Code for the overrides server and the overrides themselves can be found [here](https://github.com/discord-net/Discord.Net.BuildOverrides).

+ 2
- 0
docs/faq/toc.yml View File

@@ -22,3 +22,5 @@
topicUid: FAQ.TextCommands.General
- name: Legacy or Upgrade
topicUid: FAQ.Legacy
- name: Build Overrides
topicUid: FAQ.BuildOverrides.WhatAreThey

+ 1
- 1
docs/guides/int_framework/samples/intro/modal.cs View File

@@ -20,7 +20,7 @@ public class FoodModal : IModal

// Responds to the modal.
[ModalInteraction("food_menu")]
public async Task ModalResponce(FoodModal modal)
public async Task ModalResponse(FoodModal modal)
{
// Build the message to send.
string message = "hey @everyone, I just learned " +


+ 1
- 1
docs/guides/int_framework/samples/postexecution/error_review.cs View File

@@ -16,7 +16,7 @@ async Task SlashCommandExecuted(SlashCommandInfo arg1, Discord.IInteractionConte
await arg2.Interaction.RespondAsync("Invalid number or arguments");
break;
case InteractionCommandError.Exception:
await arg2.Interaction.RespondAsync("Command exception:{arg3.ErrorReason}");
await arg2.Interaction.RespondAsync($"Command exception: {arg3.ErrorReason}");
break;
case InteractionCommandError.Unsuccessful:
await arg2.Interaction.RespondAsync("Command could not be executed");


+ 2
- 2
docs/guides/other_libs/samples/ModifyLogMethod.cs View File

@@ -6,8 +6,8 @@ private static async Task LogAsync(LogMessage message)
LogSeverity.Error => LogEventLevel.Error,
LogSeverity.Warning => LogEventLevel.Warning,
LogSeverity.Info => LogEventLevel.Information,
LogSeverity.Verbose => LogEventLevel.Verbose,
LogSeverity.Debug => LogEventLevel.Debug,
LogSeverity.Verbose => LogEventLevel.Debug,
LogSeverity.Debug => LogEventLevel.Verbose,
_ => LogEventLevel.Information
};
Log.Write(severity, message.Exception, "[{Source}] {Message}", message.Source, message.Message);


+ 278
- 0
experiment/Discord.Net.BuildOverrides/BuildOverrides.cs View File

@@ -0,0 +1,278 @@
using Discord.Overrides;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Runtime.Loader;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// Represents an override that can be loaded.
/// </summary>
public sealed class Override
{
/// <summary>
/// Gets the ID of the override.
/// </summary>
public Guid Id { get; internal set; }

/// <summary>
/// Gets the name of the override.
/// </summary>
public string Name { get; internal set; }

/// <summary>
/// Gets the description of the override.
/// </summary>
public string Description { get; internal set; }

/// <summary>
/// Gets the date this override was created.
/// </summary>
public DateTimeOffset CreatedAt { get; internal set; }

/// <summary>
/// Gets the date the override was last modified.
/// </summary>
public DateTimeOffset LastUpdated { get; internal set; }

internal static Override FromJson(string json)
{
var result = new Override();

using(var textReader = new StringReader(json))
using(var reader = new JsonTextReader(textReader))
{
var obj = JObject.ReadFrom(reader);
result.Id = obj["id"].ToObject<Guid>();
result.Name = obj["name"].ToObject<string>();
result.Description = obj["description"].ToObject<string>();
result.CreatedAt = obj["created_at"].ToObject<DateTimeOffset>();
result.LastUpdated = obj["last_updated"].ToObject<DateTimeOffset>();
}

return result;
}
}

/// <summary>
/// Represents a loaded override instance.
/// </summary>
public sealed class LoadedOverride
{
/// <summary>
/// Gets the aseembly containing the overrides definition.
/// </summary>
public Assembly Assembly { get; internal set; }

/// <summary>
/// Gets an instance of the override.
/// </summary>
public IOverride Instance { get; internal set; }

/// <summary>
/// Gets the overrides type.
/// </summary>
public Type Type { get; internal set; }
}

public sealed class BuildOverrides
{
/// <summary>
/// Fired when an override logs a message.
/// </summary>
public static event Func<Override, string, Task> Log
{
add => _logEvents.Add(value);
remove => _logEvents.Remove(value);

}

/// <summary>
/// Gets a read-only dictionary containing the currently loaded overrides.
/// </summary>
public IReadOnlyDictionary<Override, IReadOnlyCollection<LoadedOverride>> LoadedOverrides
=> _loadedOverrides.Select(x => new KeyValuePair<Override, IReadOnlyCollection<LoadedOverride>> (x.Key, x.Value)).ToDictionary(x => x.Key, x => x.Value);

private static AssemblyLoadContext _overrideDomain;
private static List<Func<Override, string, Task>> _logEvents = new();
private static ConcurrentDictionary<Override, List<LoadedOverride>> _loadedOverrides = new ConcurrentDictionary<Override, List<LoadedOverride>>();

private const string ApiUrl = "https://overrides.discordnet.dev";
static BuildOverrides()
{
_overrideDomain = new AssemblyLoadContext("Discord.Net.Overrides.Runtime");

_overrideDomain.Resolving += _overrideDomain_Resolving;
}

/// <summary>
/// Gets details about a specific override.
/// </summary>
/// <remarks>
/// <b>Note:</b> This method does not load an override, it simply retrives the info about it.
/// </remarks>
/// <param name="name">The name of the override to get.</param>
/// <returns>
/// A task representing the asynchronous get operation. The tasks result is an <see cref="Override"/>
/// if it exists; otherwise <see langword="null"/>.
/// </returns>
public static async Task<Override> GetOverrideAsync(string name)
{
using (var client = new HttpClient())
{
var result = await client.GetAsync($"{ApiUrl}/override/{name}");

if (result.IsSuccessStatusCode)
{
var content = await result.Content.ReadAsStringAsync();

return Override.FromJson(content);
}
else
return null;
}
}

/// <summary>
/// Adds an override to the current Discord.Net instance.
/// </summary>
/// <remarks>
/// The override initialization is non-blocking, any errors that occor within
/// the overrides initialization procedure will be sent in the <see cref="Log"/> event.
/// </remarks>
/// <param name="name">The name of the override to add.</param>
/// <returns>
/// A task representing the asynchronous add operaton. The tasks result is a boolean
/// determining if the add operation was successful.
/// </returns>
public static async Task<bool> AddOverrideAsync(string name)
{
var ovrride = await GetOverrideAsync(name);

if (ovrride == null)
return false;

return await AddOverrideAsync(ovrride);
}

/// <summary>
/// Adds an override to the current Discord.Net instance.
/// </summary>
/// <remarks>
/// The override initialization is non-blocking, any errors that occor within
/// the overrides initialization procedure will be sent in the <see cref="Log"/> event.
/// </remarks>
/// <param name="ovrride">The override to add.</param>
/// <returns>
/// A task representing the asynchronous add operaton. The tasks result is a boolean
/// determining if the add operation was successful.
/// </returns>
public static async Task<bool> AddOverrideAsync(Override ovrride)
{
// download it
var ms = new MemoryStream();

using (var client = new HttpClient())
{
var result = await client.GetAsync($"{ApiUrl}/override/download/{ovrride.Id}");

if (!result.IsSuccessStatusCode)
return false;

await (await result.Content.ReadAsStreamAsync()).CopyToAsync(ms);
}

ms.Position = 0;

// load the assembly
//var test = Assembly.Load(ms.ToArray());
var asm = _overrideDomain.LoadFromStream(ms);

// find out IOverride
var overrides = asm.GetTypes().Where(x => x.GetInterfaces().Any(x => x == typeof(IOverride)));

List<LoadedOverride> loaded = new();

var context = new OverrideContext((m) => HandleLog(ovrride, m), ovrride);

foreach (var ovr in overrides)
{
var inst = (IOverride)Activator.CreateInstance(ovr);

inst.RegisterPackageLookupHandler((s) =>
{
return GetDependencyAsync(ovrride.Id, s);
});

_ = Task.Run(async () =>
{
try
{
await inst.InitializeAsync(context);
}
catch (Exception x)
{
HandleLog(ovrride, $"Failed to initialize build override: {x}");
}
});

loaded.Add(new LoadedOverride()
{
Assembly = asm,
Instance = inst,
Type = ovr
});
}

return _loadedOverrides.AddOrUpdate(ovrride, loaded, (_, __) => loaded) != null;
}

internal static void HandleLog(Override ovr, string msg)
{
_ = Task.Run(async () =>
{
foreach (var item in _logEvents)
{
await item.Invoke(ovr, msg).ConfigureAwait(false);
}
});
}

private static Assembly _overrideDomain_Resolving(AssemblyLoadContext arg1, AssemblyName arg2)
{
// resolve the override id
var v = _loadedOverrides.FirstOrDefault(x => x.Value.Any(x => x.Assembly.FullName == arg1.Assemblies.FirstOrDefault().FullName));

return GetDependencyAsync(v.Key.Id, $"{arg2}").GetAwaiter().GetResult();
}

private static async Task<Assembly> GetDependencyAsync(Guid id, string name)
{
using(var client = new HttpClient())
{
var result = await client.PostAsync($"{ApiUrl}/override/{id}/dependency", new StringContent($"{{ \"info\": \"{name}\"}}", Encoding.UTF8, "application/json"));

if (!result.IsSuccessStatusCode)
throw new Exception("Failed to get dependency");

using(var ms = new MemoryStream())
{
var innerStream = await result.Content.ReadAsStreamAsync();
await innerStream.CopyToAsync(ms);
ms.Position = 0;
return _overrideDomain.LoadFromStream(ms);
}
}
}
}
}

+ 20
- 0
experiment/Discord.Net.BuildOverrides/Discord.Net.BuildOverrides.csproj View File

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

<PropertyGroup>
<LangVersion>9.0</LangVersion>
<AssemblyName>Discord.Net.BuildOverrides</AssemblyName>
<RootNamespace>Discord.BuildOverrides</RootNamespace>
<Description>A Discord.Net extension adding a way to add build overrides for testing.</Description>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">net6.0;net5.0;</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">net6.0;net5.0;</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'net461' ">
<Reference Include="System.Net.Http" />
</ItemGroup>

</Project>

+ 34
- 0
experiment/Discord.Net.BuildOverrides/IOverride.cs View File

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

namespace Discord.Overrides
{
/// <summary>
/// Represents a generic build override for Discord.Net
/// </summary>
public interface IOverride
{
/// <summary>
/// Initializes the override.
/// </summary>
/// <remarks>
/// This method is called by the <see cref="BuildOverrides"/> class
/// and should not be called externally from it.
/// </remarks>
/// <param name="context">Context used by an override to initialize.</param>
/// <returns>
/// A task representing the asynchronous initialization operation.
/// </returns>
Task InitializeAsync(OverrideContext context);

/// <summary>
/// Registers a callback to load a dependency for this override.
/// </summary>
/// <param name="func">The callback to load an external dependency.</param>
void RegisterPackageLookupHandler(Func<string, Task<Assembly>> func);
}
}

+ 30
- 0
experiment/Discord.Net.BuildOverrides/OverrideContext.cs View File

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

namespace Discord.Overrides
{
/// <summary>
/// Represents context thats passed to an override in the initialization step.
/// </summary>
public sealed class OverrideContext
{
/// <summary>
/// A callback used to log messages.
/// </summary>
public Action<string> Log { get; private set; }

/// <summary>
/// The info about the override.
/// </summary>
public Override Info { get; private set; }

internal OverrideContext(Action<string> log, Override info)
{
Log = log;
Info = info;
}
}
}

+ 1
- 0
overrides/Discord.Net.BuildOverrides

@@ -0,0 +1 @@
Subproject commit 9b2be5597468329090015fa1b2775815b20be440

+ 0
- 152
samples/InteractionFramework/CommandHandler.cs View File

@@ -1,152 +0,0 @@
using Discord;
using Discord.Interactions;
using Discord.WebSocket;
using System;
using System.Reflection;
using System.Threading.Tasks;

namespace InteractionFramework
{
public class CommandHandler
{
private readonly DiscordSocketClient _client;
private readonly InteractionService _commands;
private readonly IServiceProvider _services;

public CommandHandler(DiscordSocketClient client, InteractionService commands, IServiceProvider services)
{
_client = client;
_commands = commands;
_services = services;
}

public async Task InitializeAsync ( )
{
// Add the public modules that inherit InteractionModuleBase<T> to the InteractionService
await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services);
// Another approach to get the assembly of a specific type is:
// typeof(CommandHandler).Assembly


// Process the InteractionCreated payloads to execute Interactions commands
_client.InteractionCreated += HandleInteraction;

// Process the command execution results
_commands.SlashCommandExecuted += SlashCommandExecuted;
_commands.ContextCommandExecuted += ContextCommandExecuted;
_commands.ComponentCommandExecuted += ComponentCommandExecuted;
}

# region Error Handling

private Task ComponentCommandExecuted (ComponentCommandInfo arg1, Discord.IInteractionContext arg2, IResult arg3)
{
if (!arg3.IsSuccess)
{
switch (arg3.Error)
{
case InteractionCommandError.UnmetPrecondition:
// implement
break;
case InteractionCommandError.UnknownCommand:
// implement
break;
case InteractionCommandError.BadArgs:
// implement
break;
case InteractionCommandError.Exception:
// implement
break;
case InteractionCommandError.Unsuccessful:
// implement
break;
default:
break;
}
}

return Task.CompletedTask;
}

private Task ContextCommandExecuted (ContextCommandInfo arg1, Discord.IInteractionContext arg2, IResult arg3)
{
if (!arg3.IsSuccess)
{
switch (arg3.Error)
{
case InteractionCommandError.UnmetPrecondition:
// implement
break;
case InteractionCommandError.UnknownCommand:
// implement
break;
case InteractionCommandError.BadArgs:
// implement
break;
case InteractionCommandError.Exception:
// implement
break;
case InteractionCommandError.Unsuccessful:
// implement
break;
default:
break;
}
}

return Task.CompletedTask;
}

private Task SlashCommandExecuted (SlashCommandInfo arg1, Discord.IInteractionContext arg2, IResult arg3)
{
if (!arg3.IsSuccess)
{
switch (arg3.Error)
{
case InteractionCommandError.UnmetPrecondition:
// implement
break;
case InteractionCommandError.UnknownCommand:
// implement
break;
case InteractionCommandError.BadArgs:
// implement
break;
case InteractionCommandError.Exception:
// implement
break;
case InteractionCommandError.Unsuccessful:
// implement
break;
default:
break;
}
}

return Task.CompletedTask;
}
# endregion

# region Execution

private async Task HandleInteraction (SocketInteraction arg)
{
try
{
// Create an execution context that matches the generic type parameter of your InteractionModuleBase<T> modules
var ctx = new SocketInteractionContext(_client, arg);
await _commands.ExecuteCommandAsync(ctx, _services);
}
catch (Exception ex)
{
Console.WriteLine(ex);

// If a Slash Command execution fails it is most likely that the original interaction acknowledgement will persist. It is a good idea to delete the original
// response, or at least let the user know that something went wrong during the command execution.
if(arg.Type == InteractionType.ApplicationCommand)
await arg.GetOriginalResponseAsync().ContinueWith(async (msg) => await msg.Result.DeleteAsync());
}
}
# endregion
}
}

samples/InteractionFramework/ExampleEnum.cs → samples/InteractionFramework/Enums/ExampleEnum.cs View File


+ 81
- 0
samples/InteractionFramework/InteractionHandler.cs View File

@@ -0,0 +1,81 @@
using Discord;
using Discord.Interactions;
using Discord.WebSocket;
using Microsoft.Extensions.Configuration;
using System;
using System.Reflection;
using System.Threading.Tasks;

namespace InteractionFramework
{
public class InteractionHandler
{
private readonly DiscordSocketClient _client;
private readonly InteractionService _handler;
private readonly IServiceProvider _services;
private readonly IConfiguration _configuration;

public InteractionHandler(DiscordSocketClient client, InteractionService handler, IServiceProvider services, IConfiguration config)
{
_client = client;
_handler = handler;
_services = services;
_configuration = config;
}

public async Task InitializeAsync()
{
// Process when the client is ready, so we can register our commands.
_client.Ready += ReadyAsync;
_handler.Log += LogAsync;

// Add the public modules that inherit InteractionModuleBase<T> to the InteractionService
await _handler.AddModulesAsync(Assembly.GetEntryAssembly(), _services);

// Process the InteractionCreated payloads to execute Interactions commands
_client.InteractionCreated += HandleInteraction;
}

private async Task LogAsync(LogMessage log)
=> Console.WriteLine(log);

private async Task ReadyAsync()
{
// Context & Slash commands can be automatically registered, but this process needs to happen after the client enters the READY state.
// Since Global Commands take around 1 hour to register, we should use a test guild to instantly update and test our commands.
if (Program.IsDebug())
await _handler.RegisterCommandsToGuildAsync(_configuration.GetValue<ulong>("testGuild"), true);
else
await _handler.RegisterCommandsGloballyAsync(true);
}

private async Task HandleInteraction(SocketInteraction interaction)
{
try
{
// Create an execution context that matches the generic type parameter of your InteractionModuleBase<T> modules.
var context = new SocketInteractionContext(_client, interaction);

// Execute the incoming command.
var result = await _handler.ExecuteCommandAsync(context, _services);

if (!result.IsSuccess)
switch (result.Error)
{
case InteractionCommandError.UnmetPrecondition:
// implement
break;
default:
break;
}
}
catch
{
// If Slash Command execution fails it is most likely that the original interaction acknowledgement will persist. It is a good idea to delete the original
// response, or at least let the user know that something went wrong during the command execution.
if (interaction.Type is InteractionType.ApplicationCommand)
await interaction.GetOriginalResponseAsync().ContinueWith(async (msg) => await msg.Result.DeleteAsync());
}
}
}
}

+ 0
- 18
samples/InteractionFramework/Modules/ComponentModule.cs View File

@@ -1,18 +0,0 @@
using Discord.Interactions;
using Discord.WebSocket;
using InteractionFramework.Attributes;
using System.Threading.Tasks;

namespace InteractionFramework
{
// As with all other modules, we create the context by defining what type of interaction this module is supposed to target.
internal class ComponentModule : InteractionModuleBase<SocketInteractionContext<SocketMessageComponent>>
{
// With the Attribute DoUserCheck you can make sure that only the user this button targets can click it. This is defined by the first wildcard: *.
// See Attributes/DoUserCheckAttribute.cs for elaboration.
[DoUserCheck]
[ComponentInteraction("myButton:*")]
public async Task ClickButtonAsync(string userId)
=> await RespondAsync(text: ":thumbsup: Clicked!");
}
}

samples/InteractionFramework/Modules/GeneralModule.cs → samples/InteractionFramework/Modules/ExampleModule.cs View File

@@ -1,32 +1,25 @@
using Discord;
using Discord.Interactions;
using InteractionFramework.Attributes;
using System;
using System.Threading.Tasks;

namespace InteractionFramework.Modules
{
// Interation modules must be public and inherit from an IInterationModuleBase
public class GeneralModule : InteractionModuleBase<SocketInteractionContext>
public class ExampleModule : InteractionModuleBase<SocketInteractionContext>
{
// Dependencies can be accessed through Property injection, public properties with public setters will be set by the service provider
public InteractionService Commands { get; set; }

private CommandHandler _handler;
private InteractionHandler _handler;

// Constructor injection is also a valid way to access the dependecies
public GeneralModule(CommandHandler handler)
public ExampleModule(InteractionHandler handler)
{
_handler = handler;
}

// Slash Commands are declared using the [SlashCommand], you need to provide a name and a description, both following the Discord guidelines
[SlashCommand("ping", "Recieve a pong")]
// By setting the DefaultPermission to false, you can disable the command by default. No one can use the command until you give them permission
[DefaultPermission(false)]
public async Task Ping ( )
{
await RespondAsync("pong");
}

// You can use a number of parameter types in you Slash Command handlers (string, int, double, bool, IUser, IChannel, IMentionable, IRole, Enums) by default. Optionally,
// you can implement your own TypeConverters to support a wider range of parameter types. For more information, refer to the library documentation.
// Optional method parameters(parameters with a default value) also will be displayed as optional on Discord.
@@ -34,9 +27,15 @@ namespace InteractionFramework.Modules
// [Summary] lets you customize the name and the description of a parameter
[SlashCommand("echo", "Repeat the input")]
public async Task Echo(string echo, [Summary(description: "mention the user")]bool mention = false)
{
await RespondAsync(echo + (mention ? Context.User.Mention : string.Empty));
}
=> await RespondAsync(echo + (mention ? Context.User.Mention : string.Empty));

[SlashCommand("ping", "Pings the bot and returns its latency.")]
public async Task GreetUserAsync()
=> await RespondAsync(text: $":ping_pong: It took me {Context.Client.Latency}ms to respond to you!", ephemeral: true);

[SlashCommand("bitrate", "Gets the bitrate of a specific voice channel.")]
public async Task GetBitrateAsync([ChannelTypes(ChannelType.Voice, ChannelType.Stage)] IChannel channel)
=> await RespondAsync(text: $"This voice channel has a bitrate of {(channel as IVoiceChannel).Bitrate}");

// [Group] will create a command group. [SlashCommand]s and [ComponentInteraction]s will be registered with the group prefix
[Group("test_group", "This is a command group")]
@@ -46,25 +45,7 @@ namespace InteractionFramework.Modules
// choice option
[SlashCommand("choice_example", "Enums create choices")]
public async Task ChoiceExample(ExampleEnum input)
{
await RespondAsync(input.ToString());
}
}

// User Commands can only have one parameter, which must be a type of SocketUser
[UserCommand("SayHello")]
public async Task SayHello(IUser user)
{
await RespondAsync($"Hello, {user.Mention}");
}

// Message Commands can only have one parameter, which must be a type of SocketMessage
[MessageCommand("Delete")]
[Attributes.RequireOwner]
public async Task DeleteMesage(IMessage message)
{
await message.DeleteAsync();
await RespondAsync("Deleted message.");
=> await RespondAsync(input.ToString());
}

// Use [ComponentInteraction] to handle message component interactions. Message component interaction with the matching customId will be executed.
@@ -80,9 +61,40 @@ namespace InteractionFramework.Modules
// Select Menu interactions, contain ids of the menu options that were selected by the user. You can access the option ids from the method parameters.
// You can also use the wild card pattern with Select Menus, in that case, the wild card captures will be passed on to the method first, followed by the option ids.
[ComponentInteraction("roleSelect")]
public async Task RoleSelect(params string[] selections)
public async Task RoleSelect(string[] selections)
{
throw new NotImplementedException();
}

// With the Attribute DoUserCheck you can make sure that only the user this button targets can click it. This is defined by the first wildcard: *.
// See Attributes/DoUserCheckAttribute.cs for elaboration.
[DoUserCheck]
[ComponentInteraction("myButton:*")]
public async Task ClickButtonAsync(string userId)
=> await RespondAsync(text: ":thumbsup: Clicked!");

// This command will greet target user in the channel this was executed in.
[UserCommand("greet")]
public async Task GreetUserAsync(IUser user)
=> await RespondAsync(text: $":wave: {Context.User} said hi to you, <@{user.Id}>!");

// Pins a message in the channel it is in.
[MessageCommand("pin")]
public async Task PinMessageAsync(IMessage message)
{
// implement
// make a safety cast to check if the message is ISystem- or IUserMessage
if (message is not IUserMessage userMessage)
await RespondAsync(text: ":x: You cant pin system messages!");

// if the pins in this channel are equal to or above 50, no more messages can be pinned.
else if ((await Context.Channel.GetPinnedMessagesAsync()).Count >= 50)
await RespondAsync(text: ":x: You cant pin any more messages, the max has already been reached in this channel!");

else
{
await userMessage.PinAsync();
await RespondAsync(":white_check_mark: Successfully pinned message!");
}
}
}
}

+ 0
- 30
samples/InteractionFramework/Modules/MessageCommandModule.cs View File

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

namespace InteractionFramework.Modules
{
// A transient module for executing commands. This module will NOT keep any information after the command is executed.
internal class MessageCommandModule : InteractionModuleBase<SocketInteractionContext<SocketMessageCommand>>
{
// Pins a message in the channel it is in.
[MessageCommand("pin")]
public async Task PinMessageAsync(IMessage message)
{
// make a safety cast to check if the message is ISystem- or IUserMessage
if (message is not IUserMessage userMessage)
await RespondAsync(text: ":x: You cant pin system messages!");

// if the pins in this channel are equal to or above 50, no more messages can be pinned.
else if ((await Context.Channel.GetPinnedMessagesAsync()).Count >= 50)
await RespondAsync(text: ":x: You cant pin any more messages, the max has already been reached in this channel!");

else
{
await userMessage.PinAsync();
await RespondAsync(":white_check_mark: Successfully pinned message!");
}
}
}
}

+ 0
- 51
samples/InteractionFramework/Modules/SlashCommandModule.cs View File

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

namespace InteractionFramework.Modules
{
public enum Hobby
{
Gaming,

Art,

Reading
}

// A transient module for executing commands. This module will NOT keep any information after the command is executed.
class SlashCommandModule : InteractionModuleBase<SocketInteractionContext<SocketSlashCommand>>
{
// Will be called before execution. Here you can populate several entities you may want to retrieve before executing a command.
// I.E. database objects
public override void BeforeExecute(ICommandInfo command)
{
// Anything
throw new NotImplementedException();
}

// Will be called after execution
public override void AfterExecute(ICommandInfo command)
{
// Anything
throw new NotImplementedException();
}

[SlashCommand("ping", "Pings the bot and returns its latency.")]
public async Task GreetUserAsync()
=> await RespondAsync(text: $":ping_pong: It took me {Context.Client.Latency}ms to respond to you!", ephemeral: true);

[SlashCommand("hobby", "Choose your hobby from the list!")]
public async Task ChooseAsync(Hobby hobby)
=> await RespondAsync(text: $":thumbsup: Your hobby is: {hobby}.");

[SlashCommand("bitrate", "Gets the bitrate of a specific voice channel.")]
public async Task GetBitrateAsync([ChannelTypes(ChannelType.Voice, ChannelType.Stage)] IChannel channel)
{
var voiceChannel = channel as IVoiceChannel;
await RespondAsync(text: $"This voice channel has a bitrate of {voiceChannel.Bitrate}");
}
}
}

+ 0
- 17
samples/InteractionFramework/Modules/UserCommandModule.cs View File

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

namespace InteractionFramework.Modules
{
// A transient module for executing commands. This module will NOT keep any information after the command is executed.
class UserCommandModule : InteractionModuleBase<SocketInteractionContext<SocketUserCommand>>
{
// This command will greet target user in the channel this was executed in.
[UserCommand("greet")]
public async Task GreetUserAsync(IUser user)
=> await RespondAsync(text: $":wave: {Context.User} said hi to you, <@{user.Id}>!");
}
}


+ 33
- 42
samples/InteractionFramework/Program.cs View File

@@ -9,69 +9,60 @@ using System.Threading.Tasks;

namespace InteractionFramework
{
class Program
public class Program
{
// Entry point of the program.
static void Main ( string[] args )
private readonly IConfiguration _configuration;
private readonly IServiceProvider _services;

private readonly DiscordSocketConfig _socketConfig = new()
{
GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.GuildMembers,
AlwaysDownloadUsers = true,
};

public Program()
{
// One of the more flexable ways to access the configuration data is to use the Microsoft's Configuration model,
// this way we can avoid hard coding the environment secrets. I opted to use the Json and environment variable providers here.
IConfiguration config = new ConfigurationBuilder()
_configuration = new ConfigurationBuilder()
.AddEnvironmentVariables(prefix: "DC_")
.AddJsonFile("appsettings.json", optional: true)
.Build();

RunAsync(config).GetAwaiter().GetResult();
_services = new ServiceCollection()
.AddSingleton(_configuration)
.AddSingleton(_socketConfig)
.AddSingleton<DiscordSocketClient>()
.AddSingleton(x => new InteractionService(x.GetRequiredService<DiscordSocketClient>()))
.AddSingleton<InteractionHandler>()
.BuildServiceProvider();
}

static async Task RunAsync (IConfiguration configuration)
{
// Dependency injection is a key part of the Interactions framework but it needs to be disposed at the end of the app's lifetime.
using var services = ConfigureServices(configuration);
static void Main(string[] args)
=> new Program().RunAsync()
.GetAwaiter()
.GetResult();

var client = services.GetRequiredService<DiscordSocketClient>();
var commands = services.GetRequiredService<InteractionService>();
public async Task RunAsync()
{
var client = _services.GetRequiredService<DiscordSocketClient>();

client.Log += LogAsync;
commands.Log += LogAsync;

// Slash Commands and Context Commands are can be automatically registered, but this process needs to happen after the client enters the READY state.
// Since Global Commands take around 1 hour to register, we should use a test guild to instantly update and test our commands. To determine the method we should
// register the commands with, we can check whether we are in a DEBUG environment and if we are, we can register the commands to a predetermined test guild.
client.Ready += async ( ) =>
{
if (IsDebug())
// Id of the test guild can be provided from the Configuration object
await commands.RegisterCommandsToGuildAsync(configuration.GetValue<ulong>("testGuild"), true);
else
await commands.RegisterCommandsGloballyAsync(true);
};

// Here we can initialize the service that will register and execute our commands
await services.GetRequiredService<CommandHandler>().InitializeAsync();
await _services.GetRequiredService<InteractionHandler>()
.InitializeAsync();

// Bot token can be provided from the Configuration object we set up earlier
await client.LoginAsync(TokenType.Bot, configuration["token"]);
await client.LoginAsync(TokenType.Bot, _configuration["token"]);
await client.StartAsync();

// Never quit the program until manually forced to.
await Task.Delay(Timeout.Infinite);
}

static Task LogAsync(LogMessage message)
{
Console.WriteLine(message.ToString());
return Task.CompletedTask;
}

static ServiceProvider ConfigureServices ( IConfiguration configuration )
=> new ServiceCollection()
.AddSingleton(configuration)
.AddSingleton<DiscordSocketClient>()
.AddSingleton(x => new InteractionService(x.GetRequiredService<DiscordSocketClient>()))
.AddSingleton<CommandHandler>()
.BuildServiceProvider();
private async Task LogAsync(LogMessage message)
=> Console.WriteLine(message.ToString());

static bool IsDebug ( )
public static bool IsDebug()
{
#if DEBUG
return true;


+ 1
- 1
samples/ShardedClient/Modules/InteractionModule.cs View File

@@ -5,7 +5,7 @@ using System.Threading.Tasks;
namespace ShardedClient.Modules
{
// A display of portability, which shows how minimal the difference between the 2 frameworks is.
public class InteractionModule : InteractionModuleBase<ShardedInteractionContext<SocketSlashCommand>>
public class InteractionModule : InteractionModuleBase<ShardedInteractionContext>
{
[SlashCommand("info", "Information about this shard.")]
public async Task InfoAsync()


+ 5
- 2
samples/ShardedClient/Program.cs View File

@@ -45,8 +45,11 @@ namespace ShardedClient
client.ShardReady += ReadyAsync;
client.Log += LogAsync;

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

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

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


+ 2
- 2
samples/ShardedClient/Services/InteractionHandlingService.cs View File

@@ -31,9 +31,9 @@ namespace ShardedClient.Services
{
await _service.AddModulesAsync(typeof(InteractionHandlingService).Assembly, _provider);
#if DEBUG
await _service.AddCommandsToGuildAsync(_client.Guilds.First(x => x.Id == 1));
await _service.RegisterCommandsToGuildAsync(1 /* implement */);
#else
await _service.AddCommandsGloballyAsync();
await _service.RegisterCommandsGloballyAsync();
#endif
}



+ 35
- 0
src/Discord.Net.Commands/CommandService.cs View File

@@ -403,6 +403,41 @@ namespace Discord.Commands
AddNullableTypeReader(type, reader);
}
}

/// <summary>
/// Removes a type reader from the list of type readers.
/// </summary>
/// <remarks>
/// Removing a <see cref="TypeReader"/> from the <see cref="CommandService"/> will not dereference the <see cref="TypeReader"/> from the loaded module/command instances.
/// You need to reload the modules for the changes to take effect.
/// </remarks>
/// <param name="type">The type to remove the readers from.</param>
/// <param name="isDefaultTypeReader"><see langword="true"/> if the default readers for <paramref name="type"/> should be removed; otherwise <see langword="false"/>.</param>
/// <param name="readers">The removed collection of type readers.</param>
/// <returns><see langword="true"/> if the remove operation was successful; otherwise <see langword="false"/>.</returns>
public bool TryRemoveTypeReader(Type type, bool isDefaultTypeReader, out IDictionary<Type, TypeReader> readers)
{
readers = new Dictionary<Type, TypeReader>();

if (isDefaultTypeReader)
{
var isSuccess = _defaultTypeReaders.TryRemove(type, out var result);
if (isSuccess)
readers.Add(result?.GetType(), result);

return isSuccess;
}
else
{
var isSuccess = _typeReaders.TryRemove(type, out var result);

if (isSuccess)
readers = result;

return isSuccess;
}
}

internal bool HasDefaultTypeReader(Type type)
{
if (_defaultTypeReaders.ContainsKey(type))


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

@@ -187,5 +187,15 @@ namespace Discord
/// <b>This will still require a stable clock on your system.</b>
/// </remarks>
public bool UseInteractionSnowflakeDate { get; set; } = true;

/// <summary>
/// Gets or sets if the Rest/Socket user <see cref="object.ToString"/> override formats the string in respect to bidirectional unicode.
/// </summary>
/// <remarks>
/// By default, the returned value will be "?Discord?#1234", to work with bidirectional usernames.
/// <br/>
/// If set to <see langword="false"/>, this value will be "Discord#1234".
/// </remarks>
public bool FormatUsersInBidirectionalUnicode { get; set; } = true;
}
}

+ 0
- 21
src/Discord.Net.Core/Entities/Guilds/GuildIntegrationProperties.cs View File

@@ -1,21 +0,0 @@
namespace Discord
{
/// <summary>
/// Provides properties used to modify an <see cref="IGuildIntegration" /> with the specified changes.
/// </summary>
public class GuildIntegrationProperties
{
/// <summary>
/// Gets or sets the behavior when an integration subscription lapses.
/// </summary>
public Optional<int> ExpireBehavior { get; set; }
/// <summary>
/// Gets or sets the period (in seconds) where the integration will ignore lapsed subscriptions.
/// </summary>
public Optional<int> ExpireGracePeriod { get; set; }
/// <summary>
/// Gets or sets whether emoticons should be synced for this integration.
/// </summary>
public Optional<bool> EnableEmoticons { get; set; }
}
}

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

@@ -718,8 +718,25 @@ namespace Discord
/// </returns>
Task<IReadOnlyCollection<IVoiceRegion>> GetVoiceRegionsAsync(RequestOptions options = null);

Task<IReadOnlyCollection<IGuildIntegration>> GetIntegrationsAsync(RequestOptions options = null);
Task<IGuildIntegration> CreateIntegrationAsync(ulong id, string type, RequestOptions options = null);
/// <summary>
/// Gets a collection of all the integrations this guild contains.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains a read-only collection of
/// integrations the guild can has.
/// </returns>
Task<IReadOnlyCollection<IIntegration>> GetIntegrationsAsync(RequestOptions options = null);

/// <summary>
/// Deletes an integration.
/// </summary>
/// <param name="id">The id for the integration.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous removal operation.
/// </returns>
Task DeleteIntegrationAsync(ulong id, RequestOptions options = null);

/// <summary>
/// Gets a collection of all invites in this guild.


+ 0
- 18
src/Discord.Net.Core/Entities/Guilds/IntegrationAccount.cs View File

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

namespace Discord
{
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public struct IntegrationAccount
{
/// <summary> Gets the ID of the account. </summary>
/// <returns> A <see cref="string"/> unique identifier of this integration account. </returns>
public string Id { get; }
/// <summary> Gets the name of the account. </summary>
/// <returns> A string containing the name of this integration account. </returns>
public string Name { get; private set; }

public override string ToString() => Name;
private string DebuggerDisplay => $"{Name} ({Id})";
}
}

src/Discord.Net.Core/Entities/Guilds/IGuildIntegration.cs → src/Discord.Net.Core/Entities/Integrations/IIntegration.cs View File

@@ -3,15 +3,16 @@ using System;
namespace Discord
{
/// <summary>
/// Holds information for a guild integration feature.
/// Holds information for an integration feature.
/// Nullable fields not provided for Discord bot integrations, but are for Twitch etc.
/// </summary>
public interface IGuildIntegration
public interface IIntegration
{
/// <summary>
/// Gets the integration ID.
/// </summary>
/// <returns>
/// An <see cref="UInt64"/> representing the unique identifier value of this integration.
/// A <see cref="ulong"/> representing the unique identifier value of this integration.
/// </returns>
ulong Id { get; }
/// <summary>
@@ -45,30 +46,52 @@ namespace Discord
/// <returns>
/// <c>true</c> if this integration is syncing; otherwise <c>false</c>.
/// </returns>
bool IsSyncing { get; }
bool? IsSyncing { get; }
/// <summary>
/// Gets the ID that this integration uses for "subscribers".
/// </summary>
ulong ExpireBehavior { get; }
ulong? RoleId { get; }
/// <summary>
/// Gets whether emoticons should be synced for this integration (twitch only currently).
/// </summary>
bool? HasEnabledEmoticons { get; }
/// <summary>
/// Gets the behavior of expiring subscribers.
/// </summary>
IntegrationExpireBehavior? ExpireBehavior { get; }
/// <summary>
/// Gets the grace period before expiring "subscribers".
/// </summary>
ulong ExpireGracePeriod { get; }
int? ExpireGracePeriod { get; }
/// <summary>
/// Gets the user for this integration.
/// </summary>
IUser User { get; }
/// <summary>
/// Gets integration account information.
/// </summary>
IIntegrationAccount Account { get; }
/// <summary>
/// Gets when this integration was last synced.
/// </summary>
/// <returns>
/// A <see cref="DateTimeOffset"/> containing a date and time of day when the integration was last synced.
/// </returns>
DateTimeOffset SyncedAt { get; }
DateTimeOffset? SyncedAt { get; }
/// <summary>
/// Gets integration account information.
/// Gets how many subscribers this integration has.
/// </summary>
IntegrationAccount Account { get; }

int? SubscriberCount { get; }
/// <summary>
/// Gets whether this integration been revoked.
/// </summary>
bool? IsRevoked { get; }
/// <summary>
/// Gets the bot/OAuth2 application for a discord integration.
/// </summary>
IIntegrationApplication Application { get; }
IGuild Guild { get; }
ulong GuildId { get; }
ulong RoleId { get; }
IUser User { get; }
}
}

+ 23
- 0
src/Discord.Net.Core/Entities/Integrations/IIntegrationAccount.cs View File

@@ -0,0 +1,23 @@
namespace Discord
{
/// <summary>
/// Provides the account information for an <see cref="IIntegration" />.
/// </summary>
public interface IIntegrationAccount
{
/// <summary>
/// Gets the ID of the account.
/// </summary>
/// <returns>
/// A <see cref="string"/> unique identifier of this integration account.
/// </returns>
string Id { get; }
/// <summary>
/// Gets the name of the account.
/// </summary>
/// <returns>
/// A string containing the name of this integration account.
/// </returns>
string Name { get; }
}
}

+ 33
- 0
src/Discord.Net.Core/Entities/Integrations/IIntegrationApplication.cs View File

@@ -0,0 +1,33 @@
namespace Discord
{
/// <summary>
/// Provides the bot/OAuth2 application for an <see cref="IIntegration" />.
/// </summary>
public interface IIntegrationApplication
{
/// <summary>
/// Gets the id of the app.
/// </summary>
ulong Id { get; }
/// <summary>
/// Gets the name of the app.
/// </summary>
string Name { get; }
/// <summary>
/// Gets the icon hash of the app.
/// </summary>
string Icon { get; }
/// <summary>
/// Gets the description of the app.
/// </summary>
string Description { get; }
/// <summary>
/// Gets the summary of the app.
/// </summary>
string Summary { get; }
/// <summary>
/// Gets the bot associated with this application.
/// </summary>
IUser Bot { get; }
}
}

+ 17
- 0
src/Discord.Net.Core/Entities/Integrations/IntegrationExpireBehavior.cs View File

@@ -0,0 +1,17 @@
namespace Discord
{
/// <summary>
/// The behavior of expiring subscribers for an <see cref="IIntegration" />.
/// </summary>
public enum IntegrationExpireBehavior
{
/// <summary>
/// Removes a role from an expired subscriber.
/// </summary>
RemoveRole = 0,
/// <summary>
/// Kicks an expired subscriber from the guild.
/// </summary>
Kick = 1
}
}

+ 20
- 1
src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteraction.cs View File

@@ -1,3 +1,6 @@
using System;
using System.Threading.Tasks;

namespace Discord
{
/// <summary>
@@ -6,7 +9,7 @@ namespace Discord
public interface IComponentInteraction : IDiscordInteraction
{
/// <summary>
/// Gets the data received with this interaction, contains the button that was clicked.
/// Gets the data received with this component interaction.
/// </summary>
new IComponentInteractionData Data { get; }

@@ -14,5 +17,21 @@ namespace Discord
/// Gets the message that contained the trigger for this interaction.
/// </summary>
IUserMessage Message { get; }

/// <summary>
/// Updates the message which this component resides in with the type <see cref="InteractionResponseType.UpdateMessage"/>
/// </summary>
/// <param name="func">A delegate containing the properties to modify the message with.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>A task that represents the asynchronous operation of updating the message.</returns>
Task UpdateAsync(Action<MessageProperties> func, RequestOptions options = null);

/// <summary>
/// Defers an interaction with the response type 5 (<see cref="InteractionResponseType.DeferredChannelMessageWithSource"/>).
/// </summary>
/// <param name="ephemeral"><see langword="true"/> to defer ephemerally, otherwise <see langword="false"/>.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>A task that represents the asynchronous operation of acknowledging the interaction.</returns>
Task DeferLoadingAsync(bool ephemeral = false, RequestOptions options = null);
}
}

+ 16
- 0
src/Discord.Net.Core/Entities/Messages/FileAttachment.cs View File

@@ -7,13 +7,29 @@ using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// Represents an outgoing file attachment used to send a file to discord.
/// </summary>
public struct FileAttachment : IDisposable
{
/// <summary>
/// Gets or sets the filename.
/// </summary>
public string FileName { get; set; }
/// <summary>
/// Gets or sets the description of the file.
/// </summary>
public string Description { get; set; }

/// <summary>
/// Gets or sets whether this file should be marked as a spoiler.
/// </summary>
public bool IsSpoiler { get; set; }

#pragma warning disable IDISP008
/// <summary>
/// Gets the stream containing the file content.
/// </summary>
public Stream Stream { get; }
#pragma warning restore IDISP008



+ 8
- 0
src/Discord.Net.Core/Entities/Messages/IAttachment.cs View File

@@ -62,5 +62,13 @@ namespace Discord
/// <see langword="true"/> if the attachment is ephemeral; otherwise <see langword="false"/>.
/// </returns>
bool Ephemeral { get; }
/// <summary>
/// Gets the description of the attachment; or <see langword="null"/> if there is none set.
/// </summary>
string Description { get; }
/// <summary>
/// Gets the media's <see href="https://en.wikipedia.org/wiki/Media_type">MIME type</see> if present; otherwise <see langword="null"/>.
/// </summary>
string ContentType { get; }
}
}

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

@@ -13,7 +13,7 @@ namespace Discord
/// <summary> Gets a <see cref="GuildPermissions"/> that grants all guild permissions for webhook users. </summary>
public static readonly GuildPermissions Webhook = new GuildPermissions(0b0_00000_0000000_0000000_0001101100000_000000);
/// <summary> Gets a <see cref="GuildPermissions"/> that grants all guild permissions. </summary>
public static readonly GuildPermissions All = new GuildPermissions(0b1_11111_1111111_1111111_1111111111111_111111);
public static readonly GuildPermissions All = new GuildPermissions(ulong.MaxValue);

/// <summary> Gets a packed value representing all the permissions in this <see cref="GuildPermissions"/>. </summary>
public ulong RawValue { get; }


+ 17
- 0
src/Discord.Net.Core/Entities/Users/ConnectionVisibility.cs View File

@@ -0,0 +1,17 @@
namespace Discord
{
/// <summary>
/// The visibility of the connected account.
/// </summary>
public enum ConnectionVisibility
{
/// <summary>
/// Invisible to everyone except the user themselves.
/// </summary>
None = 0,
/// <summary>
/// Visible to everyone.
/// </summary>
Everyone = 1
}
}

+ 44
- 15
src/Discord.Net.Core/Entities/Users/IConnection.cs View File

@@ -4,24 +4,53 @@ namespace Discord
{
public interface IConnection
{
/// <summary> Gets the ID of the connection account. </summary>
/// <returns> A <see cref="string"/> representing the unique identifier value of this connection. </returns>
/// <summary>
/// Gets the ID of the connection account.
/// </summary>
/// <returns>
/// A <see cref="string"/> representing the unique identifier value of this connection.
/// </returns>
string Id { get; }
/// <summary> Gets the service of the connection (twitch, youtube). </summary>
/// <returns> A string containing the name of this type of connection. </returns>
string Type { get; }
/// <summary> Gets the username of the connection account. </summary>
/// <returns> A string containing the name of this connection. </returns>
/// <summary>
/// Gets the username of the connection account.
/// </summary>
/// <returns>
/// A string containing the name of this connection.
/// </returns>
string Name { get; }
/// <summary> Gets whether the connection is revoked. </summary>
/// <returns> A value which if true indicates that this connection has been revoked, otherwise false. </returns>
bool IsRevoked { get; }

/// <summary> Gets a <see cref="IReadOnlyCollection{T}"/> of integration IDs. </summary>
/// <summary>
/// Gets the service of the connection (twitch, youtube).
/// </summary>
/// <returns>
/// A string containing the name of this type of connection.
/// </returns>
string Type { get; }
/// <summary>
/// Gets whether the connection is revoked.
/// </summary>
/// <returns>
/// An <see cref="IReadOnlyCollection{T}"/> containing <see cref="ulong"/>
/// representations of unique identifier values of integrations.
/// A value which if true indicates that this connection has been revoked, otherwise false.
/// </returns>
IReadOnlyCollection<ulong> IntegrationIds { get; }
bool? IsRevoked { get; }
/// <summary>
/// Gets a <see cref="IReadOnlyCollection{T}"/> of integration parials.
/// </summary>
IReadOnlyCollection<IIntegration> Integrations { get; }
/// <summary>
/// Gets whether the connection is verified.
/// </summary>
bool Verified { get; }
/// <summary>
/// Gets whether friend sync is enabled for this connection.
/// </summary>
bool FriendSync { get; }
/// <summary>
/// Gets whether activities related to this connection will be shown in presence updates.
/// </summary>
bool ShowActivity { get; }
/// <summary>
/// Visibility of this connection.
/// </summary>
ConnectionVisibility Visibility { get; }
}
}

+ 6
- 3
src/Discord.Net.Core/Format.cs View File

@@ -107,13 +107,16 @@ namespace Discord
}

/// <summary>
/// Formats a user's username + discriminator while maintaining bidirectional unicode
/// Formats a user's username + discriminator.
/// </summary>
/// <param name="doBidirectional">To format the string in bidirectional unicode or not</param>
/// <param name="user">The user whos username and discriminator to format</param>
/// <returns>The username + discriminator</returns>
public static string UsernameAndDiscriminator(IUser user)
public static string UsernameAndDiscriminator(IUser user, bool doBidirectional)
{
return $"\u2066{user.Username}\u2069#{user.Discriminator}";
return doBidirectional
? $"\u2066{user.Username}\u2069#{user.Discriminator}"
: $"{user.Username}#{user.Discriminator}";
}
}
}

+ 5
- 1
src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs View File

@@ -10,9 +10,10 @@ namespace Discord.Interactions
/// </summary>
/// <typeparam name="T">Type of the <see cref="IModal"/> implementation.</typeparam>
/// <param name="interaction">The interaction to respond to.</param>
/// <param name="modifyModal">Delegate that can be used to modify the modal.</param>
/// <param name="options">The request options for this <see langword="async"/> request.</param>
/// <returns>A task that represents the asynchronous operation of responding to the interaction.</returns>
public static async Task RespondWithModalAsync<T>(this IDiscordInteraction interaction, string customId, RequestOptions options = null)
public static async Task RespondWithModalAsync<T>(this IDiscordInteraction interaction, string customId, RequestOptions options = null, Action<ModalBuilder> modifyModal = null)
where T : class, IModal
{
if (!ModalUtils.TryGet<T>(out var modalInfo))
@@ -31,6 +32,9 @@ namespace Discord.Interactions
throw new InvalidOperationException($"{input.GetType().FullName} isn't a valid component info class");
}

if (modifyModal is not null)
modifyModal(builder);

await interaction.RespondWithModalAsync(builder.Build(), options).ConfigureAwait(false);
}
}


+ 25
- 0
src/Discord.Net.Interactions/Extensions/RestExtensions.cs View File

@@ -0,0 +1,25 @@
using Discord.Interactions;
using System;

namespace Discord.Rest
{
public static class RestExtensions
{
/// <summary>
/// Respond to an interaction with a <see cref="IModal"/>.
/// </summary>
/// <typeparam name="T">Type of the <see cref="IModal"/> implementation.</typeparam>
/// <param name="interaction">The interaction to respond to.</param>
/// <param name="options">The request options for this <see langword="async"/> request.</param>
/// <returns>Serialized payload to be used to create a HTTP response.</returns>
public static string RespondWithModal<T>(this RestInteraction interaction, string customId, RequestOptions options = null, Action<ModalBuilder> modifyModal = null)
where T : class, IModal
{
if (!ModalUtils.TryGet<T>(out var modalInfo))
throw new ArgumentException($"{typeof(T).FullName} isn't referenced by any registered Modal Interaction Command and doesn't have a cached {typeof(ModalInfo)}");

var modal = modalInfo.ToModal(customId, modifyModal);
return interaction.RespondWithModal(modal, options);
}
}
}

+ 1
- 1
src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs View File

@@ -49,7 +49,7 @@ namespace Discord.Interactions
try
{
var args = new object[Parameters.Count];
var captureCount = additionalArgs.Length;
var captureCount = additionalArgs?.Length ?? 0;

for(var i = 0; i < Parameters.Count; i++)
{


+ 75
- 0
src/Discord.Net.Interactions/InteractionService.cs View File

@@ -24,6 +24,29 @@ namespace Discord.Interactions
public event Func<LogMessage, Task> Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } }
internal readonly AsyncEvent<Func<LogMessage, Task>> _logEvent = new ();

/// <summary>
/// Occurs when any type of interaction is executed.
/// </summary>
public event Func<ICommandInfo, IInteractionContext, IResult, Task> InteractionExecuted
{
add
{
SlashCommandExecuted += value;
ContextCommandExecuted += value;
ComponentCommandExecuted += value;
AutocompleteCommandExecuted += value;
ModalCommandExecuted += value;
}
remove
{
SlashCommandExecuted -= value;
ContextCommandExecuted -= value;
ComponentCommandExecuted -= value;
AutocompleteCommandExecuted -= value;
ModalCommandExecuted -= value;
}
}

/// <summary>
/// Occurs when a Slash Command is executed.
/// </summary>
@@ -945,9 +968,61 @@ namespace Discord.Interactions
public void AddGenericTypeReader(Type targetType, Type readerType) =>
_typeReaderMap.AddGeneric(targetType, readerType);

/// <summary>
/// Removes a type reader for the type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type to remove the readers from.</typeparam>
/// <param name="reader">The reader if the resulting remove operation was successful.</param>
/// <returns><see langword="true"/> if the remove operation was successful; otherwise <see langword="false"/>.</returns>
public bool TryRemoveTypeReader<T>(out TypeReader reader)
=> TryRemoveTypeReader(typeof(T), out reader);

/// <summary>
/// Removes a type reader for the given type.
/// </summary>
/// <remarks>
/// Removing a <see cref="TypeReader"/> from the <see cref="CommandService"/> will not dereference the <see cref="TypeReader"/> from the loaded module/command instances.
/// You need to reload the modules for the changes to take effect.
/// </remarks>
/// <param name="type">The type to remove the reader from.</param>
/// <param name="reader">The reader if the resulting remove operation was successful.</param>
/// <returns><see langword="true"/> if the remove operation was successful; otherwise <see langword="false"/>.</returns>
public bool TryRemoveTypeReader(Type type, out TypeReader reader)
=> _typeReaderMap.TryRemoveConcrete(type, out reader);

/// <summary>
/// Removes a generic type reader from the type <typeparamref name="T"/>.
/// </summary>
/// <remarks>
/// Removing a <see cref="TypeReader"/> from the <see cref="CommandService"/> will not dereference the <see cref="TypeReader"/> from the loaded module/command instances.
/// You need to reload the modules for the changes to take effect.
/// </remarks>
/// <typeparam name="T">The type to remove the readers from.</typeparam>
/// <param name="readerType">The removed readers type.</param>
/// <returns><see langword="true"/> if the remove operation was successful; otherwise <see langword="false"/>.</returns>
public bool TryRemoveGenericTypeReader<T>(out Type readerType)
=> TryRemoveGenericTypeReader(typeof(T), out readerType);

/// <summary>
/// Removes a generic type reader from the given type.
/// </summary>
/// <remarks>
/// Removing a <see cref="TypeReader"/> from the <see cref="CommandService"/> will not dereference the <see cref="TypeReader"/> from the loaded module/command instances.
/// You need to reload the modules for the changes to take effect.
/// </remarks>
/// <param name="type">The type to remove the reader from.</param>
/// <param name="readerType">The readers type if the remove operation was successful.</param>
/// <returns><see langword="true"/> if the remove operation was successful; otherwise <see langword="false"/>.</returns>
public bool TryRemoveGenericTypeReader(Type type, out Type readerType)
=> _typeReaderMap.TryRemoveGeneric(type, out readerType);

/// <summary>
/// Serialize an object using a <see cref="TypeReader"/> into a <see cref="string"/> to be placed in a Component CustomId.
/// </summary>
/// <remarks>
/// Removing a <see cref="TypeReader"/> from the <see cref="CommandService"/> will not dereference the <see cref="TypeReader"/> from the loaded module/command instances.
/// You need to reload the modules for the changes to take effect.
/// </remarks>
/// <typeparam name="T">Type of the object to be serialized.</typeparam>
/// <param name="obj">Object to be serialized.</param>
/// <param name="services">Services that will be passed on to the <see cref="TypeReader"/>.</param>


+ 12
- 0
src/Discord.Net.Interactions/Map/TypeMap.cs View File

@@ -74,6 +74,18 @@ namespace Discord.Interactions
_generics[targetType] = converterType;
}

public bool TryRemoveConcrete<TTarget>(out TConverter converter)
=> TryRemoveConcrete(typeof(TTarget), out converter);

public bool TryRemoveConcrete(Type type, out TConverter converter)
=> _concretes.TryRemove(type, out converter);

public bool TryRemoveGeneric<TTarget>(out Type converterType)
=> TryRemoveGeneric(typeof(TTarget), out converterType);

public bool TryRemoveGeneric(Type targetType, out Type converterType)
=> _generics.TryRemove(targetType, out converterType);

private Type GetMostSpecific(Type type)
{
if (_generics.TryGetValue(type, out var matching))


+ 34
- 0
src/Discord.Net.Interactions/RestInteractionModuleBase.cs View File

@@ -65,5 +65,39 @@ namespace Discord.Interactions
else
await InteractionService._restResponseCallback(Context, payload).ConfigureAwait(false);
}

/// <summary>
/// Responds to the interaction with a modal.
/// </summary>
/// <param name="modal">The modal to respond with.</param>
/// <param name="options">The request options for this <see langword="async"/> request.</param>
/// <returns>A string that contains json to write back to the incoming http request.</returns>
/// <exception cref="TimeoutException"></exception>
/// <exception cref="InvalidOperationException"></exception>
protected override async Task RespondWithModalAsync(Modal modal, RequestOptions options = null)
{
if (Context.Interaction is not RestInteraction restInteraction)
throw new InvalidOperationException($"Invalid interaction type. Interaction must be a type of {nameof(RestInteraction)} in order to execute this method");

var payload = restInteraction.RespondWithModal(modal, options);

if (Context is IRestInteractionContext restContext && restContext.InteractionResponseCallback != null)
await restContext.InteractionResponseCallback.Invoke(payload).ConfigureAwait(false);
else
await InteractionService._restResponseCallback(Context, payload).ConfigureAwait(false);
}

protected override async Task RespondWithModalAsync<T>(string customId, RequestOptions options = null)
{
if (Context.Interaction is not RestInteraction restInteraction)
throw new InvalidOperationException($"Invalid interaction type. Interaction must be a type of {nameof(RestInteraction)} in order to execute this method");

var payload = restInteraction.RespondWithModal<T>(customId, options);

if (Context is IRestInteractionContext restContext && restContext.InteractionResponseCallback != null)
await restContext.InteractionResponseCallback.Invoke(payload).ConfigureAwait(false);
else
await InteractionService._restResponseCallback(Context, payload).ConfigureAwait(false);
}
}
}

+ 21
- 0
src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs View File

@@ -196,5 +196,26 @@ namespace Discord.Interactions
}).ToList(),
Options = commandOption.Options?.Select(x => x.ToApplicationCommandOptionProps()).ToList()
};

public static Modal ToModal(this ModalInfo modalInfo, string customId, Action<ModalBuilder> modifyModal = null)
{
var builder = new ModalBuilder(modalInfo.Title, customId);

foreach (var input in modalInfo.Components)
switch (input)
{
case TextInputComponentInfo textComponent:
builder.AddTextInput(textComponent.Label, textComponent.CustomId, textComponent.Style, textComponent.Placeholder, textComponent.IsRequired ? textComponent.MinLength : null,
textComponent.MaxLength, textComponent.IsRequired, textComponent.InitialValue);
break;
default:
throw new InvalidOperationException($"{input.GetType().FullName} isn't a valid component info class");
}

if(modifyModal is not null)
modifyModal(builder);

return builder.Build();
}
}
}

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

@@ -7,14 +7,22 @@ namespace Discord.API
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("revoked")]
public bool Revoked { get; set; }

public Optional<bool> Revoked { get; set; }
[JsonProperty("integrations")]
public IReadOnlyCollection<ulong> Integrations { get; set; }
public Optional<IReadOnlyCollection<Integration>> Integrations { get; set; }
[JsonProperty("verified")]
public bool Verified { get; set; }
[JsonProperty("friend_sync")]
public bool FriendSync { get; set; }
[JsonProperty("show_activity")]
public bool ShowActivity { get; set; }
[JsonProperty("visibility")]
public ConnectionVisibility Visibility { get; set; }

}
}

+ 18
- 7
src/Discord.Net.Rest/API/Common/Integration.cs View File

@@ -5,6 +5,9 @@ namespace Discord.API
{
internal class Integration
{
[JsonProperty("guild_id")]
public Optional<ulong> GuildId { get; set; }

[JsonProperty("id")]
public ulong Id { get; set; }
[JsonProperty("name")]
@@ -14,18 +17,26 @@ namespace Discord.API
[JsonProperty("enabled")]
public bool Enabled { get; set; }
[JsonProperty("syncing")]
public bool Syncing { get; set; }
public Optional<bool?> Syncing { get; set; }
[JsonProperty("role_id")]
public ulong RoleId { get; set; }
public Optional<ulong?> RoleId { get; set; }
[JsonProperty("enable_emoticons")]
public Optional<bool?> EnableEmoticons { get; set; }
[JsonProperty("expire_behavior")]
public ulong ExpireBehavior { get; set; }
public Optional<IntegrationExpireBehavior> ExpireBehavior { get; set; }
[JsonProperty("expire_grace_period")]
public ulong ExpireGracePeriod { get; set; }
public Optional<int?> ExpireGracePeriod { get; set; }
[JsonProperty("user")]
public User User { get; set; }
public Optional<User> User { get; set; }
[JsonProperty("account")]
public IntegrationAccount Account { get; set; }
public Optional<IntegrationAccount> Account { get; set; }
[JsonProperty("synced_at")]
public DateTimeOffset SyncedAt { get; set; }
public Optional<DateTimeOffset> SyncedAt { get; set; }
[JsonProperty("subscriber_count")]
public Optional<int?> SubscriberAccount { get; set; }
[JsonProperty("revoked")]
public Optional<bool?> Revoked { get; set; }
[JsonProperty("application")]
public Optional<IntegrationApplication> Application { get; set; }
}
}

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

@@ -5,7 +5,7 @@ namespace Discord.API
internal class IntegrationAccount
{
[JsonProperty("id")]
public ulong Id { get; set; }
public string Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
}


+ 20
- 0
src/Discord.Net.Rest/API/Common/IntegrationApplication.cs View File

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

namespace Discord.API
{
internal class IntegrationApplication
{
[JsonProperty("id")]
public ulong Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("icon")]
public Optional<string> Icon { get; set; }
[JsonProperty("description")]
public string Description { get; set; }
[JsonProperty("summary")]
public string Summary { get; set; }
[JsonProperty("bot")]
public Optional<User> Bot { get; set; }
}
}

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

@@ -21,6 +21,6 @@ namespace Discord.API
public Optional<bool> Invitable { get; set; }

[JsonProperty("create_timestamp")]
public Optional<DateTimeOffset> CreatedAt { get; set; }
public Optional<DateTimeOffset?> CreatedAt { get; set; }
}
}

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

@@ -36,6 +36,7 @@ namespace Discord.Rest
/// <inheritdoc />
public TokenType TokenType => ApiClient.AuthTokenType;
internal bool UseInteractionSnowflakeDate { get; private set; }
internal bool FormatUsersInBidirectionalUnicode { get; private set; }

/// <summary> Creates a new REST-only Discord client. </summary>
internal BaseDiscordClient(DiscordRestConfig config, API.DiscordRestApiClient client)
@@ -49,6 +50,7 @@ namespace Discord.Rest
_isFirstLogin = config.DisplayInitialLog;

UseInteractionSnowflakeDate = config.UseInteractionSnowflakeDate;
FormatUsersInBidirectionalUnicode = config.FormatUsersInBidirectionalUnicode;

ApiClient.RequestQueue.RateLimitTriggered += async (id, info, endpoint) =>
{


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

@@ -49,7 +49,7 @@ namespace Discord.Rest
public static async Task<IReadOnlyCollection<RestConnection>> GetConnectionsAsync(BaseDiscordClient client, RequestOptions options)
{
var models = await client.ApiClient.GetMyConnectionsAsync(options).ConfigureAwait(false);
return models.Select(RestConnection.Create).ToImmutableArray();
return models.Select(model => RestConnection.Create(client, model)).ToImmutableArray();
}

public static async Task<RestInviteMetadata> GetInviteAsync(BaseDiscordClient client,


+ 3
- 36
src/Discord.Net.Rest/DiscordRestApiClient.cs View File

@@ -1626,7 +1626,7 @@ namespace Discord.API

#region Guild Integrations
/// <exception cref="ArgumentException"><paramref name="guildId"/> must not be equal to zero.</exception>
public async Task<IReadOnlyCollection<Integration>> GetGuildIntegrationsAsync(ulong guildId, RequestOptions options = null)
public async Task<IReadOnlyCollection<Integration>> GetIntegrationsAsync(ulong guildId, RequestOptions options = null)
{
Preconditions.NotEqual(guildId, 0, nameof(guildId));
options = RequestOptions.CreateOrClone(options);
@@ -1634,47 +1634,14 @@ namespace Discord.API
var ids = new BucketIds(guildId: guildId);
return await SendAsync<IReadOnlyCollection<Integration>>("GET", () => $"guilds/{guildId}/integrations", ids, options: options).ConfigureAwait(false);
}
/// <exception cref="ArgumentException"><paramref name="guildId"/> and <paramref name="args.Id"/> must not be equal to zero.</exception>
/// <exception cref="ArgumentNullException"><paramref name="args"/> must not be <see langword="null"/>.</exception>
public async Task<Integration> CreateGuildIntegrationAsync(ulong guildId, CreateGuildIntegrationParams args, RequestOptions options = null)
{
Preconditions.NotEqual(guildId, 0, nameof(guildId));
Preconditions.NotNull(args, nameof(args));
Preconditions.NotEqual(args.Id, 0, nameof(args.Id));
options = RequestOptions.CreateOrClone(options);

var ids = new BucketIds(guildId: guildId);
return await SendAsync<Integration>("POST", () => $"guilds/{guildId}/integrations", ids, options: options).ConfigureAwait(false);
}
public async Task<Integration> DeleteGuildIntegrationAsync(ulong guildId, ulong integrationId, RequestOptions options = null)
{
Preconditions.NotEqual(guildId, 0, nameof(guildId));
Preconditions.NotEqual(integrationId, 0, nameof(integrationId));
options = RequestOptions.CreateOrClone(options);

var ids = new BucketIds(guildId: guildId);
return await SendAsync<Integration>("DELETE", () => $"guilds/{guildId}/integrations/{integrationId}", ids, options: options).ConfigureAwait(false);
}
public async Task<Integration> ModifyGuildIntegrationAsync(ulong guildId, ulong integrationId, Rest.ModifyGuildIntegrationParams args, RequestOptions options = null)
{
Preconditions.NotEqual(guildId, 0, nameof(guildId));
Preconditions.NotEqual(integrationId, 0, nameof(integrationId));
Preconditions.NotNull(args, nameof(args));
Preconditions.AtLeast(args.ExpireBehavior, 0, nameof(args.ExpireBehavior));
Preconditions.AtLeast(args.ExpireGracePeriod, 0, nameof(args.ExpireGracePeriod));
options = RequestOptions.CreateOrClone(options);

var ids = new BucketIds(guildId: guildId);
return await SendJsonAsync<Integration>("PATCH", () => $"guilds/{guildId}/integrations/{integrationId}", args, ids, options: options).ConfigureAwait(false);
}
public async Task<Integration> SyncGuildIntegrationAsync(ulong guildId, ulong integrationId, RequestOptions options = null)
public async Task DeleteIntegrationAsync(ulong guildId, ulong integrationId, RequestOptions options = null)
{
Preconditions.NotEqual(guildId, 0, nameof(guildId));
Preconditions.NotEqual(integrationId, 0, nameof(integrationId));
options = RequestOptions.CreateOrClone(options);

var ids = new BucketIds(guildId: guildId);
return await SendAsync<Integration>("POST", () => $"guilds/{guildId}/integrations/{integrationId}/sync", ids, options: options).ConfigureAwait(false);
await SendAsync("DELETE", () => $"guilds/{guildId}/integrations/{integrationId}", ids, options: options).ConfigureAwait(false);
}
#endregion



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

@@ -305,19 +305,15 @@ namespace Discord.Rest
#endregion

#region Integrations
public static async Task<IReadOnlyCollection<RestGuildIntegration>> GetIntegrationsAsync(IGuild guild, BaseDiscordClient client,
public static async Task<IReadOnlyCollection<RestIntegration>> GetIntegrationsAsync(IGuild guild, BaseDiscordClient client,
RequestOptions options)
{
var models = await client.ApiClient.GetGuildIntegrationsAsync(guild.Id, options).ConfigureAwait(false);
return models.Select(x => RestGuildIntegration.Create(client, guild, x)).ToImmutableArray();
}
public static async Task<RestGuildIntegration> CreateIntegrationAsync(IGuild guild, BaseDiscordClient client,
ulong id, string type, RequestOptions options)
{
var args = new CreateGuildIntegrationParams(id, type);
var model = await client.ApiClient.CreateGuildIntegrationAsync(guild.Id, args, options).ConfigureAwait(false);
return RestGuildIntegration.Create(client, guild, model);
var models = await client.ApiClient.GetIntegrationsAsync(guild.Id, options).ConfigureAwait(false);
return models.Select(x => RestIntegration.Create(client, guild, x)).ToImmutableArray();
}
public static async Task DeleteIntegrationAsync(IGuild guild, BaseDiscordClient client, ulong id,
RequestOptions options) =>
await client.ApiClient.DeleteIntegrationAsync(guild.Id, id, options).ConfigureAwait(false);
#endregion

#region Interactions


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

@@ -720,10 +720,10 @@ namespace Discord.Rest
#endregion

#region Integrations
public Task<IReadOnlyCollection<RestGuildIntegration>> GetIntegrationsAsync(RequestOptions options = null)
public Task<IReadOnlyCollection<RestIntegration>> GetIntegrationsAsync(RequestOptions options = null)
=> GuildHelper.GetIntegrationsAsync(this, Discord, options);
public Task<RestGuildIntegration> CreateIntegrationAsync(ulong id, string type, RequestOptions options = null)
=> GuildHelper.CreateIntegrationAsync(this, Discord, id, type, options);
public Task DeleteIntegrationAsync(ulong id, RequestOptions options = null)
=> GuildHelper.DeleteIntegrationAsync(this, Discord, id, options);
#endregion

#region Invites
@@ -763,11 +763,6 @@ namespace Discord.Rest
return null;
}

/// <inheritdoc />
public Task<RestRole> CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?),
bool isHoisted = false, RequestOptions options = null)
=> CreateRoleAsync(name, permissions, color, isHoisted, false, options);

/// <summary>
/// Creates a new role with the provided name.
/// </summary>
@@ -1375,11 +1370,11 @@ namespace Discord.Rest
=> await GetVoiceRegionsAsync(options).ConfigureAwait(false);

/// <inheritdoc />
async Task<IReadOnlyCollection<IGuildIntegration>> IGuild.GetIntegrationsAsync(RequestOptions options)
async Task<IReadOnlyCollection<IIntegration>> IGuild.GetIntegrationsAsync(RequestOptions options)
=> await GetIntegrationsAsync(options).ConfigureAwait(false);
/// <inheritdoc />
async Task<IGuildIntegration> IGuild.CreateIntegrationAsync(ulong id, string type, RequestOptions options)
=> await CreateIntegrationAsync(id, type, options).ConfigureAwait(false);
async Task IGuild.DeleteIntegrationAsync(ulong id, RequestOptions options)
=> await DeleteIntegrationAsync(id, options).ConfigureAwait(false);

/// <inheritdoc />
async Task<IReadOnlyCollection<IInviteMetadata>> IGuild.GetInvitesAsync(RequestOptions options)


+ 0
- 104
src/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs View File

@@ -1,104 +0,0 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Model = Discord.API.Integration;

namespace Discord.Rest
{
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class RestGuildIntegration : RestEntity<ulong>, IGuildIntegration
{
private long _syncedAtTicks;

/// <inheritdoc />
public string Name { get; private set; }
/// <inheritdoc />
public string Type { get; private set; }
/// <inheritdoc />
public bool IsEnabled { get; private set; }
/// <inheritdoc />
public bool IsSyncing { get; private set; }
/// <inheritdoc />
public ulong ExpireBehavior { get; private set; }
/// <inheritdoc />
public ulong ExpireGracePeriod { get; private set; }
/// <inheritdoc />
public ulong GuildId { get; private set; }
/// <inheritdoc />
public ulong RoleId { get; private set; }
public RestUser User { get; private set; }
/// <inheritdoc />
public IntegrationAccount Account { get; private set; }
internal IGuild Guild { get; private set; }

/// <inheritdoc />
public DateTimeOffset SyncedAt => DateTimeUtils.FromTicks(_syncedAtTicks);

internal RestGuildIntegration(BaseDiscordClient discord, IGuild guild, ulong id)
: base(discord, id)
{
Guild = guild;
}
internal static RestGuildIntegration Create(BaseDiscordClient discord, IGuild guild, Model model)
{
var entity = new RestGuildIntegration(discord, guild, model.Id);
entity.Update(model);
return entity;
}

internal void Update(Model model)
{
Name = model.Name;
Type = model.Type;
IsEnabled = model.Enabled;
IsSyncing = model.Syncing;
ExpireBehavior = model.ExpireBehavior;
ExpireGracePeriod = model.ExpireGracePeriod;
_syncedAtTicks = model.SyncedAt.UtcTicks;

RoleId = model.RoleId;
User = RestUser.Create(Discord, model.User);
}
public async Task DeleteAsync()
{
await Discord.ApiClient.DeleteGuildIntegrationAsync(GuildId, Id).ConfigureAwait(false);
}
public async Task ModifyAsync(Action<GuildIntegrationProperties> func)
{
if (func == null) throw new NullReferenceException(nameof(func));

var args = new GuildIntegrationProperties();
func(args);
var apiArgs = new API.Rest.ModifyGuildIntegrationParams
{
EnableEmoticons = args.EnableEmoticons,
ExpireBehavior = args.ExpireBehavior,
ExpireGracePeriod = args.ExpireGracePeriod
};
var model = await Discord.ApiClient.ModifyGuildIntegrationAsync(GuildId, Id, apiArgs).ConfigureAwait(false);

Update(model);
}
public async Task SyncAsync()
{
await Discord.ApiClient.SyncGuildIntegrationAsync(GuildId, Id).ConfigureAwait(false);
}

public override string ToString() => Name;
private string DebuggerDisplay => $"{Name} ({Id}{(IsEnabled ? ", Enabled" : "")})";

/// <inheritdoc />
IGuild IGuildIntegration.Guild
{
get
{
if (Guild != null)
return Guild;
throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object.");
}
}
/// <inheritdoc />
IUser IGuildIntegration.User => User;
}
}

+ 102
- 0
src/Discord.Net.Rest/Entities/Integrations/RestIntegration.cs View File

@@ -0,0 +1,102 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Model = Discord.API.Integration;

namespace Discord.Rest
{
/// <summary>
/// Represents a Rest-based implementation of <see cref="IIntegration"/>.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class RestIntegration : RestEntity<ulong>, IIntegration
{
private long? _syncedAtTicks;

/// <inheritdoc />
public string Name { get; private set; }
/// <inheritdoc />
public string Type { get; private set; }
/// <inheritdoc />
public bool IsEnabled { get; private set; }
/// <inheritdoc />
public bool? IsSyncing { get; private set; }
/// <inheritdoc />
public ulong? RoleId { get; private set; }
/// <inheritdoc />
public bool? HasEnabledEmoticons { get; private set; }
/// <inheritdoc />
public IntegrationExpireBehavior? ExpireBehavior { get; private set; }
/// <inheritdoc />
public int? ExpireGracePeriod { get; private set; }
/// <inheritdoc />
IUser IIntegration.User => User;
/// <inheritdoc />
public IIntegrationAccount Account { get; private set; }
/// <inheritdoc />
public DateTimeOffset? SyncedAt => DateTimeUtils.FromTicks(_syncedAtTicks);
/// <inheritdoc />
public int? SubscriberCount { get; private set; }
/// <inheritdoc />
public bool? IsRevoked { get; private set; }
/// <inheritdoc />
public IIntegrationApplication Application { get; private set; }

internal IGuild Guild { get; private set; }
public RestUser User { get; private set; }

internal RestIntegration(BaseDiscordClient discord, IGuild guild, ulong id)
: base(discord, id)
{
Guild = guild;
}
internal static RestIntegration Create(BaseDiscordClient discord, IGuild guild, Model model)
{
var entity = new RestIntegration(discord, guild, model.Id);
entity.Update(model);
return entity;
}

internal void Update(Model model)
{
Name = model.Name;
Type = model.Type;
IsEnabled = model.Enabled;

IsSyncing = model.Syncing.IsSpecified ? model.Syncing.Value : null;
RoleId = model.RoleId.IsSpecified ? model.RoleId.Value : null;
HasEnabledEmoticons = model.EnableEmoticons.IsSpecified ? model.EnableEmoticons.Value : null;
ExpireBehavior = model.ExpireBehavior.IsSpecified ? model.ExpireBehavior.Value : null;
ExpireGracePeriod = model.ExpireGracePeriod.IsSpecified ? model.ExpireGracePeriod.Value : null;
User = model.User.IsSpecified ? RestUser.Create(Discord, model.User.Value) : null;
Account = model.Account.IsSpecified ? RestIntegrationAccount.Create(model.Account.Value) : null;
SubscriberCount = model.SubscriberAccount.IsSpecified ? model.SubscriberAccount.Value : null;
IsRevoked = model.Revoked.IsSpecified ? model.Revoked.Value : null;
Application = model.Application.IsSpecified ? RestIntegrationApplication.Create(Discord, model.Application.Value) : null;

_syncedAtTicks = model.SyncedAt.IsSpecified ? model.SyncedAt.Value.UtcTicks : null;
}
public async Task DeleteAsync()
{
await Discord.ApiClient.DeleteIntegrationAsync(GuildId, Id).ConfigureAwait(false);
}

public override string ToString() => Name;
private string DebuggerDisplay => $"{Name} ({Id}{(IsEnabled ? ", Enabled" : "")})";

/// <inheritdoc />
public ulong GuildId { get; private set; }

/// <inheritdoc />
IGuild IIntegration.Guild
{
get
{
if (Guild != null)
return Guild;
throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object.");
}
}
}
}

+ 29
- 0
src/Discord.Net.Rest/Entities/Integrations/RestIntegrationAccount.cs View File

@@ -0,0 +1,29 @@
using Model = Discord.API.IntegrationAccount;

namespace Discord.Rest
{
/// <summary>
/// Represents a Rest-based implementation of <see cref="IIntegrationAccount"/>.
/// </summary>
public class RestIntegrationAccount : IIntegrationAccount
{
internal RestIntegrationAccount() { }

public string Id { get; private set; }

public string Name { get; private set; }

internal static RestIntegrationAccount Create(Model model)
{
var entity = new RestIntegrationAccount();
entity.Update(model);
return entity;
}

internal void Update(Model model)
{
model.Name = Name;
model.Id = Id;
}
}
}

+ 39
- 0
src/Discord.Net.Rest/Entities/Integrations/RestIntegrationApplication.cs View File

@@ -0,0 +1,39 @@
using Model = Discord.API.IntegrationApplication;

namespace Discord.Rest
{
/// <summary>
/// Represents a Rest-based implementation of <see cref="IIntegrationApplication"/>.
/// </summary>
public class RestIntegrationApplication : RestEntity<ulong>, IIntegrationApplication
{
public string Name { get; private set; }

public string Icon { get; private set; }

public string Description { get; private set; }

public string Summary { get; private set; }

public IUser Bot { get; private set; }

internal RestIntegrationApplication(BaseDiscordClient discord, ulong id)
: base(discord, id) { }

internal static RestIntegrationApplication Create(BaseDiscordClient discord, Model model)
{
var entity = new RestIntegrationApplication(discord, model.Id);
entity.Update(model);
return entity;
}

internal void Update(Model model)
{
Name = model.Name;
Icon = model.Icon.IsSpecified ? model.Icon.Value : null;
Description = model.Description;
Summary = model.Summary;
Bot = RestUser.Create(Discord, model.Bot.Value);
}
}
}

+ 8
- 0
src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs View File

@@ -492,5 +492,13 @@ namespace Discord.Rest

/// <inheritdoc/>
IUserMessage IComponentInteraction.Message => Message;

/// <inheritdoc />
Task IComponentInteraction.UpdateAsync(Action<MessageProperties> func, RequestOptions options)
=> Task.FromResult(Update(func, options));

/// <inheritdoc />
Task IComponentInteraction.DeferLoadingAsync(bool ephemeral, RequestOptions options)
=> Task.FromResult(DeferLoading(ephemeral, options));
}
}

+ 10
- 2
src/Discord.Net.Rest/Entities/Messages/Attachment.cs View File

@@ -23,8 +23,13 @@ namespace Discord
public int? Width { get; }
/// <inheritdoc />
public bool Ephemeral { get; }
/// <inheritdoc />
public string Description { get; }
/// <inheritdoc />
public string ContentType { get; }

internal Attachment(ulong id, string filename, string url, string proxyUrl, int size, int? height, int? width, bool? ephemeral)
internal Attachment(ulong id, string filename, string url, string proxyUrl, int size, int? height, int? width,
bool? ephemeral, string description, string contentType)
{
Id = id;
Filename = filename;
@@ -34,13 +39,16 @@ namespace Discord
Height = height;
Width = width;
Ephemeral = ephemeral.GetValueOrDefault(false);
Description = description;
ContentType = contentType;
}
internal static Attachment Create(Model model)
{
return new Attachment(model.Id, model.Filename, model.Url, model.ProxyUrl, model.Size,
model.Height.IsSpecified ? model.Height.Value : (int?)null,
model.Width.IsSpecified ? model.Width.Value : (int?)null,
model.Ephemeral.ToNullable());
model.Ephemeral.ToNullable(), model.Description.GetValueOrDefault(),
model.ContentType.GetValueOrDefault());
}

/// <summary>


+ 38
- 15
src/Discord.Net.Rest/Entities/Users/RestConnection.cs View File

@@ -1,6 +1,8 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using Model = Discord.API.Connection;

namespace Discord.Rest
@@ -9,28 +11,49 @@ namespace Discord.Rest
public class RestConnection : IConnection
{
/// <inheritdoc />
public string Id { get; }
public string Id { get; private set; }
/// <inheritdoc />
public string Type { get; }
public string Name { get; private set; }
/// <inheritdoc />
public string Name { get; }
public string Type { get; private set; }
/// <inheritdoc />
public bool IsRevoked { get; }
public bool? IsRevoked { get; private set; }
/// <inheritdoc />
public IReadOnlyCollection<ulong> IntegrationIds { get; }
public IReadOnlyCollection<IIntegration> Integrations { get; private set; }
/// <inheritdoc />
public bool Verified { get; private set; }
/// <inheritdoc />
public bool FriendSync { get; private set; }
/// <inheritdoc />
public bool ShowActivity { get; private set; }
/// <inheritdoc />
public ConnectionVisibility Visibility { get; private set; }

internal RestConnection(string id, string type, string name, bool isRevoked, IReadOnlyCollection<ulong> integrationIds)
{
Id = id;
Type = type;
Name = name;
IsRevoked = isRevoked;
internal BaseDiscordClient Discord { get; }

IntegrationIds = integrationIds;
internal RestConnection(BaseDiscordClient discord) {
Discord = discord;
}
internal static RestConnection Create(Model model)

internal static RestConnection Create(BaseDiscordClient discord, Model model)
{
var entity = new RestConnection(discord);
entity.Update(model);
return entity;
}

internal void Update(Model model)
{
return new RestConnection(model.Id, model.Type, model.Name, model.Revoked, model.Integrations.ToImmutableArray());
Id = model.Id;
Name = model.Name;
Type = model.Type;
IsRevoked = model.Revoked.IsSpecified ? model.Revoked.Value : null;
Integrations = model.Integrations.IsSpecified ?model.Integrations.Value
.Select(intergration => RestIntegration.Create(Discord, null, intergration)).ToImmutableArray() : null;
Verified = model.Verified;
FriendSync = model.FriendSync;
ShowActivity = model.ShowActivity;
Visibility = model.Visibility;
}

/// <summary>
@@ -40,6 +63,6 @@ namespace Discord.Rest
/// Name of the connection.
/// </returns>
public override string ToString() => Name;
private string DebuggerDisplay => $"{Name} ({Id}, {Type}{(IsRevoked ? ", Revoked" : "")})";
private string DebuggerDisplay => $"{Name} ({Id}, {Type}{(IsRevoked.GetValueOrDefault() ? ", Revoked" : "")})";
}
}

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

@@ -129,8 +129,10 @@ namespace Discord.Rest
/// <returns>
/// A string that resolves to Username#Discriminator of the user.
/// </returns>
public override string ToString() => Format.UsernameAndDiscriminator(this);
private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this)} ({Id}{(IsBot ? ", Bot" : "")})";
public override string ToString()
=> Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode);

private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode)} ({Id}{(IsBot ? ", Bot" : "")})";
#endregion

#region IUser


+ 14
- 0
src/Discord.Net.WebSocket/API/Gateway/IntegrationDeletedEvent.cs View File

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

namespace Discord.API.Gateway
{
internal class IntegrationDeletedEvent
{
[JsonProperty("id")]
public ulong Id { get; set; }
[JsonProperty("guild_id")]
public ulong GuildId { get; set; }
[JsonProperty("application_id")]
public Optional<ulong> ApplicationID { get; set; }
}
}

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

@@ -415,6 +415,32 @@ namespace Discord.WebSocket

#endregion

#region Integrations
/// <summary> Fired when an integration is created. </summary>
public event Func<IIntegration, Task> IntegrationCreated
{
add { _integrationCreated.Add(value); }
remove { _integrationCreated.Remove(value); }
}
internal readonly AsyncEvent<Func<IIntegration, Task>> _integrationCreated = new AsyncEvent<Func<IIntegration, Task>>();

/// <summary> Fired when an integration is updated. </summary>
public event Func<IIntegration, Task> IntegrationUpdated
{
add { _integrationUpdated.Add(value); }
remove { _integrationUpdated.Remove(value); }
}
internal readonly AsyncEvent<Func<IIntegration, Task>> _integrationUpdated = new AsyncEvent<Func<IIntegration, Task>>();

/// <summary> Fired when an integration is deleted. </summary>
public event Func<IGuild, ulong, Optional<ulong>, Task> IntegrationDeleted
{
add { _integrationDeleted.Add(value); }
remove { _integrationDeleted.Remove(value); }
}
internal readonly AsyncEvent<Func<IGuild, ulong, Optional<ulong>, Task>> _integrationDeleted = new AsyncEvent<Func<IGuild, ulong, Optional<ulong>, Task>>();
#endregion

#region Users
/// <summary> Fired when a user joins a guild. </summary>
public event Func<SocketGuildUser, Task> UserJoined
@@ -451,7 +477,7 @@ namespace Discord.WebSocket
remove { _userUpdatedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketUser, SocketUser, Task>> _userUpdatedEvent = new AsyncEvent<Func<SocketUser, SocketUser, Task>>();
/// <summary> Fired when a guild member is updated, or a member presence is updated. </summary>
/// <summary> Fired when a guild member is updated. </summary>
public event Func<Cacheable<SocketGuildUser, ulong>, SocketGuildUser, Task> GuildMemberUpdated
{
add { _guildMemberUpdatedEvent.Add(value); }


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

@@ -178,7 +178,6 @@ namespace Discord.WebSocket
await _shards[i].LogoutAsync();
}

CurrentUser = null;
if (_automaticShards)
{
_shardIds = new int[0];
@@ -450,6 +449,7 @@ namespace Discord.WebSocket
client.UserBanned += (user, guild) => _userBannedEvent.InvokeAsync(user, guild);
client.UserUnbanned += (user, guild) => _userUnbannedEvent.InvokeAsync(user, guild);
client.UserUpdated += (oldUser, newUser) => _userUpdatedEvent.InvokeAsync(oldUser, newUser);
client.PresenceUpdated += (user, oldPresence, newPresence) => _presenceUpdated.InvokeAsync(user, oldPresence, newPresence);
client.GuildMemberUpdated += (oldUser, newUser) => _guildMemberUpdatedEvent.InvokeAsync(oldUser, newUser);
client.UserVoiceStateUpdated += (user, oldVoiceState, newVoiceState) => _userVoiceStateUpdatedEvent.InvokeAsync(user, oldVoiceState, newVoiceState);
client.VoiceServerUpdated += (server) => _voiceServerUpdatedEvent.InvokeAsync(server);


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

@@ -1311,7 +1311,7 @@ namespace Discord.WebSocket
else
{
user = guild.AddOrUpdateUser(data);
var cacheableBefore = new Cacheable<SocketGuildUser, ulong>(user, user.Id, true, () => null);
var cacheableBefore = new Cacheable<SocketGuildUser, ulong>(null, user.Id, false, () => null);
await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false);
}
}
@@ -2017,6 +2017,92 @@ namespace Discord.WebSocket
break;
#endregion

#region Integrations
case "INTEGRATION_CREATE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (INTEGRATION_CREATE)").ConfigureAwait(false);

var data = (payload as JToken).ToObject<Integration>(_serializer);

// Integrations from Gateway should always have guild IDs specified.
if (!data.GuildId.IsSpecified)
return;

var guild = State.GetGuild(data.GuildId.Value);

if (guild != null)
{
if (!guild.IsSynced)
{
await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false);
return;
}

await TimedInvokeAsync(_integrationCreated, nameof(IntegrationCreated), RestIntegration.Create(this, guild, data)).ConfigureAwait(false);
}
else
{
await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false);
return;
}
}
break;
case "INTEGRATION_UPDATE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (INTEGRATION_UPDATE)").ConfigureAwait(false);

var data = (payload as JToken).ToObject<Integration>(_serializer);

// Integrations from Gateway should always have guild IDs specified.
if (!data.GuildId.IsSpecified)
return;

var guild = State.GetGuild(data.GuildId.Value);

if (guild != null)
{
if (!guild.IsSynced)
{
await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false);
return;
}

await TimedInvokeAsync(_integrationUpdated, nameof(IntegrationUpdated), RestIntegration.Create(this, guild, data)).ConfigureAwait(false);
}
else
{
await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false);
return;
}
}
break;
case "INTEGRATION_DELETE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (INTEGRATION_DELETE)").ConfigureAwait(false);

var data = (payload as JToken).ToObject<IntegrationDeletedEvent>(_serializer);

var guild = State.GetGuild(data.GuildId);

if (guild != null)
{
if (!guild.IsSynced)
{
await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false);
return;
}

await TimedInvokeAsync(_integrationDeleted, nameof(IntegrationDeleted), guild, data.Id, data.ApplicationID).ConfigureAwait(false);
}
else
{
await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false);
return;
}
}
break;
#endregion

#region Users
case "USER_UPDATE":
{
@@ -2245,7 +2331,7 @@ namespace Discord.WebSocket

SocketUser user = data.User.IsSpecified
? State.GetOrAddUser(data.User.Value.Id, (_) => SocketGlobalUser.Create(this, State, data.User.Value))
: guild.AddOrUpdateUser(data.Member.Value);
: guild?.AddOrUpdateUser(data.Member.Value); // null if the bot scope isn't set, so the guild cannot be retrieved.

SocketChannel channel = null;
if(data.ChannelId.IsSpecified)
@@ -2260,8 +2346,12 @@ namespace Discord.WebSocket
}
else
{
await UnknownChannelAsync(type, data.ChannelId.Value).ConfigureAwait(false);
return;
if (guild != null) // The guild id is set, but the guild cannot be found as the bot scope is not set.
{
await UnknownChannelAsync(type, data.ChannelId.Value).ConfigureAwait(false);
return;
}
// The channel isnt required when responding to an interaction, so we can leave the channel null.
}
}
}


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

@@ -24,7 +24,29 @@ namespace Discord.WebSocket
/// <summary>
/// Gets the owner of the current thread.
/// </summary>
public SocketThreadUser Owner { get; private set; }
public SocketThreadUser Owner
{
get
{
lock (_ownerLock)
{
var user = GetUser(_ownerId);

if (user == null)
{
var guildMember = Guild.GetUser(_ownerId);
if (guildMember == null)
return null;

user = SocketThreadUser.Create(Guild, this, guildMember);
_members[user.Id] = user;
return user;
}
else
return user;
}
}
}

/// <summary>
/// Gets the current users within this thread.
@@ -83,6 +105,9 @@ namespace Discord.WebSocket
private bool _usersDownloaded;

private readonly object _downloadLock = new object();
private readonly object _ownerLock = new object();

private ulong _ownerId;

internal SocketThreadChannel(DiscordSocketClient discord, SocketGuild guild, ulong id, SocketGuildChannel parent,
DateTimeOffset? createdAt)
@@ -96,7 +121,7 @@ namespace Discord.WebSocket
internal new static SocketThreadChannel Create(SocketGuild guild, ClientState state, Model model)
{
var parent = guild.GetChannel(model.CategoryId.Value);
var entity = new SocketThreadChannel(guild.Discord, guild, model.Id, parent, model.ThreadMetadata.GetValueOrDefault()?.CreatedAt.ToNullable());
var entity = new SocketThreadChannel(guild.Discord, guild, model.Id, parent, model.ThreadMetadata.GetValueOrDefault()?.CreatedAt.GetValueOrDefault(null));
entity.Update(state, model);
return entity;
}
@@ -120,7 +145,7 @@ namespace Discord.WebSocket

if (model.OwnerId.IsSpecified)
{
Owner = GetUser(model.OwnerId.Value);
_ownerId = model.OwnerId.Value;
}

HasJoined = model.ThreadMember.IsSpecified;


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

@@ -847,10 +847,10 @@ namespace Discord.WebSocket
#endregion

#region Integrations
public Task<IReadOnlyCollection<RestGuildIntegration>> GetIntegrationsAsync(RequestOptions options = null)
public Task<IReadOnlyCollection<RestIntegration>> GetIntegrationsAsync(RequestOptions options = null)
=> GuildHelper.GetIntegrationsAsync(this, Discord, options);
public Task<RestGuildIntegration> CreateIntegrationAsync(ulong id, string type, RequestOptions options = null)
=> GuildHelper.CreateIntegrationAsync(this, Discord, id, type, options);
public Task DeleteIntegrationAsync(ulong id, RequestOptions options = null)
=> GuildHelper.DeleteIntegrationAsync(this, Discord, id, options);
#endregion

#region Interactions
@@ -999,10 +999,6 @@ namespace Discord.WebSocket
return null;
}

/// <inheritdoc />
public Task<RestRole> CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?),
bool isHoisted = false, RequestOptions options = null)
=> GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, false, options);
/// <summary>
/// Creates a new role with the provided name.
/// </summary>
@@ -1892,11 +1888,11 @@ namespace Discord.WebSocket
=> await GetVoiceRegionsAsync(options).ConfigureAwait(false);

/// <inheritdoc />
async Task<IReadOnlyCollection<IGuildIntegration>> IGuild.GetIntegrationsAsync(RequestOptions options)
async Task<IReadOnlyCollection<IIntegration>> IGuild.GetIntegrationsAsync(RequestOptions options)
=> await GetIntegrationsAsync(options).ConfigureAwait(false);
/// <inheritdoc />
async Task<IGuildIntegration> IGuild.CreateIntegrationAsync(ulong id, string type, RequestOptions options)
=> await CreateIntegrationAsync(id, type, options).ConfigureAwait(false);
async Task IGuild.DeleteIntegrationAsync(ulong id, RequestOptions options)
=> await DeleteIntegrationAsync(id, options).ConfigureAwait(false);

/// <inheritdoc />
async Task<IReadOnlyCollection<IInviteMetadata>> IGuild.GetInvitesAsync(RequestOptions options)


+ 2
- 14
src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs View File

@@ -202,12 +202,7 @@ namespace Discord.WebSocket
HasResponded = true;
}

/// <summary>
/// Updates the message which this component resides in with the type <see cref="InteractionResponseType.UpdateMessage"/>
/// </summary>
/// <param name="func">A delegate containing the properties to modify the message with.</param>
/// <param name="options">The request options for this <see langword="async"/> request.</param>
/// <returns>A task that represents the asynchronous operation of updating the message.</returns>
/// <inheritdoc/>
public async Task UpdateAsync(Action<MessageProperties> func, RequestOptions options = null)
{
var args = new MessageProperties();
@@ -383,14 +378,7 @@ namespace Discord.WebSocket
return await InteractionHelper.SendFollowupAsync(Discord, args, Token, Channel, options).ConfigureAwait(false);
}

/// <summary>
/// Defers an interaction and responds with type 5 (<see cref="InteractionResponseType.DeferredChannelMessageWithSource"/>)
/// </summary>
/// <param name="ephemeral"><see langword="true"/> to send this message ephemerally, otherwise <see langword="false"/>.</param>
/// <param name="options">The request options for this <see langword="async"/> request.</param>
/// <returns>
/// A task that represents the asynchronous operation of acknowledging the interaction.
/// </returns>
/// <inheritdoc/>
public async Task DeferLoadingAsync(bool ephemeral = false, RequestOptions options = null)
{
if (!InteractionHelper.CanSendResponse(this))


+ 16
- 6
src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs View File

@@ -19,13 +19,25 @@ namespace Discord.WebSocket
/// Gets the <see cref="ISocketMessageChannel"/> this interaction was used in.
/// </summary>
/// <remarks>
/// If the channel isn't cached or the bot doesn't have access to it then
/// If the channel isn't cached, the bot scope isn't used, or the bot doesn't have access to it then
/// this property will be <see langword="null"/>.
/// </remarks>
public ISocketMessageChannel Channel { get; private set; }

/// <summary>
/// Gets the ID of the channel this interaction was used in.
/// </summary>
/// <remarks>
/// This property is exposed in cases where the bot scope is not provided, so the channel entity cannot be retrieved.
/// <br />
/// To get the channel, you can call <see cref="GetChannelAsync(RequestOptions)"/>
/// as this method makes a request for a <see cref="RestChannel"/> if nothing was found in cache.
/// </remarks>
public ulong? ChannelId { get; private set; }

/// <summary>
/// Gets the <see cref="SocketUser"/> who triggered this interaction.
/// This property will be <see langword="null"/> if the bot scope isn't used.
/// </summary>
public SocketUser User { get; private set; }

@@ -62,8 +74,6 @@ namespace Discord.WebSocket
/// <inheritdoc/>
public bool IsDMInteraction { get; private set; }

private ulong? _channelId;

internal SocketInteraction(DiscordSocketClient client, ulong id, ISocketMessageChannel channel, SocketUser user)
: base(client, id)
{
@@ -111,7 +121,7 @@ namespace Discord.WebSocket
{
IsDMInteraction = !model.GuildId.IsSpecified;

_channelId = model.ChannelId.ToNullable();
ChannelId = model.ChannelId.ToNullable();

Data = model.Data.IsSpecified
? model.Data.Value
@@ -396,12 +406,12 @@ namespace Discord.WebSocket
if (Channel != null)
return Channel;

if (!_channelId.HasValue)
if (!ChannelId.HasValue)
return null;

try
{
return (IMessageChannel)await Discord.GetChannelAsync(_channelId.Value, options).ConfigureAwait(false);
return (IMessageChannel)await Discord.GetChannelAsync(ChannelId.Value, options).ConfigureAwait(false);
}
catch(HttpException ex) when (ex.DiscordCode == DiscordErrorCode.MissingPermissions) { return null; } // bot can't view that channel, return null instead of throwing.
}


+ 11
- 0
src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs View File

@@ -147,6 +147,17 @@ namespace Discord.WebSocket
return entity;
}

internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, SocketGuildUser owner)
{
// this is used for creating the owner of the thread.
var entity = new SocketThreadUser(guild, thread, owner, owner.Id);
entity.Update(new Model
{
JoinTimestamp = thread.CreatedAt,
});
return entity;
}

internal void Update(Model model)
{
ThreadJoinedAt = model.JoinTimestamp;


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

@@ -117,8 +117,8 @@ namespace Discord.WebSocket
/// <returns>
/// The full name of the user.
/// </returns>
public override string ToString() => Format.UsernameAndDiscriminator(this);
private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this)} ({Id}{(IsBot ? ", Bot" : "")})";
public override string ToString() => Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode);
private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode)} ({Id}{(IsBot ? ", Bot" : "")})";
internal SocketUser Clone() => MemberwiseClone() as SocketUser;
}
}

+ 31
- 31
src/Discord.Net/Discord.Net.nuspec View File

@@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Discord.Net</id>
<version>3.4.0$suffix$</version>
<version>3.4.1$suffix$</version>
<title>Discord.Net</title>
<authors>Discord.Net Contributors</authors>
<owners>foxbot</owners>
@@ -14,44 +14,44 @@
<iconUrl>https://github.com/RogueException/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png</iconUrl>
<dependencies>
<group targetFramework="net6.0">
<dependency id="Discord.Net.Core" version="3.4.0$suffix$" />
<dependency id="Discord.Net.Rest" version="3.4.0$suffix$" />
<dependency id="Discord.Net.WebSocket" version="3.4.0$suffix$" />
<dependency id="Discord.Net.Commands" version="3.4.0$suffix$" />
<dependency id="Discord.Net.Webhook" version="3.4.0$suffix$" />
<dependency id="Discord.Net.Interactions" version="3.4.0$suffix$" />
<dependency id="Discord.Net.Core" version="3.4.1$suffix$" />
<dependency id="Discord.Net.Rest" version="3.4.1$suffix$" />
<dependency id="Discord.Net.WebSocket" version="3.4.1$suffix$" />
<dependency id="Discord.Net.Commands" version="3.4.1$suffix$" />
<dependency id="Discord.Net.Webhook" version="3.4.1$suffix$" />
<dependency id="Discord.Net.Interactions" version="3.4.1$suffix$" />
</group>
<group targetFramework="net5.0">
<dependency id="Discord.Net.Core" version="3.4.0$suffix$" />
<dependency id="Discord.Net.Rest" version="3.4.0$suffix$" />
<dependency id="Discord.Net.WebSocket" version="3.4.0$suffix$" />
<dependency id="Discord.Net.Commands" version="3.4.0$suffix$" />
<dependency id="Discord.Net.Webhook" version="3.4.0$suffix$" />
<dependency id="Discord.Net.Interactions" version="3.4.0$suffix$" />
<dependency id="Discord.Net.Core" version="3.4.1$suffix$" />
<dependency id="Discord.Net.Rest" version="3.4.1$suffix$" />
<dependency id="Discord.Net.WebSocket" version="3.4.1$suffix$" />
<dependency id="Discord.Net.Commands" version="3.4.1$suffix$" />
<dependency id="Discord.Net.Webhook" version="3.4.1$suffix$" />
<dependency id="Discord.Net.Interactions" version="3.4.1$suffix$" />
</group>
<group targetFramework="net461">
<dependency id="Discord.Net.Core" version="3.4.0$suffix$" />
<dependency id="Discord.Net.Rest" version="3.4.0$suffix$" />
<dependency id="Discord.Net.WebSocket" version="3.4.0$suffix$" />
<dependency id="Discord.Net.Commands" version="3.4.0$suffix$" />
<dependency id="Discord.Net.Webhook" version="3.4.0$suffix$" />
<dependency id="Discord.Net.Interactions" version="3.4.0$suffix$" />
<dependency id="Discord.Net.Core" version="3.4.1$suffix$" />
<dependency id="Discord.Net.Rest" version="3.4.1$suffix$" />
<dependency id="Discord.Net.WebSocket" version="3.4.1$suffix$" />
<dependency id="Discord.Net.Commands" version="3.4.1$suffix$" />
<dependency id="Discord.Net.Webhook" version="3.4.1$suffix$" />
<dependency id="Discord.Net.Interactions" version="3.4.1$suffix$" />
</group>
<group targetFramework="netstandard2.0">
<dependency id="Discord.Net.Core" version="3.4.0$suffix$" />
<dependency id="Discord.Net.Rest" version="3.4.0$suffix$" />
<dependency id="Discord.Net.WebSocket" version="3.4.0$suffix$" />
<dependency id="Discord.Net.Commands" version="3.4.0$suffix$" />
<dependency id="Discord.Net.Webhook" version="3.4.0$suffix$" />
<dependency id="Discord.Net.Interactions" version="3.4.0$suffix$" />
<dependency id="Discord.Net.Core" version="3.4.1$suffix$" />
<dependency id="Discord.Net.Rest" version="3.4.1$suffix$" />
<dependency id="Discord.Net.WebSocket" version="3.4.1$suffix$" />
<dependency id="Discord.Net.Commands" version="3.4.1$suffix$" />
<dependency id="Discord.Net.Webhook" version="3.4.1$suffix$" />
<dependency id="Discord.Net.Interactions" version="3.4.1$suffix$" />
</group>
<group targetFramework="netstandard2.1">
<dependency id="Discord.Net.Core" version="3.4.0$suffix$" />
<dependency id="Discord.Net.Rest" version="3.4.0$suffix$" />
<dependency id="Discord.Net.WebSocket" version="3.4.0$suffix$" />
<dependency id="Discord.Net.Commands" version="3.4.0$suffix$" />
<dependency id="Discord.Net.Webhook" version="3.4.0$suffix$" />
<dependency id="Discord.Net.Interactions" version="3.4.0$suffix$" />
<dependency id="Discord.Net.Core" version="3.4.1$suffix$" />
<dependency id="Discord.Net.Rest" version="3.4.1$suffix$" />
<dependency id="Discord.Net.WebSocket" version="3.4.1$suffix$" />
<dependency id="Discord.Net.Commands" version="3.4.1$suffix$" />
<dependency id="Discord.Net.Webhook" version="3.4.1$suffix$" />
<dependency id="Discord.Net.Interactions" version="3.4.1$suffix$" />
</group>
</dependencies>
</metadata>

Loading…
Cancel
Save