Project restructure, Added .Net Core support, Fixed some bugs

This commit is contained in:
Brandon Smith
2015-08-17 18:57:44 -03:00
parent f9f31c3c04
commit 60f74d088a
45 changed files with 830 additions and 164 deletions

View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{1B5603B4-6F8F-4289-B945-7BAAE523D740}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Discord.Net</RootNamespace>
<AssemblyName>Discord.Net.Commands</AssemblyName>
<FileAlignment>512</FileAlignment>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup />
<ItemGroup>
<Reference Include="System" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Discord.Net.Net45\Discord.Net.csproj">
<Project>{8d71a857-879a-4a10-859e-5ff824ed6688}</Project>
<Name>Discord.Net</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Compile Include="..\Discord.Net.Commands\CommandBuilder.cs">
<Link>CommandBuilder.cs</Link>
</Compile>
<Compile Include="..\Discord.Net.Commands\CommandParser.cs">
<Link>CommandParser.cs</Link>
</Compile>
<Compile Include="..\Discord.Net.Commands\DiscordBotClient.cs">
<Link>DiscordBotClient.cs</Link>
</Compile>
<Compile Include="..\Discord.Net.Commands\DiscordBotClient.Events.cs">
<Link>DiscordBotClient.Events.cs</Link>
</Compile>
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

View File

@@ -0,0 +1,17 @@
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("Discord.Net")]
[assembly: AssemblyDescription("An unofficial .Net API wrapper for the Discord client.")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("RogueException")]
[assembly: AssemblyProduct("Discord.Net")]
[assembly: AssemblyCopyright("Copyright © 2015")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
[assembly: Guid("76ea00e6-ea24-41e1-acb2-639c0313fa80")]
[assembly: AssemblyVersion("0.2.0.0")]
[assembly: AssemblyFileVersion("0.2.0.0")]

View File

@@ -0,0 +1,32 @@
using System;
using System.Threading.Tasks;
namespace Discord
{
public sealed class CommandBuilder
{
private readonly DiscordBotClient _client;
private readonly string _prefix;
private readonly bool _useWhitelist;
public CommandBuilder(DiscordBotClient client, string prefix, bool useWhitelist = false)
{
_client = client;
_prefix = prefix;
_useWhitelist = useWhitelist;
}
public void AddCommandGroup(string cmd, Action<CommandBuilder> config, bool useWhitelist = false)
{
config(new CommandBuilder(_client, _prefix + ' ' + cmd, useWhitelist));
}
public void AddCommand(string cmd, int minArgs, int maxArgs, Action<DiscordBotClient.CommandEventArgs> handler, bool? useWhitelist = null)
{
AddCommand(cmd, minArgs, maxArgs, e => { handler(e); return null; }, useWhitelist);
}
public void AddCommand(string cmd, int minArgs, int maxArgs, Func<DiscordBotClient.CommandEventArgs, Task> handler, bool? useWhitelist = null)
{
_client.AddCommand(cmd != "" ? _prefix + ' ' + cmd : _prefix, minArgs, maxArgs, handler, useWhitelist ?? _useWhitelist);
}
}
}

View File

@@ -0,0 +1,154 @@
using System.Collections.Generic;
namespace Discord
{
public static class CommandParser
{
private enum CommandParserPart
{
None,
CommandName,
Parameter,
QuotedParameter,
DoubleQuotedParameter
}
public static bool Parse(string input, out string command, out string[] args)
{
return Parse(input, out command, out args, true);
}
public static bool ParseArgs(string input, out string[] args)
{
string ignored;
return Parse(input, out ignored, out args, false);
}
private static bool Parse(string input, out string command, out string[] args, bool parseCommand)
{
CommandParserPart currentPart = parseCommand ? CommandParserPart.CommandName : CommandParserPart.None;
int startPosition = 0;
int endPosition = 0;
int inputLength = input.Length;
bool isEscaped = false;
List<string> argList = new List<string>();
command = null;
args = null;
if (input == "")
return false;
while (endPosition < inputLength)
{
char currentChar = input[endPosition++];
if (isEscaped)
isEscaped = false;
else if (currentChar == '\\')
isEscaped = true;
switch (currentPart)
{
case CommandParserPart.CommandName:
if ((!isEscaped && currentChar == ' ') || endPosition >= inputLength)
{
int length = (currentChar == ' ' ? endPosition - 1 : endPosition) - startPosition;
string temp = input.Substring(startPosition, length);
if (temp == "")
startPosition = endPosition;
else
{
currentPart = CommandParserPart.None;
command = temp;
startPosition = endPosition;
}
}
break;
case CommandParserPart.None:
if ((!isEscaped && currentChar == '\"'))
{
currentPart = CommandParserPart.DoubleQuotedParameter;
startPosition = endPosition;
}
else if ((!isEscaped && currentChar == '\''))
{
currentPart = CommandParserPart.QuotedParameter;
startPosition = endPosition;
}
else if ((!isEscaped && currentChar == ' ') || endPosition >= inputLength)
{
int length = (currentChar == ' ' ? endPosition - 1 : endPosition) - startPosition;
string temp = input.Substring(startPosition, length);
if (temp == "")
startPosition = endPosition;
else
{
currentPart = CommandParserPart.None;
argList.Add(temp);
startPosition = endPosition;
}
}
break;
case CommandParserPart.QuotedParameter:
if ((!isEscaped && currentChar == '\''))
{
string temp = input.Substring(startPosition, endPosition - startPosition - 1);
currentPart = CommandParserPart.None;
argList.Add(temp);
startPosition = endPosition;
}
else if (endPosition >= inputLength)
return false;
break;
case CommandParserPart.DoubleQuotedParameter:
if ((!isEscaped && currentChar == '\"'))
{
string temp = input.Substring(startPosition, endPosition - startPosition - 1);
currentPart = CommandParserPart.None;
argList.Add(temp);
startPosition = endPosition;
}
else if (endPosition >= inputLength)
return false;
break;
}
}
if (parseCommand && (command == null || command == ""))
return false;
args = argList.ToArray();
return true;
}
public static bool ArgsEqual(string[] args, int expected)
{
return args.Length == expected;
}
public static bool ArgsAtLeast(string[] args, int expected)
{
return args.Length >= expected;
}
public static bool ArgsAtMost(string[] args, int expected)
{
return args.Length <= expected;
}
public static bool ArgsIn(string[] args, params int[] expected)
{
int count = args.Length;
for (int i = 0; i < expected.Length; i++)
{
if (count == expected[i])
return true;
}
return false;
}
public static bool ArgsBetween(string[] args, int min, int max)
{
return args.Length >= min && args.Length <= max;
}
public static bool NoArgs(string[] args, params int[] expected)
{
return args.Length == 0;
}
}
}

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>19793545-ef89-48f4-8100-3ebaad0a9141</ProjectGuid>
<RootNamespace>Discord.Net.Commands</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<ProduceOutputsOnBuild>True</ProduceOutputsOnBuild>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@@ -0,0 +1,50 @@
using System;
namespace Discord
{
public partial class DiscordBotClient : DiscordClient
{
public class CommandEventArgs
{
public readonly Message Message;
public readonly Command Command;
public readonly string[] Args;
public User User => Message.User;
public string UserId => Message.UserId;
public Channel Channel => Message.Channel;
public string ChannelId => Message.ChannelId;
public Server Server => Message.Channel.Server;
public string ServerId => Message.Channel.ServerId;
public CommandEventArgs(Message message, Command command, string[] args)
{
Message = message;
Command = command;
Args = args;
}
}
public class CommandErrorEventArgs : CommandEventArgs
{
public readonly Exception Exception;
public CommandErrorEventArgs(Message message, Command command, string[] args, Exception ex)
: base(message, command, args)
{
Exception = ex;
}
}
public event EventHandler<CommandEventArgs> RanCommand;
private void RaiseRanCommand(CommandEventArgs args)
{
if (RanCommand != null)
RanCommand(this, args);
}
public event EventHandler<CommandErrorEventArgs> CommandError;
private void RaiseCommandError(Message msg, Command command, string[] args, Exception ex)
{
if (CommandError != null)
CommandError(this, new CommandErrorEventArgs(msg, command, args, ex));
}
}
}

View File

@@ -0,0 +1,151 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Discord
{
public sealed class Command
{
public readonly string[] Text;
public readonly int MinArgs, MaxArgs;
public readonly bool UseWhitelist;
internal readonly Func<DiscordBotClient.CommandEventArgs, Task> Handler;
public Command(string[] text, int minArgs, int maxArgs, bool useWhitelist, Func<DiscordBotClient.CommandEventArgs, Task> handler)
{
Text = text;
MinArgs = minArgs;
MaxArgs = maxArgs;
UseWhitelist = useWhitelist;
Handler = handler;
}
}
/// <summary>
/// A Discord.Net client with extensions for handling common bot operations like text commands.
/// </summary>
public partial class DiscordBotClient : DiscordClient
{
private List<Command> _commands;
private List<string> _whitelist;
public IEnumerable<Command> Commands => _commands;
public char CommandChar { get; set; }
public bool UseCommandChar { get; set; }
public bool RequireCommandCharInPublic { get; set; }
public bool RequireCommandCharInPrivate { get; set; }
public bool AlwaysUseWhitelist { get; set; }
public DiscordBotClient()
{
_commands = new List<Command>();
_whitelist = new List<string>();
CommandChar = '~';
RequireCommandCharInPublic = true;
RequireCommandCharInPrivate = true;
AlwaysUseWhitelist = false;
MessageCreated += async (s, e) =>
{
//Ignore messages from ourselves
if (e.Message.UserId == UserId)
return;
//Check the global whitelist
if (AlwaysUseWhitelist && !_whitelist.Contains(e.Message.UserId))
return;
//Check for the command character
string msg = e.Message.Text;
if (UseCommandChar)
{
if (msg.Length == 0)
return;
bool isPrivate = e.Message.Channel.IsPrivate;
bool hasCommandChar = msg[0] == CommandChar;
if (hasCommandChar)
msg = msg.Substring(1);
if (!isPrivate && RequireCommandCharInPublic && !hasCommandChar)
return;
if (isPrivate && RequireCommandCharInPrivate && !hasCommandChar)
return;
}
string[] args;
if (!CommandParser.ParseArgs(msg, out args))
return;
for (int i = 0; i < _commands.Count; i++)
{
Command cmd = _commands[i];
//Check Command Parts
if (args.Length < cmd.Text.Length)
continue;
bool isValid = true;
for (int j = 0; j < cmd.Text.Length; j++)
{
if (!string.Equals(args[j], cmd.Text[j], StringComparison.OrdinalIgnoreCase))
{
isValid = false;
break;
}
}
if (!isValid)
continue;
//Check Whitelist
if (cmd.UseWhitelist && !_whitelist.Contains(e.Message.UserId))
continue;
//Check Arg Count
int argCount = args.Length - cmd.Text.Length;
if (argCount < cmd.MinArgs || argCount > cmd.MaxArgs)
continue;
//Run Command
string[] newArgs = new string[argCount];
for (int j = 0; j < newArgs.Length; j++)
newArgs[j] = args[j + cmd.Text.Length];
var eventArgs = new CommandEventArgs(e.Message, cmd, newArgs);
RaiseRanCommand(eventArgs);
try
{
var task = cmd.Handler(eventArgs);
if (task != null)
await task;
}
catch (Exception ex)
{
RaiseCommandError(e.Message, cmd, newArgs, ex);
}
break;
}
};
}
public void AddCommandGroup(string cmd, Action<CommandBuilder> config, bool useWhitelist = false)
{
config(new CommandBuilder(this, cmd, useWhitelist));
}
public void AddCommand(string cmd, int minArgs, int maxArgs, Action<CommandEventArgs> handler, bool useWhitelist = false)
{
AddCommand(cmd, minArgs, maxArgs, e => { handler(e); return null; }, useWhitelist);
}
public void AddCommand(string cmd, int minArgs, int maxArgs, Func<CommandEventArgs, Task> handler, bool useWhitelist = false)
{
_commands.Add(new Command(cmd.Split(' '), minArgs, maxArgs, useWhitelist, handler));
}
public void AddWhitelist(User user)
=> AddWhitelist(user.Id);
public void AddWhitelist(string userId)
{
_whitelist.Add(userId);
}
}
}

View File

@@ -0,0 +1,26 @@
{
"version": "0.2.0-*",
"description": "A small Discord.Net extension to make bot creation easier.",
"authors": [ "RogueException" ],
"tags": [ "discord", "discordapp" ],
"projectUrl": "https://github.com/RogueException/Discord.Net",
"licenseUrl": "http://opensource.org/licenses/MIT",
"repository": {
"type": "git",
"url": "git://github.com/RogueException/Discord.Net"
},
"compilationOptions": {
"warningsAsErrors": true
},
"dependencies": {
"Discord.Net": ""
},
"frameworks": {
"dotnet": {
"dependencies": {
"System.Runtime": "4.0.20",
"Microsoft.CSharp": "4.0.0"
}
}
}
}

View File

@@ -0,0 +1,121 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{8D71A857-879A-4A10-859E-5FF824ED6688}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Discord.Net</RootNamespace>
<AssemblyName>Discord.Net</AssemblyName>
<FileAlignment>512</FileAlignment>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<Reference Include="Newtonsoft.Json, Version=7.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\Discord.Net\API\DiscordAPI.cs">
<Link>API\DiscordAPI.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\API\Endpoints.cs">
<Link>API\Endpoints.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\API\Models\APIRequests.cs">
<Link>API\Models\APIRequests.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\API\Models\APIResponses.cs">
<Link>API\Models\APIResponses.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\API\Models\Common.cs">
<Link>API\Models\Common.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\API\Models\WebSocketCommands.cs">
<Link>API\Models\WebSocketCommands.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\API\Models\WebSocketEvents.cs">
<Link>API\Models\WebSocketEvents.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Channel.cs">
<Link>Channel.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\ChannelTypes.cs">
<Link>ChannelTypes.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\DiscordClient.cs">
<Link>DiscordClient.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\DiscordClient.Events.cs">
<Link>DiscordClient.Events.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\DiscordWebSocket.cs">
<Link>DiscordWebSocket.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\DiscordWebSocket.Events.cs">
<Link>DiscordWebSocket.Events.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Helpers\AsyncCache.cs">
<Link>Helpers\AsyncCache.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Helpers\Http.cs">
<Link>Helpers\Http.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Invite.cs">
<Link>Invite.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Membership.cs">
<Link>Membership.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Message.cs">
<Link>Message.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Regions.cs">
<Link>Regions.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Role.cs">
<Link>Role.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Server.cs">
<Link>Server.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\User.cs">
<Link>User.cs</Link>
</Compile>
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

View File

@@ -0,0 +1,17 @@
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("Discord.Net")]
[assembly: AssemblyDescription("An unofficial .Net API wrapper for the Discord client.")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("RogueException")]
[assembly: AssemblyProduct("Discord.Net")]
[assembly: AssemblyCopyright("Copyright © 2015")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
[assembly: Guid("76ea00e6-ea24-41e1-acb2-639c0313fa80")]
[assembly: AssemblyVersion("0.3.3.0")]
[assembly: AssemblyFileVersion("0.3.3.0")]

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Newtonsoft.Json" version="7.0.1" targetFramework="net45" />
</packages>

View File

@@ -0,0 +1,117 @@
using Discord.API.Models;
using Discord.Helpers;
using System.Threading.Tasks;
namespace Discord.API
{
internal static class DiscordAPI
{
public const int MaxMessageSize = 2000;
//Auth
public static async Task<APIResponses.AuthRegister> LoginAnonymous(string username, HttpOptions options)
{
var fingerprintResponse = await Http.Post<APIResponses.AuthFingerprint>(Endpoints.AuthFingerprint, options);
var registerRequest = new APIRequests.AuthRegisterRequest { Fingerprint = fingerprintResponse.Fingerprint, Username = username };
var registerResponse = await Http.Post<APIResponses.AuthRegister>(Endpoints.AuthRegister, registerRequest, options);
return registerResponse;
}
public static async Task<APIResponses.AuthLogin> Login(string email, string password, HttpOptions options)
{
var request = new APIRequests.AuthLogin { Email = email, Password = password };
var response = await Http.Post<APIResponses.AuthLogin>(Endpoints.AuthLogin, request, options);
options.Token = response.Token;
return response;
}
public static Task Logout(HttpOptions options)
=> Http.Post(Endpoints.AuthLogout, options);
//Servers
public static Task<APIResponses.CreateServer> CreateServer(string name, string region, HttpOptions options)
{
var request = new APIRequests.CreateServer { Name = name, Region = region };
return Http.Post<APIResponses.CreateServer>(Endpoints.Servers, request, options);
}
public static Task LeaveServer(string id, HttpOptions options)
=> Http.Delete<APIResponses.DeleteServer>(Endpoints.Server(id), options);
//Channels
public static Task<APIResponses.CreateChannel> CreateChannel(string serverId, string name, string channelType, HttpOptions options)
{
var request = new APIRequests.CreateChannel { Name = name, Type = channelType };
return Http.Post<APIResponses.CreateChannel>(Endpoints.ServerChannels(serverId), request, options);
}
public static Task<APIResponses.CreateChannel> CreatePMChannel(string myId, string recipientId, HttpOptions options)
{
var request = new APIRequests.CreatePMChannel { RecipientId = recipientId };
return Http.Post<APIResponses.CreateChannel>(Endpoints.UserChannels(myId), request, options);
}
public static Task<APIResponses.DestroyChannel> DestroyChannel(string channelId, HttpOptions options)
=> Http.Delete<APIResponses.DestroyChannel>(Endpoints.Channel(channelId), options);
public static Task<APIResponses.GetMessages[]> GetMessages(string channelId, int count, HttpOptions options)
=> Http.Get<APIResponses.GetMessages[]>(Endpoints.ChannelMessages(channelId, count), options);
//Members
public static Task Kick(string serverId, string memberId, HttpOptions options)
=> Http.Delete(Endpoints.ServerMember(serverId, memberId), options);
public static Task Ban(string serverId, string memberId, HttpOptions options)
=> Http.Put(Endpoints.ServerBan(serverId, memberId), options);
public static Task Unban(string serverId, string memberId, HttpOptions options)
=> Http.Delete(Endpoints.ServerBan(serverId, memberId), options);
//Invites
public static Task<APIResponses.CreateInvite> CreateInvite(string channelId, int maxAge, int maxUses, bool isTemporary, bool hasXkcdPass, HttpOptions options)
{
var request = new APIRequests.CreateInvite { MaxAge = maxAge, MaxUses = maxUses, IsTemporary = isTemporary, HasXkcdPass = hasXkcdPass };
return Http.Post<APIResponses.CreateInvite>(Endpoints.ChannelInvites(channelId), request, options);
}
public static Task<APIResponses.GetInvite> GetInvite(string id, HttpOptions options)
=> Http.Get<APIResponses.GetInvite>(Endpoints.Invite(id), options);
public static Task AcceptInvite(string id, HttpOptions options)
=> Http.Post<APIResponses.AcceptInvite>(Endpoints.Invite(id), options);
public static Task DeleteInvite(string id, HttpOptions options)
=> Http.Delete(Endpoints.Invite(id), options);
//Chat
public static Task<APIResponses.SendMessage> SendMessage(string channelId, string message, string[] mentions, HttpOptions options)
{
var request = new APIRequests.SendMessage { Content = message, Mentions = mentions };
return Http.Post<APIResponses.SendMessage>(Endpoints.ChannelMessages(channelId), request, options);
}
public static Task<APIResponses.EditMessage> EditMessage(string channelId, string messageId, string message, string[] mentions, HttpOptions options)
{
var request = new APIRequests.EditMessage { Content = message, Mentions = mentions };
return Http.Patch<APIResponses.EditMessage>(Endpoints.ChannelMessage(channelId, messageId), request, options);
}
public static Task SendIsTyping(string channelId, HttpOptions options)
=> Http.Post(Endpoints.ChannelTyping(channelId), options);
public static Task DeleteMessage(string channelId, string msgId, HttpOptions options)
=> Http.Delete(Endpoints.ChannelMessage(channelId, msgId), options);
//Voice
public static Task<APIResponses.GetRegions[]> GetVoiceRegions(HttpOptions options)
=> Http.Get<APIResponses.GetRegions[]>(Endpoints.VoiceRegions, options);
public static Task<APIResponses.GetIce> GetVoiceIce(HttpOptions options)
=> Http.Get<APIResponses.GetIce>(Endpoints.VoiceIce, options);
public static Task Mute(string serverId, string memberId, HttpOptions options)
{
var request = new APIRequests.SetMemberMute { Mute = true };
return Http.Patch(Endpoints.ServerMember(serverId, memberId), options);
}
public static Task Unmute(string serverId, string memberId, HttpOptions options)
{
var request = new APIRequests.SetMemberMute { Mute = false };
return Http.Patch(Endpoints.ServerMember(serverId, memberId), options);
}
public static Task Deafen(string serverId, string memberId, HttpOptions options)
{
var request = new APIRequests.SetMemberDeaf { Deaf = true };
return Http.Patch(Endpoints.ServerMember(serverId, memberId), options);
}
public static Task Undeafen(string serverId, string memberId, HttpOptions options)
{
var request = new APIRequests.SetMemberDeaf { Deaf = false };
return Http.Patch(Endpoints.ServerMember(serverId, memberId), options);
}
}
}

View File

@@ -0,0 +1,57 @@
namespace Discord.API
{
internal static class Endpoints
{
public static readonly string BaseUrl = "discordapp.com";
public static readonly string BaseShortUrl = "discord.gg";
public static readonly string BaseHttps = $"https://{BaseUrl}";
// /api
public static readonly string BaseApi = $"{BaseHttps}/api";
public static readonly string Track = $"{BaseApi}/track";
// /api/auth
public static readonly string Auth = $"{BaseApi}/auth";
public static readonly string AuthFingerprint = $"{Auth}fingerprint";
public static readonly string AuthRegister = $"{Auth}/register";
public static readonly string AuthLogin = $"{Auth}/login";
public static readonly string AuthLogout = $"{Auth}/logout";
// /api/channels
public static readonly string Channels = $"{BaseApi}/channels";
public static string Channel(string channelId) => $"{Channels}/{channelId}";
public static string ChannelTyping(string channelId) => $"{Channels}/{channelId}/typing";
public static string ChannelMessages(string channelId) => $"{Channels}/{channelId}/messages";
public static string ChannelMessages(string channelId, int limit) => $"{Channels}/{channelId}/messages?limit={limit}";
public static string ChannelMessage(string channelId, string msgId) => $"{Channels}/{channelId}/messages/{msgId}";
public static string ChannelInvites(string channelId) => $"{Channels}/{channelId}/invites";
// /api/guilds
public static readonly string Servers = $"{BaseApi}/guilds";
public static string Server(string serverId) => $"{Servers}/{serverId}";
public static string ServerChannels(string serverId) => $"{Servers}/{serverId}/channels";
public static string ServerMember(string serverId, string userId) => $"{Servers}/{serverId}/members/{userId}";
public static string ServerBan(string serverId, string userId) => $"{Servers}/{serverId}/bans/{userId}";
// /api/invites
public static readonly string Invites = $"{BaseApi}/invite";
public static string Invite(string id) => $"{Invites}/{id}";
// /api/users
public static readonly string Users = $"{BaseApi}/users";
public static string UserChannels(string userId) => $"{Users}/{userId}/channels";
public static string UserAvatar(string userId, string avatarId) => $"{Users}/{userId}/avatars/{avatarId}.jpg";
// /api/voice
public static readonly string Voice = $"{BaseApi}/voice";
public static readonly string VoiceRegions = $"{Voice}/regions";
public static readonly string VoiceIce = $"{Voice}/ice";
//Web Sockets
public static readonly string BaseWss = "wss://" + BaseUrl;
public static readonly string WebSocket_Hub = $"{BaseWss}/hub";
//Website
public static string InviteUrl(string code) => $"{BaseShortUrl}/{code}";
}
}

View File

@@ -0,0 +1,79 @@
//Ignore unused/unassigned variable warnings
#pragma warning disable CS0649
#pragma warning disable CS0169
using Newtonsoft.Json;
namespace Discord.API.Models
{
internal static class APIRequests
{
public class AuthRegisterRequest
{
[JsonProperty(PropertyName = "fingerprint")]
public string Fingerprint;
[JsonProperty(PropertyName = "username")]
public string Username;
}
public class AuthLogin
{
[JsonProperty(PropertyName = "email")]
public string Email;
[JsonProperty(PropertyName = "password")]
public string Password;
}
public class CreateServer
{
[JsonProperty(PropertyName = "name")]
public string Name;
[JsonProperty(PropertyName = "region")]
public string Region;
}
public class CreateChannel
{
[JsonProperty(PropertyName = "name")]
public string Name;
[JsonProperty(PropertyName = "type")]
public string Type;
}
public class CreatePMChannel
{
[JsonProperty(PropertyName = "recipient_id")]
public string RecipientId;
}
public class CreateInvite
{
[JsonProperty(PropertyName = "max_age")]
public int MaxAge;
[JsonProperty(PropertyName = "max_uses")]
public int MaxUses;
[JsonProperty(PropertyName = "temporary")]
public bool IsTemporary;
[JsonProperty(PropertyName = "xkcdpass")]
public bool HasXkcdPass;
}
public class SendMessage
{
[JsonProperty(PropertyName = "content")]
public string Content;
[JsonProperty(PropertyName = "mentions")]
public string[] Mentions;
}
public class EditMessage : SendMessage { }
public class SetMemberMute
{
[JsonProperty(PropertyName = "mute")]
public bool Mute;
}
public class SetMemberDeaf
{
[JsonProperty(PropertyName = "deaf")]
public bool Deaf;
}
}
}

View File

@@ -0,0 +1,95 @@
//Ignore unused/unassigned variable warnings
#pragma warning disable CS0649
#pragma warning disable CS0169
using Newtonsoft.Json;
using System;
namespace Discord.API.Models
{
internal static class APIResponses
{
public class AuthFingerprint
{
[JsonProperty(PropertyName = "fingerprint")]
public string Fingerprint;
}
public class AuthRegister : AuthLogin { }
public class AuthLogin
{
[JsonProperty(PropertyName = "token")]
public string Token;
}
public class CreateServer : ServerInfo { }
public class DeleteServer : ServerInfo { }
public class CreateChannel : ChannelInfo { }
public class DestroyChannel : ChannelInfo { }
public class CreateInvite : GetInvite
{
[JsonProperty(PropertyName = "max_age")]
public int MaxAge;
[JsonProperty(PropertyName = "max_uses")]
public int MaxUses;
[JsonProperty(PropertyName = "revoked")]
public bool IsRevoked;
[JsonProperty(PropertyName = "temporary")]
public bool IsTemporary;
[JsonProperty(PropertyName = "uses")]
public int Uses;
[JsonProperty(PropertyName = "created_at")]
public DateTime CreatedAt;
}
public class GetInvite
{
[JsonProperty(PropertyName = "inviter")]
public UserReference Inviter;
[JsonProperty(PropertyName = "guild")]
public ServerReference Server;
[JsonProperty(PropertyName = "channel")]
public ChannelReference Channel;
[JsonProperty(PropertyName = "code")]
public string Code;
[JsonProperty(PropertyName = "xkcdpass")]
public string XkcdPass;
}
public class AcceptInvite : GetInvite { }
public class SendMessage : Message { }
public class EditMessage : Message { }
public class GetMessages : Message { }
public class GetRegions
{
[JsonProperty(PropertyName = "sample_hostname")]
public string Hostname;
[JsonProperty(PropertyName = "sample_port")]
public int Port;
[JsonProperty(PropertyName = "id")]
public string Id;
[JsonProperty(PropertyName = "name")]
public string Name;
}
public class GetIce
{
[JsonProperty(PropertyName = "ttl")]
public string TTL;
[JsonProperty(PropertyName = "servers")]
public Server[] Servers;
public class Server
{
[JsonProperty(PropertyName = "url")]
public string URL;
[JsonProperty(PropertyName = "username")]
public string Username;
[JsonProperty(PropertyName = "credential")]
public string Credential;
}
}
}
}

View File

@@ -0,0 +1,181 @@
//Ignore unused/unassigned variable warnings
#pragma warning disable CS0649
#pragma warning disable CS0169
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
namespace Discord.API.Models
{
internal class WebSocketMessage
{
[JsonProperty(PropertyName = "op")]
public int Operation;
[JsonProperty(PropertyName = "t")]
public string Type;
[JsonProperty(PropertyName = "d")]
public object Payload;
}
internal abstract class WebSocketMessage<T> : WebSocketMessage
where T : new()
{
public WebSocketMessage() { Payload = new T(); }
public WebSocketMessage(int op) { Operation = op; Payload = new T(); }
public WebSocketMessage(int op, T payload) { Operation = op; Payload = payload; }
[JsonIgnore]
public new T Payload
{
get { if (base.Payload is JToken) { base.Payload = (base.Payload as JToken).ToObject<T>(); } return (T)base.Payload; }
set { base.Payload = value; }
}
}
//Users
internal class UserReference
{
[JsonProperty(PropertyName = "username")]
public string Username;
[JsonProperty(PropertyName = "id")]
public string Id;
[JsonProperty(PropertyName = "discriminator")]
public string Discriminator;
[JsonProperty(PropertyName = "avatar")]
public string Avatar;
}
internal class SelfUserInfo : UserReference
{
[JsonProperty(PropertyName = "email")]
public string Email;
[JsonProperty(PropertyName = "verified")]
public bool IsVerified;
}
internal class PresenceUserInfo : UserReference
{
[JsonProperty(PropertyName = "game_id")]
public string GameId;
[JsonProperty(PropertyName = "status")]
public string Status;
}
//Channels
internal class ChannelReference
{
[JsonProperty(PropertyName = "id")]
public string Id;
[JsonProperty(PropertyName = "guild_id")]
public string GuildId;
[JsonProperty(PropertyName = "name")]
public string Name;
[JsonProperty(PropertyName = "type")]
public string Type;
}
internal class ChannelInfo : ChannelReference
{
[JsonProperty(PropertyName = "last_message_id")]
public string LastMessageId;
[JsonProperty(PropertyName = "is_private")]
public bool IsPrivate;
[JsonProperty(PropertyName = "permission_overwrites")]
public object[] PermissionOverwrites;
[JsonProperty(PropertyName = "recipient")]
public UserReference Recipient;
}
//Servers
internal class ServerReference
{
[JsonProperty(PropertyName = "id")]
public string Id;
[JsonProperty(PropertyName = "name")]
public string Name;
}
internal class ServerInfo : ServerReference
{
[JsonProperty(PropertyName = "afk_channel_id")]
public string AFKChannelId;
[JsonProperty(PropertyName = "afk_timeout")]
public int AFKTimeout;
[JsonProperty(PropertyName = "embed_channel_id")]
public string EmbedChannelId;
[JsonProperty(PropertyName = "embed_enabled")]
public bool EmbedEnabled;
[JsonProperty(PropertyName = "icon")]
public string Icon;
[JsonProperty(PropertyName = "joined_at")]
public DateTime? JoinedAt;
[JsonProperty(PropertyName = "owner_id")]
public string OwnerId;
[JsonProperty(PropertyName = "region")]
public string Region;
[JsonProperty(PropertyName = "roles")]
public Role[] Roles;
}
internal class ExtendedServerInfo : ServerInfo
{
public class Membership
{
[JsonProperty(PropertyName = "roles")]
public string[] Roles;
[JsonProperty(PropertyName = "mute")]
public bool IsMuted;
[JsonProperty(PropertyName = "deaf")]
public bool IsDeaf;
[JsonProperty(PropertyName = "joined_at")]
public DateTime JoinedAt;
[JsonProperty(PropertyName = "user")]
public UserReference User;
}
[JsonProperty(PropertyName = "channels")]
public ChannelInfo[] Channels;
[JsonProperty(PropertyName = "members")]
public Membership[] Members;
[JsonProperty(PropertyName = "presence")]
public object[] Presence;
[JsonProperty(PropertyName = "voice_states")]
public object[] VoiceStates;
}
//Messages
internal class MessageReference
{
[JsonProperty(PropertyName = "id")]
public string Id;
[JsonProperty(PropertyName = "channel_id")]
public string ChannelId;
[JsonProperty(PropertyName = "message_id")]
public string MessageId { get { return Id; } set { Id = value; } }
}
internal class Message : MessageReference
{
[JsonProperty(PropertyName = "tts")]
public bool IsTextToSpeech;
[JsonProperty(PropertyName = "mention_everyone")]
public bool IsMentioningEveryone;
[JsonProperty(PropertyName = "timestamp")]
public DateTime Timestamp;
[JsonProperty(PropertyName = "mentions")]
public UserReference[] Mentions;
[JsonProperty(PropertyName = "embeds")]
public object[] Embeds;
[JsonProperty(PropertyName = "attachments")]
public object[] Attachments;
[JsonProperty(PropertyName = "content")]
public string Content;
[JsonProperty(PropertyName = "author")]
public UserReference Author;
}
//Roles
internal class Role
{
[JsonProperty(PropertyName = "permissions")]
public int Permissions;
[JsonProperty(PropertyName = "name")]
public string Name;
[JsonProperty(PropertyName = "id")]
public string Id;
}
}

View File

@@ -0,0 +1,41 @@
//Ignore unused/unassigned variable warnings
#pragma warning disable CS0649
#pragma warning disable CS0169
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
namespace Discord.API.Models
{
internal static class WebSocketCommands
{
public sealed class KeepAlive : WebSocketMessage<int>
{
private static DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public KeepAlive() : base(1, (int)(DateTime.UtcNow - epoch).TotalMilliseconds) { }
}
public sealed class Login : WebSocketMessage<Login.Data>
{
public Login() : base(2) { }
public class Data
{
[JsonProperty(PropertyName = "token")]
public string Token;
[JsonProperty(PropertyName = "properties")]
public Dictionary<string, string> Properties = new Dictionary<string, string>();
}
}
public sealed class UpdateStatus : WebSocketMessage<UpdateStatus.Data>
{
public UpdateStatus() : base(3) { }
public class Data
{
[JsonProperty(PropertyName = "idle_since")]
public string IdleSince;
[JsonProperty(PropertyName = "game_id")]
public string GameId;
}
}
}
}

View File

@@ -0,0 +1,143 @@
//Ignore unused/unassigned variable warnings
#pragma warning disable CS0649
#pragma warning disable CS0169
using Newtonsoft.Json;
using System;
namespace Discord.API.Models
{
internal static class WebSocketEvents
{
public sealed class Ready
{
[JsonProperty(PropertyName = "user")]
public SelfUserInfo User;
[JsonProperty(PropertyName = "session_id")]
public string SessionId;
[JsonProperty(PropertyName = "read_state")]
public object[] ReadState;
[JsonProperty(PropertyName = "guilds")]
public ExtendedServerInfo[] Guilds;
[JsonProperty(PropertyName = "private_channels")]
public ChannelInfo[] PrivateChannels;
[JsonProperty(PropertyName = "heartbeat_interval")]
public int HeartbeatInterval;
}
//Servers
public sealed class GuildCreate : ExtendedServerInfo { }
public sealed class GuildUpdate : ServerInfo { }
public sealed class GuildDelete : ExtendedServerInfo { }
//Channels
public sealed class ChannelCreate : ChannelInfo { }
public sealed class ChannelDelete : ChannelInfo { }
public sealed class ChannelUpdate : ChannelInfo { }
//Memberships
public abstract class GuildMemberEvent
{
[JsonProperty(PropertyName = "user")]
public UserReference User;
[JsonProperty(PropertyName = "guild_id")]
public string GuildId;
}
public sealed class GuildMemberAdd : GuildMemberEvent
{
[JsonProperty(PropertyName = "joined_at")]
public DateTime JoinedAt;
[JsonProperty(PropertyName = "roles")]
public string[] Roles;
}
public sealed class GuildMemberUpdate : GuildMemberEvent
{
[JsonProperty(PropertyName = "roles")]
public string[] Roles;
}
public sealed class GuildMemberRemove : GuildMemberEvent { }
//Roles
public abstract class GuildRoleEvent
{
[JsonProperty(PropertyName = "guild_id")]
public string GuildId;
}
public sealed class GuildRoleCreateUpdate : GuildRoleEvent
{
[JsonProperty(PropertyName = "role")]
public Role Role;
}
public sealed class GuildRoleDelete : GuildRoleEvent
{
[JsonProperty(PropertyName = "role_id")]
public string RoleId;
}
//Bans
public abstract class GuildBanEvent
{
[JsonProperty(PropertyName = "guild_id")]
public string GuildId;
}
public sealed class GuildBanAddRemove : GuildBanEvent
{
[JsonProperty(PropertyName = "user")]
public UserReference User;
}
public sealed class GuildBanRemove : GuildBanEvent
{
[JsonProperty(PropertyName = "user_id")]
public string UserId;
}
//User
public sealed class UserUpdate : SelfUserInfo { }
public sealed class PresenceUpdate : PresenceUserInfo { }
public sealed class VoiceStateUpdate
{
[JsonProperty(PropertyName = "user_id")]
public string UserId;
[JsonProperty(PropertyName = "guild_id")]
public string GuildId;
[JsonProperty(PropertyName = "channel_id")]
public string ChannelId;
[JsonProperty(PropertyName = "suppress")]
public bool IsSuppressed;
[JsonProperty(PropertyName = "session_id")]
public string SessionId;
[JsonProperty(PropertyName = "self_mute")]
public bool IsSelfMuted;
[JsonProperty(PropertyName = "self_deaf")]
public bool IsSelfDeafened;
[JsonProperty(PropertyName = "mute")]
public bool IsMuted;
[JsonProperty(PropertyName = "deaf")]
public bool IsDeafened;
}
//Chat
public sealed class MessageCreate : Message { }
public sealed class MessageUpdate : Message { }
public sealed class MessageDelete : MessageReference { }
public sealed class MessageAck : MessageReference { }
public sealed class TypingStart
{
[JsonProperty(PropertyName = "user_id")]
public string UserId;
[JsonProperty(PropertyName = "channel_id")]
public string ChannelId;
[JsonProperty(PropertyName = "timestamp")]
public int Timestamp;
}
//Voice
public sealed class VoiceServerUpdate
{
[JsonProperty(PropertyName = "guild_id")]
public string ServerId;
[JsonProperty(PropertyName = "endpoint")]
public string Endpoint;
}
}
}

View File

@@ -0,0 +1,46 @@
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Linq;
namespace Discord
{
public sealed class Channel
{
private readonly DiscordClient _client;
public string Id { get; }
private string _name;
public string Name { get { return !IsPrivate ? $"#{_name}" : $"@{Recipient.Name}"; } internal set { _name = value; } }
public bool IsPrivate { get; }
public string Type { get; internal set; }
public string ServerId { get; }
[JsonIgnore]
public Server Server => ServerId != null ? _client.GetServer(ServerId) : null;
[JsonIgnore]
public string RecipientId { get; internal set; }
public User Recipient => _client.GetUser(RecipientId);
[JsonIgnore]
public IEnumerable<Message> Messages => _client.Messages.Where(x => x.ChannelId == Id);
//Not Implemented
public object[] PermissionOverwrites { get; internal set; }
internal Channel(string id, string serverId, DiscordClient client)
{
Id = id;
ServerId = serverId;
IsPrivate = serverId == null;
_client = client;
}
public override string ToString()
{
return Name;
}
}
}

View File

@@ -0,0 +1,8 @@
namespace Discord
{
public static class ChannelTypes
{
public const string Text = "text";
public const string Voice = "voice";
}
}

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>acfb060b-ec8a-4926-b293-04c01e17ee23</ProjectGuid>
<RootNamespace>Discord.Net</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<ProduceOutputsOnBuild>True</ProduceOutputsOnBuild>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@@ -0,0 +1,252 @@
using System;
namespace Discord
{
public partial class DiscordClient
{
//Debug
public sealed class LogMessageEventArgs : EventArgs
{
public readonly string Message;
internal LogMessageEventArgs(string msg) { Message = msg; }
}
public event EventHandler<LogMessageEventArgs> DebugMessage;
private void RaiseOnDebugMessage(string message)
{
if (DebugMessage != null)
DebugMessage(this, new LogMessageEventArgs(message));
}
//General
public event EventHandler Connected;
private void RaiseConnected()
{
if (Connected != null)
Connected(this, EventArgs.Empty);
}
public event EventHandler Disconnected;
private void RaiseDisconnected()
{
if (Disconnected != null)
Disconnected(this, EventArgs.Empty);
}
//Server
public sealed class ServerEventArgs : EventArgs
{
public readonly Server Server;
internal ServerEventArgs(Server server) { Server = server; }
}
public event EventHandler<ServerEventArgs> ServerCreated;
private void RaiseServerCreated(Server server)
{
if (ServerCreated != null)
ServerCreated(this, new ServerEventArgs(server));
}
public event EventHandler<ServerEventArgs> ServerDestroyed;
private void RaiseServerDestroyed(Server server)
{
if (ServerDestroyed != null)
ServerDestroyed(this, new ServerEventArgs(server));
}
public event EventHandler<ServerEventArgs> ServerUpdated;
private void RaiseServerUpdated(Server server)
{
if (ServerUpdated != null)
ServerUpdated(this, new ServerEventArgs(server));
}
//Channel
public sealed class ChannelEventArgs : EventArgs
{
public readonly Channel Channel;
internal ChannelEventArgs(Channel channel) { Channel = channel; }
}
public event EventHandler<ChannelEventArgs> ChannelCreated;
private void RaiseChannelCreated(Channel channel)
{
if (ChannelCreated != null)
ChannelCreated(this, new ChannelEventArgs(channel));
}
public event EventHandler<ChannelEventArgs> ChannelDestroyed;
private void RaiseChannelDestroyed(Channel channel)
{
if (ChannelDestroyed != null)
ChannelDestroyed(this, new ChannelEventArgs(channel));
}
public event EventHandler<ChannelEventArgs> ChannelUpdated;
private void RaiseChannelUpdated(Channel channel)
{
if (ChannelUpdated != null)
ChannelUpdated(this, new ChannelEventArgs(channel));
}
//User
public sealed class UserEventArgs : EventArgs
{
public readonly User User;
internal UserEventArgs(User user) { User = user; }
}
//Message
public sealed class MessageEventArgs : EventArgs
{
public readonly Message Message;
internal MessageEventArgs(Message msg) { Message = msg; }
}
public event EventHandler<MessageEventArgs> MessageCreated;
private void RaiseMessageCreated(Message msg)
{
if (MessageCreated != null)
MessageCreated(this, new MessageEventArgs(msg));
}
public event EventHandler<MessageEventArgs> MessageDeleted;
private void RaiseMessageDeleted(Message msg)
{
if (MessageDeleted != null)
MessageDeleted(this, new MessageEventArgs(msg));
}
public event EventHandler<MessageEventArgs> MessageUpdated;
private void RaiseMessageUpdated(Message msg)
{
if (MessageUpdated != null)
MessageUpdated(this, new MessageEventArgs(msg));
}
public event EventHandler<MessageEventArgs> MessageAcknowledged;
private void RaiseMessageAcknowledged(Message msg)
{
if (MessageAcknowledged != null)
MessageAcknowledged(this, new MessageEventArgs(msg));
}
//Role
public sealed class RoleEventArgs : EventArgs
{
public readonly Role Role;
internal RoleEventArgs(Role role) { Role = role; }
}
public event EventHandler<RoleEventArgs> RoleCreated;
private void RaiseRoleCreated(Role role)
{
if (RoleCreated != null)
RoleCreated(this, new RoleEventArgs(role));
}
public event EventHandler<RoleEventArgs> RoleUpdated;
private void RaiseRoleDeleted(Role role)
{
if (RoleDeleted != null)
RoleDeleted(this, new RoleEventArgs(role));
}
public event EventHandler<RoleEventArgs> RoleDeleted;
private void RaiseRoleUpdated(Role role)
{
if (RoleUpdated != null)
RoleUpdated(this, new RoleEventArgs(role));
}
//Ban
public sealed class BanEventArgs : EventArgs
{
public readonly User User;
public readonly Server Server;
internal BanEventArgs(User user, Server server)
{
User = user;
Server = server;
}
}
public event EventHandler<BanEventArgs> BanAdded;
private void RaiseBanAdded(User user, Server server)
{
if (BanAdded != null)
BanAdded(this, new BanEventArgs(user, server));
}
public event EventHandler<BanEventArgs> BanRemoved;
private void RaiseBanRemoved(User user, Server server)
{
if (BanRemoved != null)
BanRemoved(this, new BanEventArgs(user, server));
}
//Member
public sealed class MemberEventArgs : EventArgs
{
public readonly Membership Membership;
internal MemberEventArgs(Membership membership) { Membership = membership; }
}
public event EventHandler<MemberEventArgs> MemberAdded;
private void RaiseMemberAdded(Membership membership, Server server)
{
if (MemberAdded != null)
MemberAdded(this, new MemberEventArgs(membership));
}
public event EventHandler<MemberEventArgs> MemberRemoved;
private void RaiseMemberRemoved(Membership membership, Server server)
{
if (MemberRemoved != null)
MemberRemoved(this, new MemberEventArgs(membership));
}
public event EventHandler<MemberEventArgs> MemberUpdated;
private void RaiseMemberUpdated(Membership membership, Server server)
{
if (MemberUpdated != null)
MemberUpdated(this, new MemberEventArgs(membership));
}
//Status
public sealed class UserTypingEventArgs : EventArgs
{
public readonly User User;
public readonly Channel Channel;
internal UserTypingEventArgs(User user, Channel channel)
{
User = user;
Channel = channel;
}
}
public event EventHandler<UserEventArgs> PresenceUpdated;
private void RaisePresenceUpdated(User user)
{
if (PresenceUpdated != null)
PresenceUpdated(this, new UserEventArgs(user));
}
public event EventHandler<UserEventArgs> VoiceStateUpdated;
private void RaiseVoiceStateUpdated(User user)
{
if (VoiceStateUpdated != null)
VoiceStateUpdated(this, new UserEventArgs(user));
}
public event EventHandler<UserTypingEventArgs> UserTyping;
private void RaiseUserTyping(User user, Channel channel)
{
if (UserTyping != null)
UserTyping(this, new UserTypingEventArgs(user, channel));
}
//Voice
public sealed class VoiceServerUpdatedEventArgs : EventArgs
{
public readonly Server Server;
public readonly string Endpoint;
internal VoiceServerUpdatedEventArgs(Server server, string endpoint)
{
Server = server;
Endpoint = endpoint;
}
}
public event EventHandler<VoiceServerUpdatedEventArgs> VoiceServerUpdated;
private void RaiseVoiceServerUpdated(Server server, string endpoint)
{
if (VoiceServerUpdated != null)
VoiceServerUpdated(this, new VoiceServerUpdatedEventArgs(server, endpoint));
}
}
}

View File

@@ -0,0 +1,812 @@
using Discord.API;
using Discord.API.Models;
using Discord.Helpers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace Discord
{
public partial class DiscordClient
{
public const int ReconnectDelay = 1000; //Time in milliseconds to wait after an unexpected disconnect before retrying
public const int FailedReconnectDelay = 10000; //Time in milliseconds to wait after a failed reconnect attempt
private DiscordWebSocket _webSocket;
private HttpOptions _httpOptions;
private bool _isReady;
public string UserId { get; private set; }
public User User => _users[UserId];
public IEnumerable<User> Users => _users;
private AsyncCache<User, API.Models.UserReference> _users;
public IEnumerable<Server> Servers => _servers;
private AsyncCache<Server, API.Models.ServerReference> _servers;
public IEnumerable<Channel> Channels => _channels;
private AsyncCache<Channel, API.Models.ChannelReference> _channels;
public IEnumerable<Message> Messages => _messages;
private AsyncCache<Message, API.Models.MessageReference> _messages;
public IEnumerable<Role> Roles => _roles;
private AsyncCache<Role, API.Models.Role> _roles;
private ManualResetEventSlim _isStopping;
public bool IsConnected => _isReady;
public DiscordClient()
{
_isStopping = new ManualResetEventSlim(false);
string version = typeof(DiscordClient).GetTypeInfo().Assembly.GetName().Version.ToString(2);
_httpOptions = new HttpOptions($"Discord.Net/{version} (https://github.com/RogueException/Discord.Net)");
_servers = new AsyncCache<Server, API.Models.ServerReference>(
(key, parentKey) => new Server(key, this),
(server, model) =>
{
server.Name = model.Name;
if (!server.Channels.Any()) //Assume a default channel exists with the same id as the server. Not sure if this is safe?
{
var defaultChannel = new ChannelReference() { Id = server.DefaultChannelId, GuildId = server.Id };
_channels.Update(defaultChannel.Id, defaultChannel.GuildId, defaultChannel);
}
if (model is ExtendedServerInfo)
{
var extendedModel = model as ExtendedServerInfo;
server.AFKChannelId = extendedModel.AFKChannelId;
server.AFKTimeout = extendedModel.AFKTimeout;
server.JoinedAt = extendedModel.JoinedAt ?? DateTime.MinValue;
server.OwnerId = extendedModel.OwnerId;
server.Presence = extendedModel.Presence;
server.Region = extendedModel.Region;
server.VoiceStates = extendedModel.VoiceStates;
foreach (var role in extendedModel.Roles)
_roles.Update(role.Id, model.Id, role);
foreach (var channel in extendedModel.Channels)
_channels.Update(channel.Id, model.Id, channel);
foreach (var membership in extendedModel.Members)
{
_users.Update(membership.User.Id, membership.User);
server.AddMember(new Membership(server.Id, membership.User.Id, membership.JoinedAt, this) { RoleIds = membership.Roles, IsMuted = membership.IsMuted, IsDeafened = membership.IsDeaf });
}
}
},
server => { }
);
_channels = new AsyncCache<Channel, API.Models.ChannelReference>(
(key, parentKey) => new Channel(key, parentKey, this),
(channel, model) =>
{
channel.Name = model.Name;
channel.Type = model.Type;
if (model is ChannelInfo)
{
var extendedModel = model as ChannelInfo;
channel.PermissionOverwrites = extendedModel.PermissionOverwrites;
channel.RecipientId = extendedModel.Recipient?.Id;
}
},
channel => { });
_messages = new AsyncCache<Message, API.Models.MessageReference>(
(key, parentKey) => new Message(key, parentKey, this),
(message, model) =>
{
if (model is API.Models.Message)
{
var extendedModel = model as API.Models.Message;
message.Attachments = extendedModel.Attachments;
message.Embeds = extendedModel.Embeds;
message.IsMentioningEveryone = extendedModel.IsMentioningEveryone;
message.IsTTS = extendedModel.IsTextToSpeech;
message.MentionIds = extendedModel.Mentions?.Select(x => x.Id)?.ToArray() ?? new string[0];
if (extendedModel.Author != null)
message.UserId = extendedModel.Author.Id;
message.Timestamp = extendedModel.Timestamp;
message.Text = extendedModel.Content;
}
},
message => { }
);
_roles = new AsyncCache<Role, API.Models.Role>(
(key, parentKey) => new Role(key, parentKey, this),
(role, model) =>
{
role.Name = model.Name;
role.Permissions.RawValue = (uint)model.Permissions;
},
role => { }
);
_users = new AsyncCache<User, API.Models.UserReference>(
(key, parentKey) => new User(key, this),
(user, model) =>
{
user.AvatarId = model.Avatar;
user.Discriminator = model.Discriminator;
user.Name = model.Username;
if (model is SelfUserInfo)
{
var extendedModel = model as SelfUserInfo;
user.Email = extendedModel.Email;
user.IsVerified = extendedModel.IsVerified;
}
if (model is PresenceUserInfo)
{
var extendedModel = model as PresenceUserInfo;
user.GameId = extendedModel.GameId;
user.Status = extendedModel.Status;
}
},
user => { }
);
_webSocket = new DiscordWebSocket();
_webSocket.Connected += (s, e) => RaiseConnected();
_webSocket.Disconnected += async (s, e) =>
{
//Reconnect if we didn't cause the disconnect
RaiseDisconnected();
while (!_isStopping.IsSet)
{
try
{
await Task.Delay(ReconnectDelay);
await _webSocket.ConnectAsync(Endpoints.WebSocket_Hub, true, _httpOptions);
break;
}
catch (Exception)
{
//Net is down? We can keep trying to reconnect until the user runs Disconnect()
await Task.Delay(FailedReconnectDelay);
}
}
};
_webSocket.GotEvent += (s, e) =>
{
switch (e.Type)
{
//Global
case "READY": //Resync
{
var data = e.Event.ToObject<WebSocketEvents.Ready>();
_servers.Clear();
_channels.Clear();
_users.Clear();
UserId = data.User.Id;
_users.Update(data.User.Id, data.User);
foreach (var server in data.Guilds)
_servers.Update(server.Id, server);
foreach (var channel in data.PrivateChannels)
_channels.Update(channel.Id, null, channel);
}
break;
//Servers
case "GUILD_CREATE":
{
var data = e.Event.ToObject<WebSocketEvents.GuildCreate>();
var server = _servers.Update(data.Id, data);
RaiseServerCreated(server);
}
break;
case "GUILD_UPDATE":
{
var data = e.Event.ToObject<WebSocketEvents.GuildUpdate>();
var server = _servers.Update(data.Id, data);
RaiseServerUpdated(server);
}
break;
case "GUILD_DELETE":
{
var data = e.Event.ToObject<WebSocketEvents.GuildDelete>();
var server = _servers.Remove(data.Id);
if (server != null)
RaiseServerDestroyed(server);
}
break;
//Channels
case "CHANNEL_CREATE":
{
var data = e.Event.ToObject<WebSocketEvents.ChannelCreate>();
var channel = _channels.Update(data.Id, data.GuildId, data);
RaiseChannelCreated(channel);
}
break;
case "CHANNEL_UPDATE":
{
var data = e.Event.ToObject<WebSocketEvents.ChannelUpdate>();
var channel = _channels.Update(data.Id, data.GuildId, data);
RaiseChannelUpdated(channel);
}
break;
case "CHANNEL_DELETE":
{
var data = e.Event.ToObject<WebSocketEvents.ChannelDelete>();
var channel = _channels.Remove(data.Id);
if (channel != null)
RaiseChannelDestroyed(channel);
}
break;
//Members
case "GUILD_MEMBER_ADD":
{
var data = e.Event.ToObject<WebSocketEvents.GuildMemberAdd>();
var user = _users.Update(data.User.Id, data.User);
var server = _servers[data.GuildId];
var membership = new Membership(server.Id, data.User.Id, data.JoinedAt, this) { RoleIds = data.Roles };
server.AddMember(membership);
RaiseMemberAdded(membership, server);
}
break;
case "GUILD_MEMBER_UPDATE":
{
var data = e.Event.ToObject<WebSocketEvents.GuildMemberUpdate>();
var user = _users.Update(data.User.Id, data.User);
var server = _servers[data.GuildId];
var membership = server.GetMembership(data.User.Id);
if (membership != null)
membership.RoleIds = data.Roles;
RaiseMemberUpdated(membership, server);
}
break;
case "GUILD_MEMBER_REMOVE":
{
var data = e.Event.ToObject<WebSocketEvents.GuildMemberRemove>();
var user = _users.Update(data.User.Id, data.User);
var server = _servers[data.GuildId];
if (server != null)
{
var membership = server.RemoveMember(user.Id);
if (membership != null)
RaiseMemberRemoved(membership, server);
}
}
break;
//Roles
case "GUILD_ROLE_CREATE":
{
var data = e.Event.ToObject<WebSocketEvents.GuildRoleCreateUpdate>();
var role = _roles.Update(data.Role.Id, data.GuildId, data.Role);
RaiseRoleCreated(role);
}
break;
case "GUILD_ROLE_UPDATE":
{
var data = e.Event.ToObject<WebSocketEvents.GuildRoleCreateUpdate>();
var role = _roles.Update(data.Role.Id, data.GuildId, data.Role);
RaiseRoleUpdated(role);
}
break;
case "GUILD_ROLE_DELETE":
{
var data = e.Event.ToObject<WebSocketEvents.GuildRoleDelete>();
var role = _roles.Remove(data.RoleId);
if (role != null)
RaiseRoleDeleted(role);
}
break;
//Bans
case "GUILD_BAN_ADD":
{
var data = e.Event.ToObject<WebSocketEvents.GuildBanAddRemove>();
var user = _users.Update(data.User.Id, data.User);
var server = _servers[data.GuildId];
RaiseBanAdded(user, server);
}
break;
case "GUILD_BAN_REMOVE":
{
var data = e.Event.ToObject<WebSocketEvents.GuildBanAddRemove>();
var user = _users.Update(data.User.Id, data.User);
var server = _servers[data.GuildId];
if (server != null && server.RemoveBan(user.Id))
RaiseBanRemoved(user, server);
}
break;
//Messages
case "MESSAGE_CREATE":
{
var data = e.Event.ToObject<WebSocketEvents.MessageCreate>();
var msg = _messages.Update(data.Id, data.ChannelId, data);
msg.User.UpdateActivity(data.Timestamp);
RaiseMessageCreated(msg);
}
break;
case "MESSAGE_UPDATE":
{
var data = e.Event.ToObject<WebSocketEvents.MessageUpdate>();
var msg = _messages.Update(data.Id, data.ChannelId, data);
RaiseMessageUpdated(msg);
}
break;
case "MESSAGE_DELETE":
{
var data = e.Event.ToObject<WebSocketEvents.MessageDelete>();
var msg = GetMessage(data.MessageId);
if (msg != null)
_messages.Remove(msg.Id);
}
break;
case "MESSAGE_ACK":
{
var data = e.Event.ToObject<WebSocketEvents.MessageAck>();
var msg = GetMessage(data.MessageId);
RaiseMessageAcknowledged(msg);
}
break;
//Statuses
case "PRESENCE_UPDATE":
{
var data = e.Event.ToObject<WebSocketEvents.PresenceUpdate>();
var user = _users.Update(data.Id, data);
RaisePresenceUpdated(user);
}
break;
case "VOICE_STATE_UPDATE":
{
var data = e.Event.ToObject<WebSocketEvents.VoiceStateUpdate>();
var user = _users[data.UserId]; //TODO: Don't ignore this
RaiseVoiceStateUpdated(user);
}
break;
case "TYPING_START":
{
var data = e.Event.ToObject<WebSocketEvents.TypingStart>();
var channel = _channels[data.ChannelId];
var user = _users[data.UserId];
RaiseUserTyping(user, channel);
}
break;
//Voice
case "VOICE_SERVER_UPDATE":
{
var data = e.Event.ToObject<WebSocketEvents.VoiceServerUpdate>();
var server = _servers[data.ServerId];
RaiseVoiceServerUpdated(server, data.Endpoint);
}
break;
//Others
default:
RaiseOnDebugMessage("Unknown WebSocket message type: " + e.Type);
break;
}
};
_webSocket.OnDebugMessage += (s, e) => RaiseOnDebugMessage(e.Message);
}
//Collections
public User GetUser(string id) => _users[id];
public User FindUser(string name)
{
return _users
.Where(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase))
.FirstOrDefault();
}
public User FindUser(string name, string discriminator)
{
if (name.StartsWith("@"))
name = name.Substring(1);
return _users
.Where(x =>
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) &&
x.Discriminator == discriminator
)
.FirstOrDefault();
}
public User FindChannelUser(Channel channel, string name)
=> FindChannelUser(channel.Id, name);
public User FindChannelUser(string channelId, string name)
{
if (name.StartsWith("@"))
name = name.Substring(1);
return _users
.Where(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase))
.FirstOrDefault();
}
public Server GetServer(string id) => _servers[id];
public Server FindServer(string name)
{
return _servers
.Where(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase))
.FirstOrDefault();
}
public Channel GetChannel(string id) => _channels[id];
public Channel FindChannel(string name)
{
if (name.StartsWith("#"))
name = name.Substring(1);
return _channels
.Where(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase))
.FirstOrDefault();
}
public Channel FindChannel(Server server, string name)
=> FindChannel(server.Id, name);
public Channel FindChannel(string serverId, string name)
{
if (name.StartsWith("#"))
name = name.Substring(1);
return _channels
.Where(x =>
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) &&
x.ServerId == serverId
)
.FirstOrDefault();
}
public Role GetRole(string id) => _roles[id];
public Role FindRole(Server server, string name)
=> FindRole(server.Id, name);
public Role FindRole(string serverId, string name)
{
return _roles
.Where(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase))
.FirstOrDefault();
}
public Message GetMessage(string id) => _messages[id];
public Task<Message[]> DownloadMessages(Channel channel, int count)
=> DownloadMessages(channel.Id, count);
public async Task<Message[]> DownloadMessages(string channelId, int count)
{
Channel channel = GetChannel(channelId);
if (channel != null && channel.Type == ChannelTypes.Text)
{
try
{
var msgs = await DiscordAPI.GetMessages(channel.Id, count, _httpOptions);
return msgs.OrderBy(x => x.Timestamp)
.Select(x =>
{
var msg = _messages.Update(x.Id, x.ChannelId, x);
var user = msg.User;
if (user != null)
user.UpdateActivity(x.Timestamp);
return msg;
})
.ToArray();
}
catch { } //Bad Permissions?
}
return null;
}
//Auth
public async Task Connect(string email, string password)
{
_isStopping.Reset();
//Open websocket while we wait for login response
Task socketTask = _webSocket.ConnectAsync(Endpoints.WebSocket_Hub, false, _httpOptions);
var response = await DiscordAPI.Login(email, password, _httpOptions);
_httpOptions.Token = response.Token;
//Wait for websocket to finish connecting, then send token
await socketTask;
_webSocket.Login(_httpOptions);
_isReady = true;
}
public async Task ConnectAnonymous(string username)
{
_isStopping.Reset();
//Open websocket while we wait for login response
Task socketTask = _webSocket.ConnectAsync(Endpoints.WebSocket_Hub, false, _httpOptions);
var response = await DiscordAPI.LoginAnonymous(username, _httpOptions);
_httpOptions.Token = response.Token;
//Wait for websocket to finish connecting, then send token
await socketTask;
_webSocket.Login(_httpOptions);
_isReady = true;
}
public async Task Disconnect()
{
_isReady = false;
_isStopping.Set();
await _webSocket.DisconnectAsync();
_channels.Clear();
_messages.Clear();
_roles.Clear();
_servers.Clear();
_users.Clear();
}
//Servers
public async Task<Server> CreateServer(string name, string region)
{
CheckReady();
var response = await DiscordAPI.CreateServer(name, region, _httpOptions);
return _servers.Update(response.Id, response);
}
public Task<Server> LeaveServer(Server server)
=> LeaveServer(server.Id);
public async Task<Server> LeaveServer(string serverId)
{
CheckReady();
try
{
await DiscordAPI.LeaveServer(serverId, _httpOptions);
}
catch (WebException ex) when ((ex.Response as HttpWebResponse)?.StatusCode == HttpStatusCode.NotFound) {}
return _servers.Remove(serverId);
}
//Channels
public Task<Channel> CreateChannel(Server server, string name, string type)
=> CreateChannel(server.Id, name, type);
public async Task<Channel> CreateChannel(string serverId, string name, string type)
{
CheckReady();
var response = await DiscordAPI.CreateChannel(serverId, name, type, _httpOptions);
return _channels.Update(response.Id, serverId, response);
}
public Task<Channel> CreatePMChannel(User user)
=> CreatePMChannel(user.Id);
public async Task<Channel> CreatePMChannel(string recipientId)
{
CheckReady();
var response = await DiscordAPI.CreatePMChannel(UserId, recipientId, _httpOptions);
return _channels.Update(response.Id, response);
}
public Task<Channel> DestroyChannel(Channel channel)
=> DestroyChannel(channel.Id);
public async Task<Channel> DestroyChannel(string channelId)
{
CheckReady();
try
{
var response = await DiscordAPI.DestroyChannel(channelId, _httpOptions);
}
catch (WebException ex) when ((ex.Response as HttpWebResponse)?.StatusCode == HttpStatusCode.NotFound) { }
return _channels.Remove(channelId);
}
//Bans
public Task Ban(Server server, User user)
=> Ban(server.Id, user.Id);
public Task Ban(Server server, string userId)
=> Ban(server.Id, userId);
public Task Ban(string server, User user)
=> Ban(server, user.Id);
public Task Ban(string serverId, string userId)
{
CheckReady();
return DiscordAPI.Ban(serverId, userId, _httpOptions);
}
public Task Unban(Server server, User user)
=> Unban(server.Id, user.Id);
public Task Unban(Server server, string userId)
=> Unban(server.Id, userId);
public Task Unban(string server, User user)
=> Unban(server, user.Id);
public async Task Unban(string serverId, string userId)
{
CheckReady();
try
{
await DiscordAPI.Unban(serverId, userId, _httpOptions);
}
catch (WebException ex) when ((ex.Response as HttpWebResponse)?.StatusCode == HttpStatusCode.NotFound) { }
}
//Invites
public Task<Invite> CreateInvite(Server server, int maxAge, int maxUses, bool isTemporary, bool hasXkcdPass)
{
return CreateInvite(server.DefaultChannelId, maxAge, maxUses, isTemporary, hasXkcdPass);
}
public Task<Invite> CreateInvite(Channel channel, int maxAge, int maxUses, bool isTemporary, bool hasXkcdPass)
{
return CreateInvite(channel, maxAge, maxUses, isTemporary, hasXkcdPass);
}
public async Task<Invite> CreateInvite(string channelId, int maxAge, int maxUses, bool isTemporary, bool hasXkcdPass)
{
CheckReady();
var response = await DiscordAPI.CreateInvite(channelId, maxAge, maxUses, isTemporary, hasXkcdPass, _httpOptions);
_channels.Update(response.Channel.Id, response.Server.Id, response.Channel);
_servers.Update(response.Server.Id, response.Server);
_users.Update(response.Inviter.Id, response.Inviter);
return new Invite(response.Code, response.XkcdPass, this)
{
ChannelId = response.Channel.Id,
InviterId = response.Inviter.Id,
ServerId = response.Server.Id,
IsRevoked = response.IsRevoked,
IsTemporary = response.IsTemporary,
MaxAge = response.MaxAge,
MaxUses = response.MaxUses,
Uses = response.Uses
};
}
public async Task<Invite> GetInvite(string id)
{
CheckReady();
var response = await DiscordAPI.GetInvite(id, _httpOptions);
return new Invite(response.Code, response.XkcdPass, this)
{
ChannelId = response.Channel.Id,
InviterId = response.Inviter.Id,
ServerId = response.Server.Id
};
}
public Task AcceptInvite(Invite invite)
{
CheckReady();
return DiscordAPI.AcceptInvite(invite.Code, _httpOptions);
}
public async Task AcceptInvite(string id)
{
CheckReady();
//Check if this is a human-readable link and get its ID
var response = await DiscordAPI.GetInvite(id, _httpOptions);
await DiscordAPI.AcceptInvite(response.Code, _httpOptions);
}
public async Task DeleteInvite(string id)
{
CheckReady();
try
{
//Check if this is a human-readable link and get its ID
var response = await DiscordAPI.GetInvite(id, _httpOptions);
await DiscordAPI.DeleteInvite(response.Code, _httpOptions);
}
catch (WebException ex) when ((ex.Response as HttpWebResponse)?.StatusCode == HttpStatusCode.NotFound) { }
}
//Chat
public Task<Message[]> SendMessage(Channel channel, string text)
=> SendMessage(channel.Id, text, new string[0]);
public Task<Message[]> SendMessage(string channelId, string text)
=> SendMessage(channelId, text, new string[0]);
public Task<Message[]> SendMessage(Channel channel, string text, string[] mentions)
=> SendMessage(channel, text, mentions);
public async Task<Message[]> SendMessage(string channelId, string text, string[] mentions)
{
CheckReady();
if (text.Length <= 2000)
{
var msg = await DiscordAPI.SendMessage(channelId, text, mentions, _httpOptions);
return new Message[] { _messages.Update(msg.Id, channelId, msg) };
}
else
{
int blockCount = (int)Math.Ceiling(text.Length / (double)DiscordAPI.MaxMessageSize);
Message[] result = new Message[blockCount];
for (int i = 0; i < blockCount; i++)
{
int index = i * DiscordAPI.MaxMessageSize;
var msg = await DiscordAPI.SendMessage(channelId, text.Substring(index, Math.Min(2000, text.Length - index)), mentions, _httpOptions);
result[i] = _messages.Update(msg.Id, channelId, msg);
await Task.Delay(1000);
}
return result;
}
}
public Task EditMessage(Message message, string text)
=> EditMessage(message.ChannelId, message.Id, text, new string[0]);
public Task EditMessage(Channel channel, string messageId, string text)
=> EditMessage(channel.Id, messageId, text, new string[0]);
public Task EditMessage(string channelId, string messageId, string text)
=> EditMessage(channelId, messageId, text, new string[0]);
public Task EditMessage(Message message, string text, string[] mentions)
=> EditMessage(message.ChannelId, message.Id, text, mentions);
public Task EditMessage(Channel channel, string messageId, string text, string[] mentions)
=> EditMessage(channel.Id, messageId, text, mentions);
public async Task EditMessage(string channelId, string messageId, string text, string[] mentions)
{
CheckReady();
if (text.Length > DiscordAPI.MaxMessageSize)
text = text.Substring(0, DiscordAPI.MaxMessageSize);
var msg = await DiscordAPI.EditMessage(channelId, messageId, text, mentions, _httpOptions);
_messages.Update(msg.Id, channelId, msg);
}
public Task DeleteMessage(Message msg)
=> DeleteMessage(msg.ChannelId, msg.Id);
public async Task<Message> DeleteMessage(string channelId, string msgId)
{
try
{
await DiscordAPI.DeleteMessage(channelId, msgId, _httpOptions);
return _messages.Remove(msgId);
}
catch (WebException ex) when ((ex.Response as HttpWebResponse)?.StatusCode == HttpStatusCode.NotFound) { }
catch (WebException ex) when ((ex.Response as HttpWebResponse)?.StatusCode == HttpStatusCode.InternalServerError) { } //TODO: Remove me - temporary fix for deleting nonexisting messages
return null;
}
//Voice
public Task Mute(Server server, User user)
=> Mute(server.Id, user.Id);
public Task Mute(Server server, string userId)
=> Mute(server.Id, userId);
public Task Mute(string server, User user)
=> Mute(server, user.Id);
public Task Mute(string serverId, string userId)
{
CheckReady();
return DiscordAPI.Mute(serverId, userId, _httpOptions);
}
public Task Unmute(Server server, User user)
=> Unmute(server.Id, user.Id);
public Task Unmute(Server server, string userId)
=> Unmute(server.Id, userId);
public Task Unmute(string server, User user)
=> Unmute(server, user.Id);
public Task Unmute(string serverId, string userId)
{
CheckReady();
return DiscordAPI.Unmute(serverId, userId, _httpOptions);
}
public Task Deafen(Server server, User user)
=> Deafen(server.Id, user.Id);
public Task Deafen(Server server, string userId)
=> Deafen(server.Id, userId);
public Task Deafen(string server, User user)
=> Deafen(server, user.Id);
public Task Deafen(string serverId, string userId)
{
CheckReady();
return DiscordAPI.Deafen(serverId, userId, _httpOptions);
}
public Task Undeafen(Server server, User user)
=> Undeafen(server.Id, user.Id);
public Task Undeafen(Server server, string userId)
=> Undeafen(server.Id, userId);
public Task Undeafen(string server, User user)
=> Undeafen(server, user.Id);
public Task Undeafen(string serverId, string userId)
{
CheckReady();
return DiscordAPI.Undeafen(serverId, userId, _httpOptions);
}
private void CheckReady()
{
if (!_isReady)
throw new InvalidOperationException("The client is not currently connected to Discord");
}
/// <summary>
/// Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications.
/// </summary>
/// <returns></returns>
public void Block()
{
_isStopping.Wait();
}
}
}

View File

@@ -0,0 +1,46 @@
using Newtonsoft.Json.Linq;
using System;
namespace Discord
{
internal partial class DiscordWebSocket
{
public event EventHandler Connected;
private void RaiseConnected()
{
if (Connected != null)
Connected(this, EventArgs.Empty);
}
public event EventHandler Disconnected;
private void RaiseDisconnected()
{
if (Disconnected != null)
Disconnected(this, EventArgs.Empty);
}
public event EventHandler<MessageEventArgs> GotEvent;
public sealed class MessageEventArgs : EventArgs
{
public readonly string Type;
public readonly JToken Event;
internal MessageEventArgs(string type, JToken data)
{
Type = type;
Event = data;
}
}
private void RaiseGotEvent(string type, JToken payload)
{
if (GotEvent != null)
GotEvent(this, new MessageEventArgs(type, payload));
}
public event EventHandler<DiscordClient.LogMessageEventArgs> OnDebugMessage;
private void RaiseOnDebugMessage(string message)
{
if (OnDebugMessage != null)
OnDebugMessage(this, new DiscordClient.LogMessageEventArgs(message));
}
}
}

View File

@@ -0,0 +1,204 @@
using Discord.API.Models;
using Discord.Helpers;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Concurrent;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Discord
{
internal sealed partial class DiscordWebSocket : IDisposable
{
private const int ReceiveChunkSize = 4096;
private const int SendChunkSize = 4096;
private const int ReadyTimeout = 5000; //Max time in milliseconds between connecting to Discord and receiving a READY event
private volatile ClientWebSocket _webSocket;
private volatile CancellationTokenSource _cancelToken;
private volatile Task _tasks;
private ConcurrentQueue<byte[]> _sendQueue;
private int _heartbeatInterval;
private DateTime _lastHeartbeat;
private AutoResetEvent _connectWaitOnLogin, _connectWaitOnLogin2;
public async Task ConnectAsync(string url, bool autoLogin, HttpOptions options)
{
await DisconnectAsync();
_connectWaitOnLogin = new AutoResetEvent(false);
_connectWaitOnLogin2 = new AutoResetEvent(false);
_sendQueue = new ConcurrentQueue<byte[]>();
_webSocket = new ClientWebSocket();
_webSocket.Options.KeepAliveInterval = TimeSpan.Zero;
_cancelToken = new CancellationTokenSource();
var cancelToken = _cancelToken.Token;
await _webSocket.ConnectAsync(new Uri(url), cancelToken);
_tasks = Task.WhenAll(
await Task.Factory.StartNew(ReceiveAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default),
await Task.Factory.StartNew(SendAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default)
).ContinueWith(x =>
{
//Do not clean up until both tasks have ended
_heartbeatInterval = 0;
_lastHeartbeat = DateTime.MinValue;
_webSocket.Dispose();
_webSocket = null;
_cancelToken.Dispose();
_cancelToken = null;
_tasks = null;
RaiseDisconnected();
});
if (autoLogin)
Login(options);
}
public void Login(HttpOptions options)
{
WebSocketCommands.Login msg = new WebSocketCommands.Login();
msg.Payload.Token = options.Token;
msg.Payload.Properties["$os"] = "";
msg.Payload.Properties["$browser"] = "";
msg.Payload.Properties["$device"] = "Discord.Net";
msg.Payload.Properties["$referrer"] = "";
msg.Payload.Properties["$referring_domain"] = "";
SendMessage(msg, _cancelToken.Token);
if (!_connectWaitOnLogin.WaitOne(ReadyTimeout)) //Pre-Event
throw new Exception("No reply from Discord server");
_connectWaitOnLogin2.WaitOne(); //Post-Event
RaiseConnected();
}
public async Task DisconnectAsync()
{
if (_tasks != null)
{
_cancelToken.Cancel();
await _tasks;
}
}
private async Task ReceiveAsync()
{
var cancelToken = _cancelToken.Token;
var buffer = new byte[ReceiveChunkSize];
var builder = new StringBuilder();
try
{
while (_webSocket.State == WebSocketState.Open && !cancelToken.IsCancellationRequested)
{
WebSocketReceiveResult result;
do
{
result = await _webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), _cancelToken.Token);
if (result.MessageType == WebSocketMessageType.Close)
{
await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
return;
}
else
builder.Append(Encoding.UTF8.GetString(buffer, 0, result.Count));
}
while (!result.EndOfMessage);
var msg = JsonConvert.DeserializeObject<WebSocketMessage>(builder.ToString());
switch (msg.Operation)
{
case 0:
if (msg.Type == "READY")
{
var payload = (msg.Payload as JToken).ToObject<WebSocketEvents.Ready>();
_heartbeatInterval = payload.HeartbeatInterval;
SendMessage(new WebSocketCommands.UpdateStatus(), cancelToken);
SendMessage(new WebSocketCommands.KeepAlive(), cancelToken);
_connectWaitOnLogin.Set(); //Pre-Event
}
RaiseGotEvent(msg.Type, msg.Payload as JToken);
if (msg.Type == "READY")
_connectWaitOnLogin2.Set(); //Post-Event
break;
default:
RaiseOnDebugMessage("Unknown WebSocket operation ID: " + msg.Operation);
break;
}
builder.Clear();
}
}
catch { }
finally { _cancelToken.Cancel(); }
}
private async Task SendAsync()
{
var cancelToken = _cancelToken.Token;
try
{
byte[] bytes;
while (_webSocket.State == WebSocketState.Open && !cancelToken.IsCancellationRequested)
{
if (_heartbeatInterval > 0)
{
DateTime now = DateTime.UtcNow;
if ((now - _lastHeartbeat).TotalMilliseconds > _heartbeatInterval)
{
SendMessage(new WebSocketCommands.KeepAlive(), cancelToken);
_lastHeartbeat = now;
}
}
while (_sendQueue.TryDequeue(out bytes))
{
var frameCount = (int)Math.Ceiling((double)bytes.Length / SendChunkSize);
int offset = 0;
for (var i = 0; i < frameCount; i++, offset += SendChunkSize)
{
bool isLast = i == (frameCount - 1);
int count;
if (isLast)
count = bytes.Length - (i * SendChunkSize);
else
count = SendChunkSize;
await _webSocket.SendAsync(new ArraySegment<byte>(bytes, offset, count), WebSocketMessageType.Text, isLast, cancelToken);
}
}
await Task.Delay(100);
}
}
catch { }
finally { _cancelToken.Cancel(); }
}
private void SendMessage(object frame, CancellationToken token)
{
var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(frame));
_sendQueue.Enqueue(bytes);
}
#region IDisposable Support
private bool _isDisposed = false;
public void Dispose()
{
if (!_isDisposed)
{
DisconnectAsync().Wait();
_isDisposed = true;
}
}
#endregion
}
}

View File

@@ -0,0 +1,90 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
namespace Discord.Helpers
{
public class AsyncCache<TValue, TModel> : IEnumerable<TValue>
where TValue : class
where TModel : class
{
protected readonly ConcurrentDictionary<string, TValue> _dictionary;
private readonly Func<string, string, TValue> _onCreate;
private readonly Action<TValue, TModel> _onUpdate;
private readonly Action<TValue> _onRemove;
public AsyncCache(Func<string, string, TValue> onCreate, Action<TValue, TModel> onUpdate, Action<TValue> onRemove)
{
_dictionary = new ConcurrentDictionary<string, TValue>();
_onCreate = onCreate;
_onUpdate = onUpdate;
_onRemove = onRemove;
}
public TValue this[string key]
{
get
{
if (key == null)
return null;
TValue value = null;
_dictionary.TryGetValue(key, out value);
return value;
}
}
public TValue Update(string key, TModel model)
{
return Update(key, null, model);
}
public TValue Update(string key, string parentKey, TModel model)
{
if (key == null)
return null;
while (true)
{
bool isNew;
TValue value;
isNew = !_dictionary.TryGetValue(key, out value);
if (isNew)
value = _onCreate(key, parentKey);
_onUpdate(value, model);
if (isNew)
{
//If this fails, repeat as an update instead of an add
if (_dictionary.TryAdd(key, value))
return value;
}
else
{
_dictionary[key] = value;
return value;
}
}
}
public TValue Remove(string key)
{
TValue value = null;
if (_dictionary.TryRemove(key, out value))
return value;
else
return null;
}
public void Clear()
{
_dictionary.Clear();
}
public IEnumerator<TValue> GetEnumerator()
{
return _dictionary.Values.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return _dictionary.Values.GetEnumerator();
}
}
}

View File

@@ -0,0 +1,205 @@
using Newtonsoft.Json;
using System;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Text;
using System.Threading.Tasks;
namespace Discord.Helpers
{
internal class HttpOptions
{
public readonly string UserAgent;
public string Token;
public HttpOptions(string userAgent)
{
UserAgent = userAgent;
}
}
internal static class Http
{
#if DEBUG
private const bool _isDebug = true;
#else
private const bool _isDebug = false;
#endif
internal static Task<ResponseT> Get<ResponseT>(string path, HttpOptions options)
where ResponseT : class
=> Send<ResponseT>("GET", path, null, options);
internal static Task<string> Get(string path, HttpOptions options)
=> Send("GET", path, null, options);
internal static Task<ResponseT> Post<ResponseT>(string path, object data, HttpOptions options)
where ResponseT : class
=> Send<ResponseT>("POST", path, data, options);
internal static Task<string> Post(string path, object data, HttpOptions options)
=> Send("POST", path, data, options);
internal static Task<ResponseT> Post<ResponseT>(string path, HttpOptions options)
where ResponseT : class
=> Send<ResponseT>("POST", path, null, options);
internal static Task<string> Post(string path, HttpOptions options)
=> Send("POST", path, null, options);
internal static Task<ResponseT> Put<ResponseT>(string path, object data, HttpOptions options)
where ResponseT : class
=> Send<ResponseT>("PUT", path, data, options);
internal static Task<string> Put(string path, object data, HttpOptions options)
=> Send("PUT", path, data, options);
internal static Task<ResponseT> Put<ResponseT>(string path, HttpOptions options)
where ResponseT : class
=> Send<ResponseT>("PUT", path, null, options);
internal static Task<string> Put(string path, HttpOptions options)
=> Send("PUT", path, null, options);
internal static Task<ResponseT> Patch<ResponseT>(string path, object data, HttpOptions options)
where ResponseT : class
=> Send<ResponseT>("PATCH", path, data, options);
internal static Task<string> Patch(string path, object data, HttpOptions options)
=> Send("PATCH", path, data, options);
internal static Task<ResponseT> Patch<ResponseT>(string path, HttpOptions options)
where ResponseT : class
=> Send<ResponseT>("PATCH", path, null, options);
internal static Task<string> Patch(string path, HttpOptions options)
=> Send("PATCH", path, null, options);
internal static Task<ResponseT> Delete<ResponseT>(string path, object data, HttpOptions options)
where ResponseT : class
=> Send<ResponseT>("DELETE", path, data, options);
internal static Task<string> Delete(string path, object data, HttpOptions options)
=> Send("DELETE", path, data, options);
internal static Task<ResponseT> Delete<ResponseT>(string path, HttpOptions options)
where ResponseT : class
=> Send<ResponseT>("DELETE", path, null, options);
internal static Task<string> Delete(string path, HttpOptions options)
=> Send("DELETE", path, null, options);
internal static async Task<ResponseT> Send<ResponseT>(string method, string path, object data, HttpOptions options)
where ResponseT : class
{
string requestJson = data != null ? JsonConvert.SerializeObject(data) : null;
string responseJson = await SendRequest(method, path, requestJson, options, true);
var response = JsonConvert.DeserializeObject<ResponseT>(responseJson);
#if DEBUG
CheckResponse(responseJson, response);
#endif
return response;
}
internal static async Task<string> Send(string method, string path, object data, HttpOptions options)
{
string requestJson = data != null ? JsonConvert.SerializeObject(data) : null;
string responseJson = await SendRequest(method, path, requestJson, options, _isDebug);
#if DEBUG
CheckEmptyResponse(responseJson);
#endif
return responseJson;
}
private static async Task<string> SendRequest(string method, string path, string data, HttpOptions options, bool hasResponse)
{
//Create Request
HttpWebRequest request = WebRequest.CreateHttp(path);
request.Accept = "*/*";
request.Method = method;
request.Proxy = null;
request.Headers[HttpRequestHeader.AcceptLanguage] = "en-US;q=0.8";
request.Headers[HttpRequestHeader.AcceptEncoding] = "gzip, deflate";
request.Headers[HttpRequestHeader.UserAgent] = options.UserAgent;
request.Headers[HttpRequestHeader.Authorization] = options.Token;
//request.UserAgent = options.UserAgent;
//Add Payload
if (data != null)
{
byte[] buffer = Encoding.UTF8.GetBytes(data);
using (var payload = await request.GetRequestStreamAsync())
payload.Write(buffer, 0, buffer.Length);
request.ContentType = "application/json";
}
//Get Response
using (var response = (HttpWebResponse)(await request.GetResponseAsync()))
{
if (hasResponse)
{
using (var stream = response.GetResponseStream())
using (var reader = new BinaryReader(stream))
using (var largeBuffer = new MemoryStream())
{
//Read the response in small chunks and add them to a larger buffer.
//ContentLength isn't always provided, so this is safer.
int bytesRead = 0;
byte[] smallBuffer = new byte[4096];
while ((bytesRead = reader.Read(smallBuffer, 0, smallBuffer.Length)) > 0)
largeBuffer.Write(smallBuffer, 0, bytesRead);
//Do we need to decompress?
string encoding = response.Headers[HttpResponseHeader.ContentEncoding];
if (!string.IsNullOrEmpty(encoding))
{
largeBuffer.Position = 0;
using (var decoder = GetDecoder(encoding, largeBuffer))
using (var decodedStream = new MemoryStream())
{
decoder.CopyTo(decodedStream);
#if !DOTNET
return Encoding.UTF8.GetString(decodedStream.GetBuffer(), 0, (int)decodedStream.Length);
#else
ArraySegment<byte> buffer;
if (!decodedStream.TryGetBuffer(out buffer))
throw new InvalidOperationException("Failed to get response buffer.");
return Encoding.UTF8.GetString(buffer.Array, buffer.Offset, (int)decodedStream.Length);
#endif
}
}
else
{
#if !DOTNET
return Encoding.UTF8.GetString(largeBuffer.GetBuffer(), 0, (int)largeBuffer.Length);
#else
ArraySegment<byte> buffer;
if (!largeBuffer.TryGetBuffer(out buffer))
throw new InvalidOperationException("Failed to get response buffer.");
return Encoding.UTF8.GetString(buffer.Array, buffer.Offset, (int)largeBuffer.Length);
#endif
}
}
}
else
return null;
}
}
private static Stream GetDecoder(string contentEncoding, MemoryStream encodedStream)
{
switch (contentEncoding)
{
case "gzip":
return new GZipStream(encodedStream, CompressionMode.Decompress, true);
case "deflate":
return new DeflateStream(encodedStream, CompressionMode.Decompress, true);
default:
throw new ArgumentOutOfRangeException("Unknown encoding: " + contentEncoding);
}
}
#if DEBUG
private static void CheckResponse<T>(string json, T obj)
{
/*JToken token = JToken.Parse(json);
JToken token2 = JToken.FromObject(obj);
if (!JToken.DeepEquals(token, token2))
throw new Exception("API check failed: Objects do not match.");*/
}
private static void CheckEmptyResponse(string json)
{
if (!string.IsNullOrEmpty(json))
throw new Exception("API check failed: Response is not empty.");
}
#endif
}
}

34
src/Discord.Net/Invite.cs Normal file
View File

@@ -0,0 +1,34 @@
using Newtonsoft.Json;
namespace Discord
{
public sealed class Invite
{
private readonly DiscordClient _client;
public int MaxAge, Uses, MaxUses;
public bool IsRevoked, IsTemporary;
public readonly string Code, XkcdPass;
public string Url => API.Endpoints.InviteUrl(XkcdPass ?? Code);
public string InviterId { get; internal set; }
[JsonIgnore]
public User Inviter => _client.GetUser(InviterId);
public string ServerId { get; internal set; }
[JsonIgnore]
public Server Server => _client.GetServer(ServerId);
public string ChannelId { get; internal set; }
[JsonIgnore]
public Channel Channel => _client.GetChannel(ChannelId);
internal Invite(string code, string xkcdPass, DiscordClient client)
{
Code = code;
XkcdPass = xkcdPass;
_client = client;
}
}
}

View File

@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Discord
{
public class Membership
{
private readonly DiscordClient _client;
public DateTime JoinedAt;
public bool IsMuted { get; internal set; }
public bool IsDeafened { get; internal set; }
public string ServerId { get; }
public Server Server => _client.GetServer(ServerId);
public string UserId { get; }
public User User => _client.GetUser(UserId);
public string[] RoleIds { get; internal set; }
public IEnumerable<Role> Roles => RoleIds.Select(x => _client.GetRole(x));
public Membership(string serverId, string userId, DateTime joinedAt, DiscordClient client)
{
ServerId = serverId;
UserId = userId;
_client = client;
JoinedAt = joinedAt;
}
}
}

View File

@@ -0,0 +1,47 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Discord
{
public sealed class Message
{
private readonly DiscordClient _client;
public string Id { get; }
public bool IsMentioningEveryone { get; internal set; }
public bool IsTTS { get; internal set; }
public string Text { get; internal set; }
public DateTime Timestamp { get; internal set; }
public string[] MentionIds { get; internal set; }
[JsonIgnore]
public IEnumerable<User> Mentions => MentionIds.Select(x => _client.GetUser(x)).Where(x => x != null);
public string ChannelId { get; }
[JsonIgnore]
public Channel Channel => _client.GetChannel(ChannelId);
public string UserId { get; internal set; }
[JsonIgnore]
public User User => _client.GetUser(UserId);
//Not Implemented
public object[] Attachments { get; internal set; }
public object[] Embeds { get; internal set; }
internal Message(string id, string channelId, DiscordClient client)
{
Id = id;
ChannelId = channelId;
_client = client;
}
public override string ToString()
{
return User.ToString() + ": " + Text;
}
}
}

View File

@@ -0,0 +1,12 @@
namespace Discord
{
public static class Regions
{
public const string US_West = "us-west";
public const string US_East = "us-east";
public const string Singapore = "singapore";
public const string London = "london";
public const string Sydney = "sydney";
public const string Amsterdam = "amsterdam";
}
}

63
src/Discord.Net/Role.cs Normal file
View File

@@ -0,0 +1,63 @@
using Newtonsoft.Json;
namespace Discord
{
public sealed class Role
{
public sealed class PackedPermissions
{
private uint _rawValue;
public uint RawValue { get { return _rawValue; } set { _rawValue = value; } }
public PackedPermissions() { }
public bool General_CreateInstantInvite => ((_rawValue >> 0) & 0x1) == 1;
public bool General_BanMembers => ((_rawValue >> 1) & 0x1) == 1;
public bool General_KickMembers => ((_rawValue >> 2) & 0x1) == 1;
public bool General_ManageRoles => ((_rawValue >> 3) & 0x1) == 1;
public bool General_ManageChannels => ((_rawValue >> 4) & 0x1) == 1;
public bool General_ManageServer => ((_rawValue >> 5) & 0x1) == 1;
//4 Unused
public bool Text_ReadMessages => ((_rawValue >> 10) & 0x1) == 1;
public bool Text_SendMessages => ((_rawValue >> 11) & 0x1) == 1;
public bool Text_SendTTSMessages => ((_rawValue >> 12) & 0x1) == 1;
public bool Text_ManageMessages => ((_rawValue >> 13) & 0x1) == 1;
public bool Text_EmbedLinks => ((_rawValue >> 14) & 0x1) == 1;
public bool Text_AttachFiles => ((_rawValue >> 15) & 0x1) == 1;
public bool Text_ReadMessageHistory => ((_rawValue >> 16) & 0x1) == 1;
public bool Text_MentionEveryone => ((_rawValue >> 17) & 0x1) == 1;
//2 Unused
public bool Voice_Connect => ((_rawValue >> 20) & 0x1) == 1;
public bool Voice_Speak => ((_rawValue >> 21) & 0x1) == 1;
public bool Voice_MuteMembers => ((_rawValue >> 22) & 0x1) == 1;
public bool Voice_DeafenMembers => ((_rawValue >> 23) & 0x1) == 1;
public bool Voice_MoveMembers => ((_rawValue >> 24) & 0x1) == 1;
public bool Voice_UseVoiceActivation => ((_rawValue >> 25) & 0x1) == 1;
//6 Unused
}
private readonly DiscordClient _client;
public string Id { get; }
public string Name { get; internal set; }
public PackedPermissions Permissions { get; }
public string ServerId { get; }
[JsonIgnore]
public Server Server => _client.GetServer(ServerId);
internal Role(string id, string serverId, DiscordClient client)
{
Permissions = new PackedPermissions();
Id = id;
ServerId = serverId;
_client = client;
}
public override string ToString()
{
return Name;
}
}
}

82
src/Discord.Net/Server.cs Normal file
View File

@@ -0,0 +1,82 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
namespace Discord
{
public sealed class Server
{
private readonly DiscordClient _client;
public string Id { get; }
public string Name { get; internal set; }
public string AFKChannelId { get; internal set; }
public int AFKTimeout { get; internal set; }
public DateTime JoinedAt { get; internal set; }
public string Region { get; internal set; }
public string OwnerId { get; internal set; }
public User Owner => _client.GetUser(OwnerId);
public bool IsOwner => _client.UserId == OwnerId;
public string DefaultChannelId => Id;
public Channel DefaultChannel =>_client.GetChannel(DefaultChannelId);
internal ConcurrentDictionary<string, Membership> _members;
public IEnumerable<Membership> Members => _members.Values;
internal ConcurrentDictionary<string, bool> _bans;
public IEnumerable<User> Bans => _bans.Keys.Select(x => _client.GetUser(x));
public IEnumerable<Channel> Channels => _client.Channels.Where(x => x.ServerId == Id);
public IEnumerable<Role> Roles => _client.Roles.Where(x => x.ServerId == Id);
//Not Implemented
public object Presence { get; internal set; }
public object[] VoiceStates { get; internal set; }
internal Server(string id, DiscordClient client)
{
Id = id;
_client = client;
_members = new ConcurrentDictionary<string, Membership>();
_bans = new ConcurrentDictionary<string, bool>();
}
public override string ToString()
{
return Name;
}
internal void AddMember(Membership membership)
{
_members[membership.UserId] = membership;
}
internal Membership RemoveMember(string userId)
{
Membership result = null;
_members.TryRemove(userId, out result);
return result;
}
public Membership GetMembership(User user)
=> GetMembership(user.Id);
public Membership GetMembership(string userId)
{
Membership result = null;
_members.TryGetValue(userId, out result);
return result;
}
internal void AddBan(string banId)
{
_bans.TryAdd(banId, true);
}
internal bool RemoveBan(string banId)
{
bool ignored;
return _bans.TryRemove(banId, out ignored);
}
}
}

44
src/Discord.Net/User.cs Normal file
View File

@@ -0,0 +1,44 @@
using Discord.API;
using Newtonsoft.Json;
using System;
namespace Discord
{
public sealed class User
{
private readonly DiscordClient _client;
public string Id { get; }
public string Name { get; internal set; }
public string AvatarId { get; internal set; }
public string AvatarUrl => Endpoints.UserAvatar(Id, AvatarId);
public string Discriminator { get; internal set; }
[JsonIgnore]
public string Email { get; internal set; }
[JsonIgnore]
public bool IsVerified { get; internal set; } = true;
public string GameId { get; internal set; }
public string Status { get; internal set; }
public DateTime LastActivity { get; private set; }
internal User(string id, DiscordClient client)
{
Id = id;
_client = client;
LastActivity = DateTime.UtcNow;
}
internal void UpdateActivity(DateTime activity)
{
if (activity > LastActivity)
LastActivity = activity;
}
public override string ToString()
{
return Name;
}
}
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Newtonsoft.Json" version="7.0.1" targetFramework="net45" />
</packages>

View File

@@ -0,0 +1,28 @@
{
"version": "0.3.3-*",
"description": "An unofficial .Net API wrapper for the Discord client.",
"authors": [ "RogueException" ],
"tags": [ "discord", "discordapp" ],
"projectUrl": "https://github.com/RogueException/Discord.Net",
"licenseUrl": "http://opensource.org/licenses/MIT",
"repository": {
"type": "git",
"url": "git://github.com/RogueException/Discord.Net"
},
"compilationOptions": {
"warningsAsErrors": true
},
"frameworks": {
"dotnet": {
"dependencies": {
"System.Collections.Concurrent": "4.0.10",
"System.IO.Compression": "4.0.0-beta-23109",
"System.Net.Requests": "4.0.10-beta-23109",
"System.Net.WebSockets.Client": "4.0.0-beta-23109",
"System.Runtime": "4.0.20",
"Microsoft.CSharp": "4.0.0",
"Newtonsoft.Json": "7.0.1"
}
}
}
}