Browse Source

Merge 07f74b70b4 into 9e0d99106d

pull/1340/merge
Christopher F GitHub 5 years ago
parent
commit
b1e0ee5cef
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 1705 additions and 41 deletions
  1. +3
    -1
      .gitignore
  2. +73
    -0
      Discord.Net.sln
  3. +1
    -1
      LICENSE
  4. +2
    -39
      README.md
  5. +37
    -0
      TODO
  6. +0
    -0
      doc/.gitkeep
  7. +106
    -0
      ep.txt
  8. +0
    -0
      sample/.gitkeep
  9. +74
    -0
      sample/idn/Inspector.cs
  10. +86
    -0
      sample/idn/Program.cs
  11. +16
    -0
      sample/idn/idn.csproj
  12. +21
    -0
      src/Discord.Net/Discord.Net.csproj
  13. +37
    -0
      src/Discord.Net/DiscordClient.cs
  14. +51
    -0
      src/Discord.Net/DiscordConfig.cs
  15. +24
    -0
      src/Discord.Net/Entities/Snowflake.cs
  16. +28
    -0
      src/Discord.Net/IDiscordClient.cs
  17. +23
    -0
      src/Discord.Net/Rest/DiscordHttpClientHandler.cs
  18. +47
    -0
      src/Discord.Net/Rest/DiscordRestApi.cs
  19. +34
    -0
      src/Discord.Net/Rest/IDiscordRestApi.cs
  20. +59
    -0
      src/Discord.Net/Rest/Models/Channel/Channel.cs
  21. +13
    -0
      src/Discord.Net/Rest/Models/Channel/ChannelType.cs
  22. +25
    -0
      src/Discord.Net/Rest/Models/GatewayInfo.cs
  23. +36
    -0
      src/Discord.Net/Rest/Models/Permissions/ChannelPermissions.cs
  24. +45
    -0
      src/Discord.Net/Rest/Models/Permissions/GuildPermissions.cs
  25. +16
    -0
      src/Discord.Net/Rest/Models/Permissions/Overwrite.cs
  26. +8
    -0
      src/Discord.Net/Rest/Models/Permissions/PermissionTarget.cs
  27. +20
    -0
      src/Discord.Net/Rest/Models/Users/AccountFlags.cs
  28. +8
    -0
      src/Discord.Net/Rest/Models/Users/PremiumType.cs
  29. +33
    -0
      src/Discord.Net/Rest/Models/Users/User.cs
  30. +33
    -0
      src/Discord.Net/Rest/Models/Webhook/Webhook.cs
  31. +8
    -0
      src/Discord.Net/Rest/Models/Webhook/WebhookType.cs
  32. +49
    -0
      src/Discord.Net/Serialization/JsonContentSerializer.cs
  33. +41
    -0
      src/Discord.Net/Serialization/OptionalConverter.cs
  34. +45
    -0
      src/Discord.Net/Socket/DiscordGatewayApi.cs
  35. +35
    -0
      src/Discord.Net/Socket/ISocket.cs
  36. +196
    -0
      src/Discord.Net/Socket/Providers/DefaultSocket.cs
  37. +159
    -0
      src/Discord.Net/Utilities/Logging.cs
  38. +62
    -0
      src/Discord.Net/Utilities/Optional.cs
  39. +0
    -0
      test/.gitkeep
  40. +20
    -0
      test/Discord.Tests.Unit/Discord.Tests.Unit.csproj
  41. +118
    -0
      test/Discord.Tests.Unit/Serialization/OptionalConverterTests.cs
  42. +13
    -0
      test/Discord.Tests.Unit/UnitTest1.cs

+ 3
- 1
.gitignore View File

@@ -206,4 +206,6 @@ docs/api/\.manifest
\.idea/ \.idea/


# Codealike UID # Codealike UID
codealike.json
codealike.json

*.ignore

+ 73
- 0
Discord.Net.sln View File

@@ -0,0 +1,73 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26124.0
MinimumVisualStudioVersion = 15.0.26124.0
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5DAC796B-0B77-4F84-B790-83DB78C6DFFE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net", "src\Discord.Net\Discord.Net.csproj", "{3194F5DC-C0AF-4459-AAA3-91CB8FB8C370}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample", "sample", "{4795640A-030C-4A9A-A9B0-20C56AF4DA3F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "idn", "sample\idn\idn.csproj", "{5BE5DE89-53B7-4243-AEA8-FD8A6420908A}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{68EE1EAC-F487-4BAC-917B-233370B3AEA1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Tests.Unit", "test\Discord.Tests.Unit\Discord.Tests.Unit.csproj", "{6AD4FF67-D45E-4E7E-8853-990390D35C9F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{3194F5DC-C0AF-4459-AAA3-91CB8FB8C370}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3194F5DC-C0AF-4459-AAA3-91CB8FB8C370}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3194F5DC-C0AF-4459-AAA3-91CB8FB8C370}.Debug|x64.ActiveCfg = Debug|Any CPU
{3194F5DC-C0AF-4459-AAA3-91CB8FB8C370}.Debug|x64.Build.0 = Debug|Any CPU
{3194F5DC-C0AF-4459-AAA3-91CB8FB8C370}.Debug|x86.ActiveCfg = Debug|Any CPU
{3194F5DC-C0AF-4459-AAA3-91CB8FB8C370}.Debug|x86.Build.0 = Debug|Any CPU
{3194F5DC-C0AF-4459-AAA3-91CB8FB8C370}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3194F5DC-C0AF-4459-AAA3-91CB8FB8C370}.Release|Any CPU.Build.0 = Release|Any CPU
{3194F5DC-C0AF-4459-AAA3-91CB8FB8C370}.Release|x64.ActiveCfg = Release|Any CPU
{3194F5DC-C0AF-4459-AAA3-91CB8FB8C370}.Release|x64.Build.0 = Release|Any CPU
{3194F5DC-C0AF-4459-AAA3-91CB8FB8C370}.Release|x86.ActiveCfg = Release|Any CPU
{3194F5DC-C0AF-4459-AAA3-91CB8FB8C370}.Release|x86.Build.0 = Release|Any CPU
{5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Debug|x64.ActiveCfg = Debug|Any CPU
{5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Debug|x64.Build.0 = Debug|Any CPU
{5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Debug|x86.ActiveCfg = Debug|Any CPU
{5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Debug|x86.Build.0 = Debug|Any CPU
{5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Release|Any CPU.Build.0 = Release|Any CPU
{5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Release|x64.ActiveCfg = Release|Any CPU
{5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Release|x64.Build.0 = Release|Any CPU
{5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Release|x86.ActiveCfg = Release|Any CPU
{5BE5DE89-53B7-4243-AEA8-FD8A6420908A}.Release|x86.Build.0 = Release|Any CPU
{6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Debug|x64.ActiveCfg = Debug|Any CPU
{6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Debug|x64.Build.0 = Debug|Any CPU
{6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Debug|x86.ActiveCfg = Debug|Any CPU
{6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Debug|x86.Build.0 = Debug|Any CPU
{6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Release|Any CPU.Build.0 = Release|Any CPU
{6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Release|x64.ActiveCfg = Release|Any CPU
{6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Release|x64.Build.0 = Release|Any CPU
{6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Release|x86.ActiveCfg = Release|Any CPU
{6AD4FF67-D45E-4E7E-8853-990390D35C9F}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{3194F5DC-C0AF-4459-AAA3-91CB8FB8C370} = {5DAC796B-0B77-4F84-B790-83DB78C6DFFE}
{5BE5DE89-53B7-4243-AEA8-FD8A6420908A} = {4795640A-030C-4A9A-A9B0-20C56AF4DA3F}
{6AD4FF67-D45E-4E7E-8853-990390D35C9F} = {68EE1EAC-F487-4BAC-917B-233370B3AEA1}
EndGlobalSection
EndGlobal

+ 1
- 1
LICENSE View File

@@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)


Copyright (c) 2015-2017 Discord.Net Contributors
Copyright (c) 2015-2020 Discord.Net Contributors


Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal


+ 2
- 39
README.md View File

@@ -1,40 +1,3 @@
# Discord.Net
[![NuGet](https://img.shields.io/nuget/vpre/Discord.Net.svg?maxAge=2592000?style=plastic)](https://www.nuget.org/packages/Discord.Net)
[![MyGet](https://img.shields.io/myget/discord-net/vpre/Discord.Net.svg)](https://www.myget.org/feed/Packages/discord-net)
[![Build status](https://ci.appveyor.com/api/projects/status/5sb7n8a09w9clute/branch/dev?svg=true)](https://ci.appveyor.com/project/RogueException/discord-net/branch/dev)
[![Discord](https://discordapp.com/api/guilds/81384788765712384/widget.png)](https://discord.gg/jkrBmQR)
# Discord.Net 2020


An unofficial .NET API Wrapper for the Discord client (http://discordapp.com).

Check out the [documentation](https://discord.foxbot.me/docs/) or join the [Discord API Chat](https://discord.gg/jkrBmQR).

## Installation
### Stable (NuGet)
Our stable builds available from NuGet through the Discord.Net metapackage:
- [Discord.Net](https://www.nuget.org/packages/Discord.Net/)

The individual components may also be installed from NuGet:
- [Discord.Net.Commands](https://www.nuget.org/packages/Discord.Net.Commands/)
- [Discord.Net.Rest](https://www.nuget.org/packages/Discord.Net.Rest/)
- [Discord.Net.WebSocket](https://www.nuget.org/packages/Discord.Net.WebSocket/)
- [Discord.Net.Webhook](https://www.nuget.org/packages/Discord.Net.Webhook/)

### Unstable (MyGet)
Nightly builds are available through our MyGet feed (`https://www.myget.org/F/discord-net/api/v3/index.json`).

## Compiling
In order to compile Discord.Net, you require the following:

### Using Visual Studio
- [Visual Studio 2017](https://www.microsoft.com/net/core#windowsvs2017)
- [.NET Core SDK](https://www.microsoft.com/net/download/core)

The .NET Core workload must be selected during Visual Studio installation.

### Using Command Line
- [.NET Core SDK](https://www.microsoft.com/net/download/core)

## Known Issues

### WebSockets (Win7 and earlier)
.NET Core 1.1 does not support WebSockets on Win7 and earlier. This issue has been fixed since the release of .NET Core 2.1. It is recommended to target .NET Core 2.1 or above for your project if you wish to run your bot on legacy platforms; alternatively, you may choose to install the [Discord.Net.Providers.WS4Net](https://www.nuget.org/packages/Discord.Net.Providers.WS4Net/) package.
Rewrite branch, work in progress.

+ 37
- 0
TODO View File

@@ -0,0 +1,37 @@
- REST
- Models
- Preconditions
- Endpoints
- Channel
- Emoji
- Guild
- Invite
- User
- Voice
- Webhook
- Ratelimiter with refit
- Gateway
- Models
- Client
- Socket
- use token
* Receive
* Compression
- Voice (long)
- Core
- CDN
- Datastore
- Entities
- Channel
- Emoji
- Guild
- User
- Utilities
- Token Validation (port from @ChrisJ)
- Tests
- Unit test Gateway stability / deadlockability?
- Port ChrisJ's token validator tests
- Extensions
- Commands
? design - use finite's or quahu's
- Interactivity

+ 0
- 0
doc/.gitkeep View File


+ 106
- 0
ep.txt View File

@@ -0,0 +1,106 @@
https://gist.github.com/SinisterRectus/9518f3e7d0d1ccb4335b2a0d389c30b0

Sorted By Route
--------------------------------------------------------------------------------------------------------------------
Get Entitlements GET /applications/{application.id}/entitlements
Get Entitlement GET /applications/{application.id}/entitlements/{entitlement.id}
Delete Test Entitlement DELETE /applications/{application.id}/entitlements/{entitlement.id}/
Consume SKU POST /applications/{application.id}/entitlements/{entitlement.id}/consume
Get SKUs GET /applications/{application.id}/skus
Delete/Close Channel DELETE /channels/{channel.id}
Get Channel GET /channels/{channel.id}
Modify Channel PUT/PATCH /channels/{channel.id}
Get Channel Invites GET /channels/{channel.id}/invites
Create Channel Invite POST /channels/{channel.id}/invites
Get Channel Messages GET /channels/{channel.id}/messages
Create Message POST /channels/{channel.id}/messages
Bulk Delete Messages POST /channels/{channel.id}/messages/bulk-delete
Bulk Delete Messages (deprecated) POST /channels/{channel.id}/messages/bulk_delete
Delete Message DELETE /channels/{channel.id}/messages/{message.id}
Get Channel Message GET /channels/{channel.id}/messages/{message.id}
Edit Message PATCH /channels/{channel.id}/messages/{message.id}
Delete All Reactions DELETE /channels/{channel.id}/messages/{message.id}/reactions
Get Reactions GET /channels/{channel.id}/messages/{message.id}/reactions/{emoji}
Delete Own Reaction DELETE /channels/{channel.id}/messages/{message.id}/reactions/{emoji}/@me
Create Reaction PUT /channels/{channel.id}/messages/{message.id}/reactions/{emoji}/@me
Delete User Reaction DELETE /channels/{channel.id}/messages/{message.id}/reactions/{emoji}/{user.id}
Delete Channel Permission DELETE /channels/{channel.id}/permissions/{overwrite.id}
Edit Channel Permissions PUT /channels/{channel.id}/permissions/{overwrite.id}
Get Pinned Messages GET /channels/{channel.id}/pins
Delete Pinned Channel Message DELETE /channels/{channel.id}/pins/{message.id}
Add Pinned Channel Message PUT /channels/{channel.id}/pins/{message.id}
Group DM Remove Recipient DELETE /channels/{channel.id}/recipients/{user.id}
Group DM Add Recipient PUT /channels/{channel.id}/recipients/{user.id}
Trigger Typing Indicator POST /channels/{channel.id}/typing
Get Channel Webhooks GET /channels/{channel.id}/webhooks
Create Webhook POST /channels/{channel.id}/webhooks
Get Gateway GET /gateway
Get Gateway Bot GET /gateway/bot
Create Guild POST /guilds
Delete Guild DELETE /guilds/{guild.id}
Get Guild GET /guilds/{guild.id}
Modify Guild PATCH /guilds/{guild.id}
Get Guild Audit Log GET /guilds/{guild.id}/audit-logs
Get Guild Bans GET /guilds/{guild.id}/bans
Remove Guild Ban DELETE /guilds/{guild.id}/bans/{user.id}
Get Guild Ban GET /guilds/{guild.id}/bans/{user.id}
Create Guild Ban PUT /guilds/{guild.id}/bans/{user.id}
Get Guild Channels GET /guilds/{guild.id}/channels
Modify Guild Channel Positions PATCH /guilds/{guild.id}/channels
Create Guild Channel POST /guilds/{guild.id}/channels
Get Guild Embed GET /guilds/{guild.id}/embed
Modify Guild Embed PATCH /guilds/{guild.id}/embed
List Guild Emojis GET /guilds/{guild.id}/emojis
Create Guild Emoji POST /guilds/{guild.id}/emojis
Delete Guild Emoji DELETE /guilds/{guild.id}/emojis/{emoji.id}
Get Guild Emoji GET /guilds/{guild.id}/emojis/{emoji.id}
Modify Guild Emoji PATCH /guilds/{guild.id}/emojis/{emoji.id}
Get Guild Integrations GET /guilds/{guild.id}/integrations
Create Guild Integration POST /guilds/{guild.id}/integrations
Delete Guild Integration DELETE /guilds/{guild.id}/integrations/{integration.id}
Modify Guild Integration PATCH /guilds/{guild.id}/integrations/{integration.id}
Sync Guild Integration POST /guilds/{guild.id}/integrations/{integration.id}/sync
Get Guild Invites GET /guilds/{guild.id}/invites
List Guild Members GET /guilds/{guild.id}/members
Modify Current User Nick PATCH /guilds/{guild.id}/members/@me/nick
Remove Guild Member DELETE /guilds/{guild.id}/members/{user.id}
Get Guild Member GET /guilds/{guild.id}/members/{user.id}
Modify Guild Member PATCH /guilds/{guild.id}/members/{user.id}
Add Guild Member PUT /guilds/{guild.id}/members/{user.id}
Remove Guild Member Role DELETE /guilds/{guild.id}/members/{user.id}/roles/{role.id}
Add Guild Member Role PUT /guilds/{guild.id}/members/{user.id}/roles/{role.id}
Get Guild Prune Count GET /guilds/{guild.id}/prune
Begin Guild Prune POST /guilds/{guild.id}/prune
Get Guild Voice Regions GET /guilds/{guild.id}/regions
Get Guild Roles GET /guilds/{guild.id}/roles
Modify Guild Role Positions PATCH /guilds/{guild.id}/roles
Create Guild Role POST /guilds/{guild.id}/roles
Delete Guild Role DELETE /guilds/{guild.id}/roles/{role.id}
Modify Guild Role PATCH /guilds/{guild.id}/roles/{role.id}
Get Guild Vanity URL GET /guilds/{guild.id}/vanity-url
Get Guild Webhooks GET /guilds/{guild.id}/webhooks
Get Guild Widget Image GET /guilds/{guild.id}/widget.png
Delete Invite DELETE /invites/{invite.code}
Get Invite GET /invites/{invite.code}
Get Current Application Information GET /oauth2/applications/@me
Delete Purchase Discount DELETE /store/skus/{sku.id}/discounts/{user.id}/
Create Purchase Discount PUT /store/skus/{sku.id}/discounts/{user.id}/
Get Current User GET /users/@me
Modify Current User PATCH /users/@me
Get User DMs GET /users/@me/channels
Create DM POST /users/@me/channels
Create Group DM POST /users/@me/channels
Get User Connections GET /users/@me/connections
Get Current User Guilds GET /users/@me/guilds
Leave Guild DELETE /users/@me/guilds/{guild.id}
Get User GET /users/{user.id}
List Voice Regions GET /voice/regions
Delete Webhook DELETE /webhooks/{webhook.id}
Get Webhook GET /webhooks/{webhook.id}
Modify Webhook PATCH /webhooks/{webhook.id}
Delete Webhook with Token DELETE /webhooks/{webhook.id}/{webhook.token}
Get Webhook with Token GET /webhooks/{webhook.id}/{webhook.token}
Modify Webhook with Token PATCH /webhooks/{webhook.id}/{webhook.token}
Execute Webhook POST /webhooks/{webhook.id}/{webhook.token}
Execute GitHub-Compatible Webhook POST /webhooks/{webhook.id}/{webhook.token}/github
Execute Slack-Compatible Webhook POST /webhooks/{webhook.id}/{webhook.token}/slack

+ 0
- 0
sample/.gitkeep View File


+ 74
- 0
sample/idn/Inspector.cs View File

@@ -0,0 +1,74 @@
using System.Collections;
using System.Linq;
using System.Reflection;
using System.Text;

namespace idn
{
public static class Inspector
{
public static string Inspect(object value)
{
var builder = new StringBuilder();
if (value != null)
{
var type = value.GetType().GetTypeInfo();
builder.AppendLine($"[{type.Namespace}.{type.Name}]");
builder.AppendLine($"{InspectProperty(value)}");

if (value is IEnumerable)
{
var items = (value as IEnumerable).Cast<object>().ToArray();
if (items.Length > 0)
{
builder.AppendLine();
foreach (var item in items)
builder.AppendLine($"- {InspectProperty(item)}");
}
}
else
{
var groups = type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(x => x.GetIndexParameters().Length == 0)
.GroupBy(x => x.Name)
.OrderBy(x => x.Key)
.ToArray();
if (groups.Length > 0)
{
builder.AppendLine();
int pad = groups.Max(x => x.Key.Length) + 1;
foreach (var group in groups)
builder.AppendLine($"{group.Key.PadRight(pad, ' ')}{InspectProperty(group.First().GetValue(value))}");
}
}
}
else
builder.AppendLine("null");
return builder.ToString();
}

private static string InspectProperty(object obj)
{
if (obj == null)
return "null";

var type = obj.GetType();

var debuggerDisplay = type.GetProperty("DebuggerDisplay", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (debuggerDisplay != null)
return debuggerDisplay.GetValue(obj).ToString();

var toString = type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Where(x => x.Name == "ToString" && x.DeclaringType != typeof(object))
.FirstOrDefault();
if (toString != null)
return obj.ToString();

var count = type.GetProperty("Count", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (count != null)
return $"[{count.GetValue(obj)} Items]";

return obj.ToString();
}
}
}

+ 86
- 0
sample/idn/Program.cs View File

@@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
using Discord;

namespace idn
{
public class Program
{
public static readonly string[] Imports =
{
"System",
"System.Collections.Generic",
"System.Linq",
"System.Threading.Tasks",
"System.Diagnostics",
"System.IO",
"Discord",
"Discord.Rest",
"Discord.Socket",
"idn"
};

static async Task Main(string[] args)
{
var token = File.ReadAllText("token.ignore");
var client = IDiscordClient.Create(token);
// client.start

var options = ScriptOptions.Default
.AddReferences(GetAssemblies().ToArray())
.AddImports(Imports);

var globals = new ScriptGlobals
{
Client = client,
};

while (true)
{
Console.Write("> ");
string input = Console.ReadLine();

if (input == "quit")
{
break;
}

object eval;
try
{
eval = await CSharpScript.EvaluateAsync(input, options, globals);
}
catch (Exception e)
{
eval = e;
}
Console.WriteLine(Inspector.Inspect(eval));
}

// client.Stop
client.Dispose();
}

static IEnumerable<Assembly> GetAssemblies()
{
var Assemblies = Assembly.GetEntryAssembly().GetReferencedAssemblies();
foreach (var a in Assemblies)
{
var asm = Assembly.Load(a);
yield return asm;
}
yield return Assembly.GetEntryAssembly();
}

public class ScriptGlobals
{
public IDiscordClient Client { get; set; }
}
}
}

+ 16
- 0
sample/idn/idn.csproj View File

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

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="3.5.0-beta1-final" />
</ItemGroup>

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

</Project>

+ 21
- 0
src/Discord.Net/Discord.Net.csproj View File

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

<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<LangVersion>8.0</LangVersion>
<Nullable>enable</Nullable>
<RootNamespace>Discord</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="refit" Version="5.0.23" />
<PackageReference Include="System.Text.Json" Version="4.7.0" />
</ItemGroup>

<ItemGroup>
<Folder Include="Entities\" />
<Folder Include="..\..\..\..\..\%2540discord\%2540next\Discord.Net\src\Discord.Net\Rest\Requests\" />
<Folder Include="Rest\Requests\" />
</ItemGroup>

</Project>

+ 37
- 0
src/Discord.Net/DiscordClient.cs View File

@@ -0,0 +1,37 @@
using System;
using Discord.Rest;
using Discord.Socket;

namespace Discord
{
internal class DiscordClient : IDiscordClient
{
public DiscordRestApi Rest { get; }
public DiscordGatewayApi Gateway { get; }

private readonly DiscordConfig _config;
private readonly Logger _logger;

public DiscordClient(DiscordConfig config, DiscordRestApi restApi, DiscordGatewayApi gatewayApi)
{
_config = config;
_logger = new Logger("Client", config.MinClientSeverity);

Rest = restApi;
Gateway = gatewayApi;

Log += _ => { }; // initialize log method
Rest.Logger.Message += m => Log(m);
Gateway.Logger.Message += m => Log(m);
_logger.Message += m => Log(m);
}

public event Action<LogMessage> Log;

public void Dispose()
{
Rest.Dispose();
Gateway.Dispose();
}
}
}

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

@@ -0,0 +1,51 @@
using Discord.Socket;
using Discord.Socket.Providers;
using System;

namespace Discord
{
public class DiscordConfig
{
/// <summary>
/// Discord.Net version
/// </summary>
public const string Version = "3.0.0a0";
/// <summary>
/// Discord.Net User-Agent
/// </summary>
public const string UserAgent = "DiscordBot (https://github.com/discord-net/Discord.Net, " + Version + ")";

/// <summary>
/// The default, fallback Gateway URI. This will generally be replaced by <see cref="Rest.IDiscordRestApi.GetGatewayAsync"/>.
/// </summary>
public static readonly Uri DefaultGatewayUri = new Uri("wss://gateway.discord.gg");
/// <summary>
/// The default REST URI.
/// </summary>
public static readonly Uri DefaultRestUri = new Uri("https://discordapp.com/api/v6/");
/// <summary>
/// The URI to use when making HTTP requests. If specified, this will override the default.
/// </summary>
public Uri? RestUri = null;
/// <summary>
/// The URI to use when connecting to the gateway. If specified, this will override the URI Discord instructs us to use.
/// </summary>
public Uri? GatewayUri = null;
/// <summary>
/// SocketFactory gets or sets how a WebSocket will be created.
/// </summary>
public SocketFactory SocketFactory { get; set; } = DefaultSocketFactory.Create;
/// <summary>
/// Minimum Log Severity for the Rest API.
/// </summary>
public LogSeverity MinRestSeverity { get; set; } = LogSeverity.Info;
/// <summary>
/// Minimum Log Severity for the Gateway API.
/// </summary>
public LogSeverity MinGatewaySeverity { get; set; } = LogSeverity.Info;
/// <summary>
/// Minimum Log Severity for the Client.
/// </summary>
public LogSeverity MinClientSeverity { get; set; } = LogSeverity.Info;
}
}

+ 24
- 0
src/Discord.Net/Entities/Snowflake.cs View File

@@ -0,0 +1,24 @@
namespace Discord
{
/// <summary>
/// A Snowflake represents a unique, 64-bit identifier.
/// </summary>
public struct Snowflake
{
private readonly ulong _value;

private Snowflake(ulong value)
{
_value = value;
}

public static implicit operator ulong(Snowflake snowflake)
{
return snowflake._value;
}
public static implicit operator Snowflake(ulong value)
{
return new Snowflake(value);
}
}
}

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

@@ -0,0 +1,28 @@
using System;
using System.Net.Http.Headers;
using Discord.Rest;
using Discord.Socket;

namespace Discord
{
public interface IDiscordClient : IDisposable
{
public static IDiscordClient Create(string token, DiscordConfig? config = default)
{
config = config ?? new DiscordConfig();

// todo: validate token
var tokenHeader = AuthenticationHeaderValue.Parse(token);

var rest = new DiscordRestApi(config, tokenHeader);
var gateway = new DiscordGatewayApi(config, token);

return new DiscordClient(config, rest, gateway);
}

DiscordRestApi Rest { get; }
DiscordGatewayApi Gateway { get; }

event Action<LogMessage> Log;
}
}

+ 23
- 0
src/Discord.Net/Rest/DiscordHttpClientHandler.cs View File

@@ -0,0 +1,23 @@
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;

namespace Discord.Rest
{
internal sealed class DiscordHttpClientHandler : HttpClientHandler
{
private readonly AuthenticationHeaderValue _token;

public DiscordHttpClientHandler(AuthenticationHeaderValue token)
{
_token = token;
}

protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Headers.Authorization = _token;
return base.SendAsync(request, cancellationToken);
}
}
}

+ 47
- 0
src/Discord.Net/Rest/DiscordRestApi.cs View File

@@ -0,0 +1,47 @@
using System.Text.Json;
using System.Threading.Tasks;
using System.Net.Http.Headers;
using System.Net.Http;
using Refit;
using Discord.Models;
using Discord.Serialization;

// This is essentially a reimplementation of Wumpus.Net.Rest
namespace Discord.Rest
{
public class DiscordRestApi : IDiscordRestApi
{
private readonly IDiscordRestApi _api;
private readonly HttpClient _http;

internal Logger Logger { get; private set; }

public DiscordRestApi(DiscordConfig config, AuthenticationHeaderValue token)
{
Logger = new Logger("Rest", config.MinRestSeverity);

_http = new HttpClient(new DiscordHttpClientHandler(token), true)
{
BaseAddress = config.RestUri ?? DiscordConfig.DefaultRestUri,
};

var jsonOptions = new JsonSerializerOptions();
jsonOptions.Converters.Add(new OptionalConverter());
var refitSettings = new RefitSettings
{
ContentSerializer = new JsonContentSerializer(jsonOptions),
};
_api = RestService.For<IDiscordRestApi>(_http, refitSettings);
}
public Task<GatewayInfo> GetGatewayInfoAsync()
=> _api.GetGatewayInfoAsync();
public Task<GatewayInfo> GetBotGatewayInfoAsync()
=> _api.GetBotGatewayInfoAsync();

public void Dispose()
{
_http.Dispose();
}
}
}

+ 34
- 0
src/Discord.Net/Rest/IDiscordRestApi.cs View File

@@ -0,0 +1,34 @@
using System;
using System.Threading.Tasks;
using Refit;
using Discord.Models;

namespace Discord.Rest
{
public interface IDiscordRestApi
{
// --- /applications

// --- /channels

// --- /gateway
[Get("/gateway/bot")]
Task<GatewayInfo> GetGatewayInfoAsync();
[Get("/gateway/bot")]
Task<GatewayInfo> GetBotGatewayInfoAsync();

// --- /guilds

// --- /invites

// --- /oauth2

// --- /store

// --- /users

// --- /voice

// --- /webhooks
}
}

+ 59
- 0
src/Discord.Net/Rest/Models/Channel/Channel.cs View File

@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json.Serialization;

namespace Discord.Models
{
public class Channel
{
public const int MinChannelNameLength = 2;
public const int MaxChannelNameLength = 100;

public const int MinChannelTopicLength = 0;
public const int MaxChannelTopicLength = 1024;

public const int MinUserLimit = 0;
public const int MaxUserLimit = 100;

public const int MinBitrate = 8000;
public const int MaxBitrate = 384000;

public const int MinRateLimitPerUser = 0;
public const int MaxRateLimitPerUser = 21600;

[JsonPropertyName("id")]
public Snowflake Id { get; set; }
[JsonPropertyName("type")]
public ChannelType Type { get; set; }
[JsonPropertyName("guild_id")]
public Optional<Snowflake> GuildId { get; set; }
[JsonPropertyName("position")]
public Optional<short> Position { get; set; }
[JsonPropertyName("permission_overwrites")]
public Optional<Overwrite[]> Overwrites { get; set; }
[JsonPropertyName("name")]
public Optional<string> Name { get; set; }
[JsonPropertyName("topic")]
public Optional<string?> Topic { get; set; }
[JsonPropertyName("nsfw")]
public Optional<bool> Nsfw { get; set; }
[JsonPropertyName("user_limit")]
public Optional<short> Bitrate { get; set; }
[JsonPropertyName("rate_limit_per_user")]
public Optional<int> RateLimitPerUser { get; set; }
[JsonPropertyName("recipients")]
public Optional<User[]> Recipients { get; set; }
[JsonPropertyName("icon")]
public Optional<string?> IconId { get; set; }
[JsonPropertyName("owner_id")]
public Optional<Snowflake> OwnerId { get; set; }
[JsonPropertyName("application_id")]
public Optional<Snowflake> ApplicationId { get; set; }
[JsonPropertyName("parent_id")]
public Optional<Snowflake> ParentId { get; set; }
[JsonPropertyName("last_pin_timestamp")]
public Optional<DateTimeOffset> LastPinTimestamp { get; set; }
// omitted: last_message_id
}
}

+ 13
- 0
src/Discord.Net/Rest/Models/Channel/ChannelType.cs View File

@@ -0,0 +1,13 @@
namespace Discord.Models
{
public enum ChannelType : byte
{
Text = 0,
Direct = 1,
Voice = 2,
Group = 3,
Category = 4,
News = 5,
Store = 6
}
}

+ 25
- 0
src/Discord.Net/Rest/Models/GatewayInfo.cs View File

@@ -0,0 +1,25 @@
#pragma warning disable CS8618 // Uninitialized NRT expected in models
using System.Text.Json.Serialization;

namespace Discord.Models
{
public class GatewayInfo
{
[JsonPropertyName("url")]
public string Url { get; set; }
[JsonPropertyName("shards")]
public int? Shards { get; set; }
[JsonPropertyName("session_start_limit")]
public GatewaySessionStartInfo? SessionStartInfo { get; set; }
}

public class GatewaySessionStartInfo
{
[JsonPropertyName("total")]
public int Total { get; set; }
[JsonPropertyName("remaining")]
public int Remaining { get; set; }
[JsonPropertyName("reset_after")]
public int ResetAfter { get; set; }
}
}

+ 36
- 0
src/Discord.Net/Rest/Models/Permissions/ChannelPermissions.cs View File

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

namespace Discord.Models
{
[Flags]
public enum ChannelPermissions : ulong
{
// General
CreateInstantInvite = 0x0000_0001,
ManageChannel = 0x0000_0010,
AddReactions = 0x0000_0040,
ViewChannel = 0x0000_0400,
ManagePermissions = 0x1000_0000,
ManageWebhooks = 0x2000_0000,

// Messages
SendMessages = 0x0000_0800,
SendTtsMessages = 0x0000_0100,
ManageMessages = 0x0000_02000,
EmbedLinks = 0x0000_4000,
AttachFiles = 0x0000_8000,
ReadMessageHistory = 0x0001_0000,
MentionEveryone = 0x0002_0000,
UseExternalEmoji = 0x0004_0000,

// Voice
Connect = 0x0010_0000,
Speak = 0x0020_0000,
MuteMembers = 0x0040_0000,
DeafenMembers = 0x0080_0000,
MoveMembers = 0x0100_0000,
UseVoiceActivity = 0x0200_0000,
PrioritySpeaker = 0x0000_0100,
Stream = 0x0000_0200,
}
}

+ 45
- 0
src/Discord.Net/Rest/Models/Permissions/GuildPermissions.cs View File

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

namespace Discord.Models
{
// todo: doc these when other models exist
[Flags]
public enum GuildPermissions : ulong
{
// General
CreateInstantInvite = 0x0000_0001,
KickMembers = 0x0000_0002,
BanMembers = 0x0000_0004,
Administrator = 0x0000_0008,
ManageChannels = 0x0000_0010,
ManageGuild = 0x0000_0020,
AddReactions = 0x0000_0040,
ViewAuditLog = 0x0000_0080,
ViewChannel = 0x0000_0400,
ChangeNickname = 0x0400_0000,
ManageNicknames = 0x0800_0000,
ManageRoles = 0x1000_0000,
ManageWebhooks = 0x2000_0000,
ManageEmoji = 0x4000_0000,

// Messages
SendMessages = 0x0000_0800,
SendTtsMessages = 0x0000_0100,
ManageMessages = 0x0000_02000,
EmbedLinks = 0x0000_4000,
AttachFiles = 0x0000_8000,
ReadMessageHistory = 0x0001_0000,
MentionEveryone = 0x0002_0000,
UseExternalEmoji = 0x0004_0000,

// Voice
Connect = 0x0010_0000,
Speak = 0x0020_0000,
MuteMembers = 0x0040_0000,
DeafenMembers = 0x0080_0000,
MoveMembers = 0x0100_0000,
UseVoiceActivity = 0x0200_0000,
PrioritySpeaker = 0x0000_0100,
Stream = 0x0000_0200,
}
}

+ 16
- 0
src/Discord.Net/Rest/Models/Permissions/Overwrite.cs View File

@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json.Serialization;

namespace Discord.Models
{
public class Overwrite
{
[JsonPropertyName("id")]
public Snowflake Id { get; set; }
[JsonPropertyName("type")]
[JsonConverter(typeof(JsonStringEnumConverter))]
public PermissionTarget TargetType { get; set; }
}
}

+ 8
- 0
src/Discord.Net/Rest/Models/Permissions/PermissionTarget.cs View File

@@ -0,0 +1,8 @@
namespace Discord.Models
{
public enum PermissionTarget
{
Member,
Role
}
}

+ 20
- 0
src/Discord.Net/Rest/Models/Users/AccountFlags.cs View File

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

namespace Discord.Models
{
[Flags]
public enum AccountFlags : short
{
None = 0,
Employee = 1<<0,
Partner = 1<<1,
HypesquadEvents = 1<<2,
BugHunter = 1<<3,
HypesquadBravery = 1<<6,
HypesquadBrilliance = 1<<7,
HypesquadBalance = 1<<8,
EarlySupporter = 1<<9,
TeamUser = 1<<10,
System = 1<<12,
}
}

+ 8
- 0
src/Discord.Net/Rest/Models/Users/PremiumType.cs View File

@@ -0,0 +1,8 @@
namespace Discord.Models
{
public enum PremiumType : byte
{
Classic = 1,
Nitro = 2
}
}

+ 33
- 0
src/Discord.Net/Rest/Models/Users/User.cs View File

@@ -0,0 +1,33 @@
#pragma warning disable CS8618 // Uninitialized NRT expected in models <username>
using System.Text.Json.Serialization;

namespace Discord.Models
{
public class User
{
[JsonPropertyName("id")]
public Snowflake Id { get; set; }
[JsonPropertyName("username")]
public string Username { get; set; }
[JsonPropertyName("discriminator")]
public ushort Discriminator { get; set; }
[JsonPropertyName("avatar")]
public string? AvatarId { get; set; }
[JsonPropertyName("bot")]
public Optional<bool> Bot { get; set; }
[JsonPropertyName("system")]
public Optional<bool> System { get; set; }
[JsonPropertyName("mfa_enabled")]
public Optional<bool> MfaEnabled { get; set; }
[JsonPropertyName("locale")]
public Optional<string> Locale { get; set; }
[JsonPropertyName("verified")]
public Optional<bool> Verified { get; set; }
[JsonPropertyName("email")]
public Optional<string> Email { get; set; }
[JsonPropertyName("flags")]
public Optional<AccountFlags> Flags { get; set; }
[JsonPropertyName("premium_type")]
public Optional<PremiumType> PremiumType { get; set; }
}
}

+ 33
- 0
src/Discord.Net/Rest/Models/Webhook/Webhook.cs View File

@@ -0,0 +1,33 @@
using System.Text.Json.Serialization;

namespace Discord.Models
{
public class Webhook
{
public const int MinWebhookNameLength = 2;
public const int MaxWebhookNameLength = 32;

public const int MinMessageContentLength = 0;
public const int MaxMessageContentLength = 2000;

public const int MinEmbedLimit = 0;
public const int MaxEmbedLimit = 10;

[JsonPropertyName("id")]
public Snowflake Id { get; set; }
[JsonPropertyName("type")]
public WebhookType Type { get; set; }
[JsonPropertyName("guild_id")]
public Optional<Snowflake> GuildId { get; set; }
[JsonPropertyName("channel_id")]
public Snowflake ChannelId { get; set; }
[JsonPropertyName("user")]
public Optional<User> Creator { get; set; }
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("avatar")]
public string? AvatarId { get; set; }
[JsonPropertyName("token")]
public Optional<string> Token { get; set; }
}
}

+ 8
- 0
src/Discord.Net/Rest/Models/Webhook/WebhookType.cs View File

@@ -0,0 +1,8 @@
namespace Discord.Models
{
public enum WebhookType : byte
{
Incoming = 1,
ChannelFollower = 2
}
}

+ 49
- 0
src/Discord.Net/Serialization/JsonContentSerializer.cs View File

@@ -0,0 +1,49 @@
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Refit;

// https://blog.martincostello.com/refit-and-system-text-json/

namespace Discord
{
public class JsonContentSerializer : IContentSerializer
{
private static readonly MediaTypeHeaderValue _jsonMediaType = new MediaTypeHeaderValue("application/json") { CharSet = Encoding.UTF8.WebName };
private readonly JsonSerializerOptions _serializerOptions;

public JsonContentSerializer(JsonSerializerOptions serializerOptions)
{
_serializerOptions = serializerOptions;
}

public async Task<T> DeserializeAsync<T>(HttpContent content)
{
using var json = await content.ReadAsStreamAsync().ConfigureAwait(false);
return await JsonSerializer.DeserializeAsync<T>(json, _serializerOptions).ConfigureAwait(false);
}

public async Task<HttpContent> SerializeAsync<T>(T data)
{
var stream = new MemoryStream();
try
{
await JsonSerializer.SerializeAsync<T>(stream, data, _serializerOptions).ConfigureAwait(false);
await stream.FlushAsync();

var content = new StreamContent(stream);
content.Headers.ContentType = _jsonMediaType;

return content;
}
catch
{
await stream.DisposeAsync().ConfigureAwait(false);
throw;
}
}
}
}

+ 41
- 0
src/Discord.Net/Serialization/OptionalConverter.cs View File

@@ -0,0 +1,41 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Discord.Serialization
{
// TODO: This does not allow us to omit properties at runtime
// Need to evaluate which cases need us to omit properties and write a separate converter
// for those. At this time I can only think of the outgoing REST PATCH requests. Incoming
// omitted properties will be correctly treated as Optional.Unspecified (the default)
public class OptionalConverter : JsonConverterFactory
{
private class OptionalTypeConverter<T> : JsonConverter<Optional<T>>
{
public override Optional<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
return Optional<T>.Unspecified;
else
return new Optional<T>(JsonSerializer.Deserialize<T>(ref reader, options));
}

public override void Write(Utf8JsonWriter writer, Optional<T> value, JsonSerializerOptions options)
{
if (!value.IsSpecified)
writer.WriteNullValue();
else
JsonSerializer.Serialize(writer, value.Value, options);
}
}

public override bool CanConvert(Type typeToConvert)
=> typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == typeof(Optional<>);
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var innerType = typeToConvert.GetGenericArguments()[0];
var converterType = typeof(OptionalTypeConverter<>).MakeGenericType(innerType);
return (JsonConverter)Activator.CreateInstance(converterType);
}
}
}

+ 45
- 0
src/Discord.Net/Socket/DiscordGatewayApi.cs View File

@@ -0,0 +1,45 @@
using System;
using System.Threading;
using System.Threading.Tasks;

namespace Discord.Socket
{
public class DiscordGatewayApi : IDisposable
{
private readonly DiscordConfig _config;
private readonly string _token;

internal Logger Logger { get; private set; }
public ISocket Socket { get; set; }

public DiscordGatewayApi(DiscordConfig config, string token)
{
Logger = new Logger("Gateway", config.MinGatewaySeverity);

_config = config;
_token = token;

Socket = config.SocketFactory(OnAborted, OnPacket);
}

public async Task ConnectAsync(Uri? gatewayUri)
{
var baseUri = _config.GatewayUri ?? (gatewayUri ?? DiscordConfig.DefaultGatewayUri);
await Socket.ConnectAsync(baseUri, CancellationToken.None).ConfigureAwait(false);
}

public void OnAborted(Exception error)
{
// todo: log
}
public async Task OnPacket(object packet)
{
await Task.CompletedTask;
}

public void Dispose()
{
Socket.Dispose();
}
}
}

+ 35
- 0
src/Discord.Net/Socket/ISocket.cs View File

@@ -0,0 +1,35 @@
using System;
using System.Threading;
using System.Threading.Tasks;

namespace Discord.Socket
{
public delegate ISocket SocketFactory(OnAbortionHandler abortionHandler, OnPacketHandler packetHandler);

// A socket should only have one parent, so these do not need to be decoupled events.
public delegate Task OnPacketHandler(object packet);
public delegate void OnAbortionHandler(Exception error);

public enum SocketState
{
Closed = default,
AcquiringOpenLock,
Opening,
Open,
AcquiringClosingLock,
Closing,
Aborted
}

public interface ISocket : IDisposable
{
SocketState State { get; }

Task ConnectAsync(Uri uri, CancellationToken token);
Task CloseAsync(int? code = null, string? reason = null);
Task SendAsync(ReadOnlyMemory<byte> payload);

OnAbortionHandler OnAbortion { get; }
OnPacketHandler OnPacket { get; }
}
}

+ 196
- 0
src/Discord.Net/Socket/Providers/DefaultSocket.cs View File

@@ -0,0 +1,196 @@
using System;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;

namespace Discord.Socket.Providers
{
public static class DefaultSocketFactory
{
public static ISocket Create(OnAbortionHandler onAbortion, OnPacketHandler onPacket)
{
return new DefaultSocket(onAbortion, onPacket);
}
}

internal class DefaultSocket : ISocket
{
public SocketState State { get; private set; }
public OnAbortionHandler OnAbortion { get; }
public OnPacketHandler OnPacket { get; }

private ClientWebSocket _socket;
private Task? _receiveTask;

private CancellationTokenSource _cancelTokenSource;
private SemaphoreSlim _sendLock;
private SemaphoreSlim _stateLock;

public DefaultSocket(OnAbortionHandler onAbortion, OnPacketHandler onPacket)
{
_socket = new ClientWebSocket();

_cancelTokenSource = new CancellationTokenSource();
_sendLock = new SemaphoreSlim(1);
_stateLock = new SemaphoreSlim(1);

OnAbortion = onAbortion;
OnPacket = onPacket;
}

public async Task ConnectAsync(Uri uri, CancellationToken connectCancelToken)
{
if (State == SocketState.Open
|| State == SocketState.Opening
|| State == SocketState.AcquiringOpenLock
|| State == SocketState.Aborted)
{
// todo: evaluate how to handle a (redundant?) state operation
return;
}

CancellationTokenSource openLock; // create a linked token in case the caller wants to cancel an opening connection
try
{
openLock = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, connectCancelToken);
}
catch (ObjectDisposedException e)
{
// Failed to link openLock, an expired cancellation token was passed
State = SocketState.Aborted;
OnAbortion(e);
return;
}

State = SocketState.AcquiringOpenLock;
try
{
await _stateLock.WaitAsync(openLock.Token).ConfigureAwait(false);
}
catch (Exception e)
{
// Failed to acquire openLock
State = SocketState.Aborted;
OnAbortion(e);
}
State = SocketState.Opening;

try
{
await _socket.ConnectAsync(uri, _cancelTokenSource.Token).ConfigureAwait(false);
}
catch (Exception e)
{
// Failed to open socket connection
State = SocketState.Aborted;
OnAbortion(e);
return;
}
State = SocketState.Open;

_receiveTask = ReceiveAsync();

// TODO: this should not be expected to fail
_stateLock.Release();
openLock.Dispose();
}
public async Task CloseAsync(int? code, string? reason)
{
if (State == SocketState.Closed
|| State == SocketState.Closing
|| State == SocketState.AcquiringClosingLock
|| State == SocketState.Aborted)
{
// todo: evaluate how to handle a (redundant?) state operation; see OpenAsync
return;
}

State = SocketState.AcquiringClosingLock;
try
{
await _stateLock.WaitAsync();
}
catch (Exception e)
{
State = SocketState.Aborted;
OnAbortion(e);
return;
}
State = SocketState.Closing;
// I think it is acceptable to use CancellationToken.None here, as no parallel operation should need to cancel the socket closure
await _socket.CloseAsync((WebSocketCloseStatus)(code ?? 1005),
reason ?? string.Empty,
CancellationToken.None
).ConfigureAwait(false);

// Wait until after .NET has been told to close the socket to cancel any pending sends/receives
//
// Presumably, sends/receives should have failed gracefully by this point, instead of aborting the underlying socket
try
{
_cancelTokenSource.Cancel();
await (_receiveTask ?? Task.CompletedTask);
}
catch
{
// just log for now
}

State = SocketState.Closed;
}
public async Task ReceiveAsync()
{
while (State == SocketState.Open && !_cancelTokenSource.IsCancellationRequested)
{
try
{
Memory<byte> buffer = new Memory<byte>();
var res = await _socket.ReceiveAsync(buffer, _cancelTokenSource.Token).ConfigureAwait(false);
// todo: handle memory renting and ongoing messages
// todo: parse and OnPacket
}
catch (Exception err)
{
// log error
if (_socket.State != WebSocketState.Open) // detrimental error
{
State = SocketState.Aborted;
OnAbortion(err);
return;
}
}
}
}
public async Task SendAsync(ReadOnlyMemory<byte> data)
{
if (State != SocketState.Open)
{
// raise error?
return;
}

await _sendLock.WaitAsync().ConfigureAwait(false);
try
{
// TODO: compression? who needs it
await _socket.SendAsync(data, WebSocketMessageType.Text, true, _cancelTokenSource.Token).ConfigureAwait(false);
}
finally
{
_sendLock.Release();
}
}

public void Dispose()
{
if (State != SocketState.Closed)
{
// log error? can this still proceed...
}
_socket.Dispose();
_cancelTokenSource.Dispose();
_stateLock.Dispose();
}
}
}

+ 159
- 0
src/Discord.Net/Utilities/Logging.cs View File

@@ -0,0 +1,159 @@
using System;
using System.Text;

namespace Discord
{
public enum LogSeverity
{
Trace,
Debug,
Info,
Warn,
Error
}

public struct LogMessage
{
public LogSeverity Level { get; }
public string Source { get; }
public string Message { get; }
public Exception? Exception { get; }

public LogMessage(LogSeverity level, string source, string message, Exception? exception = null)
{
Level = level;
Source = source;
Message = message;
Exception = exception;
}

public override string ToString() => ToString();
public string ToString(StringBuilder? builder = null,
bool fullException = true,
bool prependTimestamp = true,
DateTimeKind timestampKind = DateTimeKind.Local,
int? padSource = 11)
{
string? exMessage = fullException ? Exception?.ToString() : Exception?.Message;
int maxLength = 1 +
(prependTimestamp ? 8 : 0) + 1 +
(padSource.HasValue ? padSource.Value : Source?.Length ?? 0) + 1 +
(Message?.Length ?? 0) +
(exMessage?.Length ?? 0) + 3;

if (builder == null)
builder = new StringBuilder(maxLength);
else
{
builder.Clear();
builder.EnsureCapacity(maxLength);
}

if (prependTimestamp)
{
DateTime now;
if (timestampKind == DateTimeKind.Utc)
now = DateTime.UtcNow;
else
now = DateTime.Now;
if (now.Hour < 10)
builder.Append('0');
builder.Append(now.Hour);
builder.Append(':');
if (now.Minute < 10)
builder.Append('0');
builder.Append(now.Minute);
builder.Append(':');
if (now.Second < 10)
builder.Append('0');
builder.Append(now.Second);
builder.Append(' ');
}
if (Source != null)
{
if (padSource.HasValue)
{
if (Source.Length < padSource.Value)
{
builder.Append(Source);
builder.Append(' ', padSource.Value - Source.Length);
}
else if (Source.Length > padSource.Value)
builder.Append(Source.Substring(0, padSource.Value));
else
builder.Append(Source);
}
else
builder.Append(Source);
builder.Append(' ');
}
if (!string.IsNullOrEmpty(Message))
{
char c;
for (int i = 0; i < Message.Length; i++)
{
c = Message[i];
if (!char.IsControl(c))
builder.Append(c);
}
}
if (exMessage != null)
{
if (!string.IsNullOrEmpty(Message))
{
builder.Append(':');
builder.AppendLine();
}
builder.Append(exMessage);
}

return builder.ToString();
}
}

public class Logger
{
public event Action<LogMessage>? Message;
public string Name { get; set; }
public LogSeverity MinSeverity { get; set; }

public Logger(string source, LogSeverity minSeverity)
{
Name = source;
MinSeverity = minSeverity;
}

public void Log(LogMessage message)
{
if (message.Level < MinSeverity)
return;
Message?.Invoke(message);
}

public void Log(LogSeverity severity, string message, Exception? err = null)
=> Log(new LogMessage(severity, Name, message, err));

public void Trace(string message, Exception? err = null)
=> Log(LogSeverity.Trace, message, err);
public void Debug(string message, Exception? err = null)
=> Log(LogSeverity.Debug, message, err);
public void Info(string message, Exception? err = null)
=> Log(LogSeverity.Info, message, err);
public void Warn(string message, Exception? err = null)
=> Log(LogSeverity.Warn, message, err);
public void Error(string message, Exception? err = null)
=> Log(LogSeverity.Error, message, err);

public void Trace(Exception err)
=> Log(LogSeverity.Trace, null!, err);
public void Debug(Exception err)
=> Log(LogSeverity.Debug, null!, err);
public void Info(Exception err)
=> Log(LogSeverity.Info, null!, err);
public void Warn(Exception err)
=> Log(LogSeverity.Warn, null!, err);
public void Error(Exception err)
=> Log(LogSeverity.Error, null!, err);

}
}

+ 62
- 0
src/Discord.Net/Utilities/Optional.cs View File

@@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Text;
// todo: impl
namespace Discord
{
public struct Optional<T>
{
public static Optional<T> Unspecified => default;

public bool IsSpecified { get; }
private readonly T _innerValue;

public T Value
{
get
{
if (!IsSpecified)
throw new UnspecifiedOptionalException();
return _innerValue;
}
}

public Optional(T value)
{
IsSpecified = true;
_innerValue = value;
}

public override string ToString()
{
return $"<Optional IsSpecified={IsSpecified}, Value={(IsSpecified ? Value?.ToString() ?? "null" : "(unspecified)")}>";
}

public override bool Equals(object obj)
{
if (obj is Optional<T> opt)
{
if (IsSpecified && opt.IsSpecified)
return Value?.Equals(opt.Value) ?? opt.Value == null;
return IsSpecified == opt.IsSpecified;
}
return base.Equals(obj);
}

public override int GetHashCode()
=> IsSpecified ? Value?.GetHashCode() ?? 0 : 0;

public static bool operator ==(Optional<T> a, Optional<T> b)
=> a.Equals(b);
public static bool operator !=(Optional<T> a, Optional<T> b)
=> !a.Equals(b);

// todo: implement comparing, GetValueOrDefault, hash codes etc
}


public class UnspecifiedOptionalException : Exception
{
public UnspecifiedOptionalException() : base("An attempt was made to access an unspecified optional value") { }
}
}

+ 0
- 0
test/.gitkeep View File


+ 20
- 0
test/Discord.Tests.Unit/Discord.Tests.Unit.csproj View File

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

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>

<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
<PackageReference Include="coverlet.collector" Version="1.0.1" />
</ItemGroup>

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

</Project>

+ 118
- 0
test/Discord.Tests.Unit/Serialization/OptionalConverterTests.cs View File

@@ -0,0 +1,118 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Xunit;
using Discord.Serialization;

namespace Discord.Tests.Unit.Serialization
{
public class OptionalConverterTests
{
private readonly JsonSerializerOptions _jsonOptions;

public OptionalConverterTests()
{
_jsonOptions = new JsonSerializerOptions();
_jsonOptions.Converters.Add(new OptionalConverter());
}

public class SampleOptionalClass
{
[JsonPropertyName("optional_number")]
public Optional<int> OptionalNumber { get; set; }
[JsonPropertyName("required_number")]
public int RequiredNumber { get; set; }

public override bool Equals(object obj)
=> (obj is SampleOptionalClass other) && (other.OptionalNumber == OptionalNumber && other.RequiredNumber == RequiredNumber);
public override int GetHashCode()
=> OptionalNumber.GetHashCode() ^ RequiredNumber.GetHashCode();
}

private string expectedOptionalUnset = "{\"optional_number\":null,\"required_number\":10}";
private SampleOptionalClass withOptionalUnset = new SampleOptionalClass
{
OptionalNumber = Optional<int>.Unspecified,
RequiredNumber = 10,
};
private string expectedOptionalSet = "{\"optional_number\":11,\"required_number\":10}";
private SampleOptionalClass withOptionalSet = new SampleOptionalClass
{
OptionalNumber = new Optional<int>(11),
RequiredNumber = 10,
};

[Fact]
public void OptionalConverter_Can_Write()
{
// todo: is STJ deterministic in writing order? want to make sure this test doesn't fail because of cosmic rays
var unsetString = JsonSerializer.Serialize(withOptionalUnset, _jsonOptions);
Assert.Equal(expectedOptionalUnset, unsetString);
var setString = JsonSerializer.Serialize(withOptionalSet, _jsonOptions);
Assert.Equal(expectedOptionalSet, setString);
}

[Fact]
public void OptionalConverter_Can_Read()
{
var unset = JsonSerializer.Deserialize<SampleOptionalClass>(expectedOptionalUnset, _jsonOptions);
Assert.Equal(withOptionalUnset, unset);

var set = JsonSerializer.Deserialize<SampleOptionalClass>(expectedOptionalSet, _jsonOptions);
Assert.Equal(withOptionalSet, set);
}

public class NestedPoco
{
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("age")]
public int Age { get; set; }

public override bool Equals(object obj)
=> (obj is NestedPoco other) && (Name == other.Name && Age == other.Age);
public override int GetHashCode()
=> Name.GetHashCode() ^ Age.GetHashCode();
}
public class NestedSampleClass
{
[JsonPropertyName("nested")]
public Optional<NestedPoco> Nested { get; set; }
}

private string expectedNestedWithUnset = "{\"nested\":null}";
private NestedSampleClass nestedWithUnset = new NestedSampleClass
{
Nested = Optional<NestedPoco>.Unspecified
};
private string expectedNestedWithSet = "{\"nested\":{\"name\":\"Ashley\",\"age\":23}}";
private NestedSampleClass nestedWithSet = new NestedSampleClass
{
Nested = new Optional<NestedPoco>(new NestedPoco
{
Name = "Ashley",
Age = 23
}),
};

[Fact]
public void OptionalConverter_Can_Write_Nested_Poco()
{
var unset = JsonSerializer.Serialize(nestedWithUnset, _jsonOptions);
Assert.Equal(expectedNestedWithUnset, unset);

var set = JsonSerializer.Serialize(nestedWithSet, _jsonOptions);
Assert.Equal(expectedNestedWithSet, set);
}
[Fact]
public void OptionalConverter_Can_Read_Nested_Poco()
{
var unset = JsonSerializer.Deserialize<NestedSampleClass>(expectedNestedWithUnset, _jsonOptions);
Assert.Equal(nestedWithUnset.Nested, unset.Nested);

var set = JsonSerializer.Deserialize<NestedSampleClass>(expectedNestedWithSet, _jsonOptions);
Assert.Equal(nestedWithSet.Nested, set.Nested);
}
}
}

+ 13
- 0
test/Discord.Tests.Unit/UnitTest1.cs View File

@@ -0,0 +1,13 @@
using Xunit;

namespace Discord.Tests.Unit
{
public class UnitTest1
{
[Fact]
public void Test1()
{
Assert.True(true);
}
}
}

Loading…
Cancel
Save