@@ -0,0 +1,28 @@ | |||
| |||
Microsoft Visual Studio Solution File, Format Version 12.00 | |||
# Visual Studio 14 | |||
VisualStudioVersion = 14.0.23107.0 | |||
MinimumVisualStudioVersion = 10.0.40219.1 | |||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net", "Discord.Net\Discord.Net.csproj", "{8D23F61B-723C-4966-859D-1119B28BCF19}" | |||
EndProject | |||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1DDC89B5-2A88-45E5-A743-7A43E6B5C4B3}" | |||
ProjectSection(SolutionItems) = preProject | |||
.gitignore = .gitignore | |||
LICENSE = LICENSE | |||
EndProjectSection | |||
EndProject | |||
Global | |||
GlobalSection(SolutionConfigurationPlatforms) = preSolution | |||
Debug|Any CPU = Debug|Any CPU | |||
Release|Any CPU = Release|Any CPU | |||
EndGlobalSection | |||
GlobalSection(ProjectConfigurationPlatforms) = postSolution | |||
{8D23F61B-723C-4966-859D-1119B28BCF19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | |||
{8D23F61B-723C-4966-859D-1119B28BCF19}.Debug|Any CPU.Build.0 = Debug|Any CPU | |||
{8D23F61B-723C-4966-859D-1119B28BCF19}.Release|Any CPU.ActiveCfg = Release|Any CPU | |||
{8D23F61B-723C-4966-859D-1119B28BCF19}.Release|Any CPU.Build.0 = Release|Any CPU | |||
EndGlobalSection | |||
GlobalSection(SolutionProperties) = preSolution | |||
HideSolutionNode = FALSE | |||
EndGlobalSection | |||
EndGlobal |
@@ -0,0 +1,61 @@ | |||
using Discord.API.Models; | |||
using Discord.Helpers; | |||
using System.Threading.Tasks; | |||
namespace Discord.API | |||
{ | |||
internal static class DiscordAPI | |||
{ | |||
public static async Task<AuthRegisterResponse> LoginAnonymous(string username, HttpOptions options) | |||
{ | |||
var fingerprintResponse = await Http.Post<AuthFingerprintResponse>(Endpoints.AuthFingerprint, options); | |||
var registerRequest = new AuthRegisterRequest { Fingerprint = fingerprintResponse.Fingerprint, Username = username }; | |||
var registerResponse = await Http.Post<AuthRegisterResponse>(Endpoints.AuthRegister, registerRequest, options); | |||
return registerResponse; | |||
} | |||
public static async Task<AuthLoginResponse> Login(string email, string password, HttpOptions options) | |||
{ | |||
var request = new AuthLoginRequest { Email = email, Password = password }; | |||
var response = await Http.Post<AuthLoginResponse>(Endpoints.AuthLogin, request, options); | |||
options.Token = response.Token; | |||
return response; | |||
} | |||
public static Task Logout(HttpOptions options) | |||
{ | |||
return Http.Post(Endpoints.AuthLogout, options); | |||
} | |||
public static Task CreateServer(string name, Region region, HttpOptions options) | |||
{ | |||
var request = new CreateServerRequest { Name = name, Region = RegionConverter.Convert(region) }; | |||
return Http.Post(Endpoints.Servers, request, options); | |||
} | |||
public static Task DeleteServer(string id, HttpOptions options) | |||
{ | |||
return Http.Delete(Endpoints.Server(id), options); | |||
} | |||
public static Task<GetInviteResponse> GetInvite(string id, HttpOptions options) | |||
{ | |||
return Http.Get<GetInviteResponse>(Endpoints.Invite(id), options); | |||
} | |||
public static Task AcceptInvite(string id, HttpOptions options) | |||
{ | |||
return Http.Post(Endpoints.Invite(id), options); | |||
} | |||
public static Task DeleteInvite(string id, HttpOptions options) | |||
{ | |||
return Http.Delete(Endpoints.Invite(id), options); | |||
} | |||
public static Task Typing(string channelId, HttpOptions options) | |||
{ | |||
return Http.Post(Endpoints.ChannelTyping(channelId), options); | |||
} | |||
public static Task SendMessage(string channelId, string message, string[] mentions, HttpOptions options) | |||
{ | |||
var request = new SendMessageRequest { Content = message, Mentions = mentions }; | |||
return Http.Post(Endpoints.ChannelMessages(channelId), request, options); | |||
} | |||
} | |||
} |
@@ -0,0 +1,30 @@ | |||
namespace Discord.API | |||
{ | |||
internal static class Endpoints | |||
{ | |||
public static readonly string BaseUrl = "discordapp.com/"; | |||
public static readonly string BaseHttps = "https://" + BaseUrl; | |||
public static readonly string BaseWss = "wss://" + BaseUrl; | |||
public static readonly string Auth = $"{BaseHttps}/api/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"; | |||
public static readonly string Servers = $"{BaseHttps}/api/guilds"; | |||
public static string Server(string id) { return $"{Servers}/{id}"; } | |||
public static string ServerMessages(string id) { return $"{Servers}/{id}/messages?limit=50"; } | |||
public static readonly string Invites = $"{BaseHttps}/api/invite"; | |||
public static string Invite(string id) { return $"{Invites}/{id}"; } | |||
public static readonly string Channels = $"{BaseHttps}/api/channels"; | |||
public static string Channel(string id) { return $"{Channels}/{id}"; } | |||
public static string ChannelTyping(string id) { return $"{Channels}/{id}/typing"; } | |||
public static string ChannelMessages(string id) { return $"{Channels}/{id}/messages"; } | |||
public static readonly string WebSocket_Hub = BaseWss + "hub"; | |||
} | |||
} |
@@ -0,0 +1,66 @@ | |||
//Ignore unused/unassigned variable warnings | |||
#pragma warning disable CS0649 | |||
#pragma warning disable CS0169 | |||
using Newtonsoft.Json; | |||
namespace Discord.API.Models | |||
{ | |||
public class AuthFingerprintResponse | |||
{ | |||
[JsonProperty(PropertyName = "fingerprint")] | |||
public string Fingerprint; | |||
} | |||
public class AuthRegisterRequest | |||
{ | |||
[JsonProperty(PropertyName = "fingerprint")] | |||
public string Fingerprint; | |||
[JsonProperty(PropertyName = "username")] | |||
public string Username; | |||
} | |||
public class AuthRegisterResponse : AuthLoginResponse { } | |||
public class AuthLoginRequest | |||
{ | |||
[JsonProperty(PropertyName = "email")] | |||
public string Email; | |||
[JsonProperty(PropertyName = "password")] | |||
public string Password; | |||
} | |||
public class AuthLoginResponse | |||
{ | |||
[JsonProperty(PropertyName = "token")] | |||
public string Token; | |||
} | |||
public class CreateServerRequest | |||
{ | |||
[JsonProperty(PropertyName = "name")] | |||
public string Name; | |||
[JsonProperty(PropertyName = "region")] | |||
public string Region; | |||
} | |||
public class GetInviteResponse | |||
{ | |||
[JsonProperty(PropertyName = "inviter")] | |||
public UserInfo Inviter; | |||
[JsonProperty(PropertyName = "guild")] | |||
public ServerInfo Server; | |||
[JsonProperty(PropertyName = "channel")] | |||
public ChannelInfo Channel; | |||
[JsonProperty(PropertyName = "code")] | |||
public string Code; | |||
[JsonProperty(PropertyName = "xkcdpass")] | |||
public string XkcdPass; | |||
} | |||
public class SendMessageRequest | |||
{ | |||
[JsonProperty(PropertyName = "content")] | |||
public string Content; | |||
[JsonProperty(PropertyName = "mentions")] | |||
public string[] Mentions; | |||
} | |||
} |
@@ -0,0 +1,131 @@ | |||
//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; } | |||
} | |||
} | |||
public class UserInfo | |||
{ | |||
[JsonProperty(PropertyName = "username")] | |||
public string Username; | |||
[JsonProperty(PropertyName = "id")] | |||
public string Id; | |||
[JsonProperty(PropertyName = "discriminator")] | |||
public string Discriminator; | |||
[JsonProperty(PropertyName = "avatar")] | |||
public string Avatar; | |||
} | |||
public class SelfUserInfo : UserInfo | |||
{ | |||
[JsonProperty(PropertyName = "email")] | |||
public string Email; | |||
[JsonProperty(PropertyName = "verified")] | |||
public bool IsVerified; | |||
} | |||
public class PresenceUserInfo : UserInfo | |||
{ | |||
[JsonProperty(PropertyName = "game_id")] | |||
public string GameId; | |||
[JsonProperty(PropertyName = "status")] | |||
public string Status; | |||
} | |||
public class MembershipInfo | |||
{ | |||
[JsonProperty(PropertyName = "roles")] | |||
public object[] Roles; | |||
[JsonProperty(PropertyName = "mute")] | |||
public bool IsMuted; | |||
[JsonProperty(PropertyName = "deaf")] | |||
public bool IsDeaf; | |||
[JsonProperty(PropertyName = "joined_at")] | |||
public DateTime JoinedAt; | |||
[JsonProperty(PropertyName = "user")] | |||
public UserInfo User; | |||
} | |||
public class ChannelInfo | |||
{ | |||
[JsonProperty(PropertyName = "id")] | |||
public string Id; | |||
[JsonProperty(PropertyName = "name")] | |||
public string Name; | |||
[JsonProperty(PropertyName = "last_message_id")] | |||
public string LastMessageId; | |||
[JsonProperty(PropertyName = "is_private")] | |||
public bool IsPrivate; | |||
[JsonProperty(PropertyName = "type")] | |||
public string Type; | |||
[JsonProperty(PropertyName = "permission_overwrites")] | |||
public object[] PermissionOverwrites; | |||
[JsonProperty(PropertyName = "recipient")] | |||
public UserInfo Recipient; | |||
} | |||
public class ServerInfo | |||
{ | |||
[JsonProperty(PropertyName = "id")] | |||
public string Id; | |||
[JsonProperty(PropertyName = "name")] | |||
public string Name; | |||
} | |||
public class ExtendedServerInfo : ServerInfo | |||
{ | |||
[JsonProperty(PropertyName = "afk_channel_id")] | |||
public string AFKChannelId; | |||
[JsonProperty(PropertyName = "afk_timeout")] | |||
public int AFKTimeout; | |||
[JsonProperty(PropertyName = "channels")] | |||
public ChannelInfo[] Channels; | |||
[JsonProperty(PropertyName = "joined_at")] | |||
public DateTime JoinedAt; | |||
[JsonProperty(PropertyName = "members")] | |||
public MembershipInfo[] Members; | |||
[JsonProperty(PropertyName = "owner_id")] | |||
public string OwnerId; | |||
[JsonProperty(PropertyName = "presence")] | |||
public object[] Presence; | |||
[JsonProperty(PropertyName = "region")] | |||
public string Region; | |||
[JsonProperty(PropertyName = "roles")] | |||
public object[] Roles; | |||
[JsonProperty(PropertyName = "voice_states")] | |||
public object[] VoiceStates; | |||
} | |||
internal class MessageReference | |||
{ | |||
[JsonProperty(PropertyName = "message_id")] | |||
public string MessageId; | |||
[JsonProperty(PropertyName = "channel_id")] | |||
public string ChannelId; | |||
} | |||
} |
@@ -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 | |||
{ | |||
internal 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) { } | |||
} | |||
internal 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>(); | |||
} | |||
} | |||
internal 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; | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,120 @@ | |||
//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 | |||
{ | |||
internal 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; | |||
} | |||
internal sealed class GuildCreate : ExtendedServerInfo { } | |||
internal sealed class GuildDelete : ExtendedServerInfo { } | |||
internal sealed class ChannelCreate : ChannelInfo { } | |||
internal sealed class ChannelDelete : ChannelInfo { } | |||
internal sealed class GuildMemberAdd | |||
{ | |||
[JsonProperty(PropertyName = "user")] | |||
public UserInfo User; | |||
[JsonProperty(PropertyName = "roles")] | |||
public object[] Roles; | |||
[JsonProperty(PropertyName = "joined_at")] | |||
public DateTime JoinedAt; | |||
[JsonProperty(PropertyName = "guild_id")] | |||
public string GuildId; | |||
} | |||
internal sealed class GuildMemberRemove | |||
{ | |||
[JsonProperty(PropertyName = "user")] | |||
public UserInfo User; | |||
[JsonProperty(PropertyName = "guild_id")] | |||
public string GuildId; | |||
} | |||
internal sealed class UserUpdate : SelfUserInfo { } | |||
internal sealed class PresenceUpdate : PresenceUserInfo { } | |||
internal 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; | |||
} | |||
internal sealed class MessageCreate | |||
{ | |||
[JsonProperty(PropertyName = "id")] | |||
public string Id; | |||
[JsonProperty(PropertyName = "channel_id")] | |||
public string ChannelId; | |||
[JsonProperty(PropertyName = "tts")] | |||
public bool IsTextToSpeech; | |||
[JsonProperty(PropertyName = "mention_everyone")] | |||
public bool IsMentioningEveryone; | |||
[JsonProperty(PropertyName = "timestamp")] | |||
public DateTime Timestamp; | |||
[JsonProperty(PropertyName = "mentions")] | |||
public UserInfo[] Mentions; | |||
[JsonProperty(PropertyName = "embeds")] | |||
public object[] Embeds; | |||
[JsonProperty(PropertyName = "attachments")] | |||
public object[] Attachments; | |||
[JsonProperty(PropertyName = "content")] | |||
public string Content; | |||
[JsonProperty(PropertyName = "author")] | |||
public UserInfo Author; | |||
} | |||
internal sealed class MessageUpdate | |||
{ | |||
[JsonProperty(PropertyName = "id")] | |||
public string Id; | |||
[JsonProperty(PropertyName = "channel_id")] | |||
public string ChannelId; | |||
[JsonProperty(PropertyName = "embeds")] | |||
public object[] Embeds; | |||
} | |||
internal sealed class MessageDelete : MessageReference { } | |||
internal sealed class MessageAck : MessageReference { } | |||
internal sealed class TypingStart | |||
{ | |||
[JsonProperty(PropertyName = "user_id")] | |||
public string UserId; | |||
[JsonProperty(PropertyName = "channel_id")] | |||
public string ChannelId; | |||
[JsonProperty(PropertyName = "timestamp")] | |||
public int Timestamp; | |||
} | |||
} | |||
} |
@@ -0,0 +1,78 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<Project ToolsVersion="14.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>{8D23F61B-723C-4966-859D-1119B28BCF19}</ProjectGuid> | |||
<OutputType>Library</OutputType> | |||
<AppDesignerFolder>Properties</AppDesignerFolder> | |||
<RootNamespace>Discord</RootNamespace> | |||
<AssemblyName>Discord.Net</AssemblyName> | |||
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion> | |||
<FileAlignment>512</FileAlignment> | |||
<TargetFrameworkProfile /> | |||
</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> | |||
</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> | |||
</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" /> | |||
<Reference Include="System.Core" /> | |||
<Reference Include="System.Xml.Linq" /> | |||
<Reference Include="System.Data.DataSetExtensions" /> | |||
<Reference Include="Microsoft.CSharp" /> | |||
<Reference Include="System.Data" /> | |||
<Reference Include="System.Net.Http" /> | |||
<Reference Include="System.Xml" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<Compile Include="API\Models\General.cs" /> | |||
<Compile Include="API\Models\ApiRequests.cs" /> | |||
<Compile Include="API\Endpoints.cs" /> | |||
<Compile Include="API\Models\WebSocketCommands.cs" /> | |||
<Compile Include="Models\ChatMessageReference.cs" /> | |||
<Compile Include="Models\ChatMessage.cs" /> | |||
<Compile Include="Models\Channel.cs" /> | |||
<Compile Include="DiscordWebSocket.Events.cs" /> | |||
<Compile Include="Helpers\Http.cs" /> | |||
<Compile Include="API\DiscordAPI.cs" /> | |||
<Compile Include="API\Models\WebSocketEvents.cs" /> | |||
<Compile Include="DiscordClient.cs" /> | |||
<Compile Include="Region.cs" /> | |||
<Compile Include="DiscordClient.Events.cs" /> | |||
<Compile Include="Properties\AssemblyInfo.cs" /> | |||
<Compile Include="DiscordWebSocket.cs" /> | |||
<Compile Include="Models\Server.cs" /> | |||
<Compile Include="Models\User.cs" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<None Include="packages.config" /> | |||
</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> |
@@ -0,0 +1,145 @@ | |||
using Discord.Models; | |||
using System; | |||
namespace Discord | |||
{ | |||
public partial class DiscordClient | |||
{ | |||
public sealed class ServerEventArgs : EventArgs | |||
{ | |||
public readonly Server Server; | |||
internal ServerEventArgs(Server server) { Server = server; } | |||
} | |||
public sealed class ChannelEventArgs : EventArgs | |||
{ | |||
public readonly Channel Channel; | |||
internal ChannelEventArgs(Channel channel) { Channel = channel; } | |||
} | |||
public sealed class UserEventArgs : EventArgs | |||
{ | |||
public readonly User User; | |||
internal UserEventArgs(User user) { User = user; } | |||
} | |||
public sealed class MessageCreateEventArgs : EventArgs | |||
{ | |||
public readonly ChatMessage Message; | |||
internal MessageCreateEventArgs(ChatMessage msg) { Message = msg; } | |||
} | |||
public sealed class MessageEventArgs : EventArgs | |||
{ | |||
public readonly ChatMessageReference Message; | |||
internal MessageEventArgs(ChatMessageReference msg) { Message = msg; } | |||
} | |||
public sealed class LogMessageEventArgs : EventArgs | |||
{ | |||
public readonly string Message; | |||
internal LogMessageEventArgs(string msg) { Message = msg; } | |||
} | |||
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<LogMessageEventArgs> DebugMessage; | |||
private void RaiseOnDebugMessage(string message) | |||
{ | |||
if (DebugMessage != null) | |||
DebugMessage(this, new LogMessageEventArgs(message)); | |||
} | |||
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 LoggedIn; | |||
private void RaiseLoggedIn() | |||
{ | |||
if (LoggedIn != null) | |||
LoggedIn(this, EventArgs.Empty); | |||
} | |||
public event EventHandler<ServerEventArgs> ServerCreated, ServerDestroyed; | |||
private void RaiseServerCreated(Server server) | |||
{ | |||
if (ServerCreated != null) | |||
ServerCreated(this, new ServerEventArgs(server)); | |||
} | |||
private void RaiseServerDestroyed(Server server) | |||
{ | |||
if (ServerDestroyed != null) | |||
ServerDestroyed(this, new ServerEventArgs(server)); | |||
} | |||
public event EventHandler<ChannelEventArgs> ChannelCreated, ChannelDestroyed; | |||
private void RaiseChannelCreated(Channel channel) | |||
{ | |||
if (ChannelCreated != null) | |||
ChannelCreated(this, new ChannelEventArgs(channel)); | |||
} | |||
private void RaiseChannelDestroyed(Channel channel) | |||
{ | |||
if (ChannelDestroyed != null) | |||
ChannelDestroyed(this, new ChannelEventArgs(channel)); | |||
} | |||
public event EventHandler<MessageCreateEventArgs> MessageCreated; | |||
public event EventHandler<MessageEventArgs> MessageDeleted, MessageUpdated, MessageAcknowledged; | |||
private void RaiseMessageCreated(ChatMessage msg) | |||
{ | |||
if (MessageCreated != null) | |||
MessageCreated(this, new MessageCreateEventArgs(msg)); | |||
} | |||
private void RaiseMessageDeleted(ChatMessageReference msg) | |||
{ | |||
if (MessageDeleted != null) | |||
MessageDeleted(this, new MessageEventArgs(msg)); | |||
} | |||
private void RaiseMessageUpdated(ChatMessageReference msg) | |||
{ | |||
if (MessageUpdated != null) | |||
MessageUpdated(this, new MessageEventArgs(msg)); | |||
} | |||
private void RaiseMessageAcknowledged(ChatMessageReference msg) | |||
{ | |||
if (MessageAcknowledged != null) | |||
MessageAcknowledged(this, new MessageEventArgs(msg)); | |||
} | |||
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)); | |||
} | |||
} | |||
} |
@@ -0,0 +1,400 @@ | |||
using Discord.API; | |||
using Discord.API.Models; | |||
using Discord.Helpers; | |||
using Discord.Models; | |||
using System; | |||
using System.Collections.Concurrent; | |||
using System.Collections.Generic; | |||
using System.Reflection; | |||
using System.Threading.Tasks; | |||
namespace Discord | |||
{ | |||
public partial class DiscordClient | |||
{ | |||
private const int MaxMessageSize = 2000; | |||
private DiscordWebSocket _webSocket; | |||
private HttpOptions _httpOptions; | |||
private bool _isClosing, _isReady; | |||
public string SelfId { get; private set; } | |||
public User Self { get { return GetUser(SelfId); } } | |||
public IEnumerable<User> Users { get { return _users.Values; } } | |||
private ConcurrentDictionary<string, User> _users; | |||
public IEnumerable<Server> Servers { get { return _servers.Values; } } | |||
private ConcurrentDictionary<string, Server> _servers; | |||
public IEnumerable<Channel> Channels { get { return _channels.Values; } } | |||
private ConcurrentDictionary<string, Channel> _channels; | |||
public DiscordClient() | |||
{ | |||
string version = typeof(DiscordClient).GetTypeInfo().Assembly.GetName().Version.ToString(2); | |||
_httpOptions = new HttpOptions { UserAgent = $"Discord.Net/{version} (https://github.com/RogueException/Discord.Net)" }; | |||
_users = new ConcurrentDictionary<string, User>(); | |||
_servers = new ConcurrentDictionary<string, Server>(); | |||
_channels = new ConcurrentDictionary<string, Channel>(); | |||
_webSocket = new DiscordWebSocket(); | |||
_webSocket.Connected += (s,e) => RaiseConnected(); | |||
_webSocket.Disconnected += async (s,e) => | |||
{ | |||
//Reconnect if we didn't cause the disconnect | |||
RaiseDisconnected(); | |||
if (!_isClosing) | |||
{ | |||
await Task.Delay(1000); | |||
await _webSocket.ConnectAsync(Endpoints.WebSocket_Hub, _httpOptions); | |||
} | |||
}; | |||
_webSocket.GotEvent += (s, e) => | |||
{ | |||
switch (e.Type) | |||
{ | |||
//Global | |||
case "READY": //Resync | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.Ready>(); | |||
_servers.Clear(); | |||
_channels.Clear(); | |||
_users.Clear(); | |||
SelfId = data.User.Id; | |||
UpdateUser(data.User); | |||
foreach (var server in data.Guilds) | |||
UpdateServer(server); | |||
foreach (var channel in data.PrivateChannels) | |||
UpdateChannel(channel as ChannelInfo, null); | |||
RaiseLoggedIn(); | |||
} | |||
break; | |||
//Servers | |||
case "GUILD_CREATE": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.GuildCreate>(); | |||
var server = UpdateServer(data); | |||
RaiseServerCreated(server); | |||
} | |||
break; | |||
case "GUILD_DELETE": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.GuildDelete>(); | |||
Server server; | |||
if (_servers.TryRemove(data.Id, out server)) | |||
RaiseServerDestroyed(server); | |||
} | |||
break; | |||
//Channels | |||
case "CHANNEL_CREATE": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.ChannelCreate>(); | |||
var channel = UpdateChannel(data, null); | |||
RaiseChannelCreated(channel); | |||
} | |||
break; | |||
case "CHANNEL_DELETE": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.ChannelDelete>(); | |||
var channel = DeleteChannel(data.Id); | |||
RaiseChannelDestroyed(channel); | |||
} | |||
break; | |||
//Members | |||
case "GUILD_MEMBER_ADD": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.GuildMemberAdd>(); | |||
var user = UpdateUser(data.User); | |||
var server = GetServer(data.GuildId); | |||
server._members[user.Id] = true; | |||
} | |||
break; | |||
case "GUILD_MEMBER_REMOVE": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.GuildMemberRemove>(); | |||
var user = UpdateUser(data.User); | |||
var server = GetServer(data.GuildId); | |||
server._members[user.Id] = true; | |||
} | |||
break; | |||
//Users | |||
case "PRESENCE_UPDATE": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.PresenceUpdate>(); | |||
var user = UpdateUser(data); | |||
RaisePresenceUpdated(user); | |||
} | |||
break; | |||
case "VOICE_STATE_UPDATE": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.VoiceStateUpdate>(); | |||
var user = GetUser(data.UserId); //TODO: Don't ignore this | |||
RaiseVoiceStateUpdated(user); | |||
} | |||
break; | |||
//Messages | |||
case "MESSAGE_CREATE": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.MessageCreate>(); | |||
var msg = UpdateMessage(data); | |||
msg.User.UpdateActivity(data.Timestamp); | |||
RaiseMessageCreated(msg); | |||
} | |||
break; | |||
case "MESSAGE_UPDATE": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.MessageUpdate>(); | |||
var msg = GetMessage(data.Id, data.ChannelId); | |||
RaiseMessageUpdated(msg); | |||
} | |||
break; | |||
case "MESSAGE_DELETE": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.MessageDelete>(); | |||
var msg = GetMessage(data.MessageId, data.ChannelId); | |||
RaiseMessageDeleted(msg); | |||
} | |||
break; | |||
case "MESSAGE_ACK": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.MessageAck>(); | |||
var msg = GetMessage(data.MessageId, data.ChannelId); | |||
RaiseMessageAcknowledged(msg); | |||
} | |||
break; | |||
case "TYPING_START": | |||
{ | |||
var data = e.Event.ToObject<WebSocketEvents.TypingStart>(); | |||
var channel = GetChannel(data.ChannelId); | |||
var user = GetUser(data.UserId); | |||
RaiseUserTyping(user, channel); | |||
} | |||
break; | |||
default: | |||
RaiseOnDebugMessage("Unknown WebSocket message type: " + e.Type); | |||
break; | |||
} | |||
}; | |||
_webSocket.OnDebugMessage += (s, e) => RaiseOnDebugMessage(e.Message); | |||
} | |||
public async Task Connect(string email, string password) | |||
{ | |||
_isClosing = false; | |||
var response = await DiscordAPI.Login(email, password, _httpOptions); | |||
_httpOptions.Token = response.Token; | |||
await _webSocket.ConnectAsync(Endpoints.WebSocket_Hub, _httpOptions); | |||
_isReady = true; | |||
} | |||
public async Task ConnectAnonymous(string username) | |||
{ | |||
_isClosing = false; | |||
var response = await DiscordAPI.LoginAnonymous(username, _httpOptions); | |||
_httpOptions.Token = response.Token; | |||
await _webSocket.ConnectAsync(Endpoints.WebSocket_Hub, _httpOptions); | |||
_isReady = true; | |||
} | |||
public async Task Disconnect() | |||
{ | |||
_isReady = false; | |||
_isClosing = true; | |||
await _webSocket.DisconnectAsync(); | |||
_isClosing = false; | |||
} | |||
public Task CreateServer(string name, Region region) | |||
{ | |||
CheckReady(); | |||
return DiscordAPI.CreateServer(name, region, _httpOptions); | |||
} | |||
public Task DeleteServer(string id) | |||
{ | |||
CheckReady(); | |||
return DiscordAPI.DeleteServer(id, _httpOptions); | |||
} | |||
public Task<GetInviteResponse> GetInvite(string id) | |||
{ | |||
CheckReady(); | |||
return DiscordAPI.GetInvite(id, _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(); | |||
//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); | |||
} | |||
public Task SendMessage(string channelId, string text) | |||
{ | |||
return SendMessage(channelId, text, new string[0]); | |||
} | |||
public async Task SendMessage(string channelId, string text, string[] mentions) | |||
{ | |||
CheckReady(); | |||
if (text.Length <= 2000) | |||
await DiscordAPI.SendMessage(channelId, text, mentions, _httpOptions); | |||
else | |||
{ | |||
int blockCount = (int)Math.Ceiling(text.Length / (double)MaxMessageSize); | |||
for (int i = 0; i < blockCount; i++) | |||
{ | |||
int index = i * MaxMessageSize; | |||
await DiscordAPI.SendMessage(channelId, text.Substring(index, Math.Min(2000, text.Length - index)), mentions, _httpOptions); | |||
await Task.Delay(1000); | |||
} | |||
} | |||
} | |||
public User GetUser(string id) | |||
{ | |||
if (id == null) return null; | |||
User user = null; | |||
_users.TryGetValue(id, out user); | |||
return user; | |||
} | |||
private User UpdateUser(UserInfo model) | |||
{ | |||
var user = GetUser(model.Id) ?? new User(model.Id, this); | |||
user.Avatar = 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; | |||
} | |||
_users[model.Id] = user; | |||
return user; | |||
} | |||
public Server GetServer(string id) | |||
{ | |||
if (id == null) return null; | |||
Server server = null; | |||
_servers.TryGetValue(id, out server); | |||
return server; | |||
} | |||
private Server UpdateServer(ServerInfo model) | |||
{ | |||
var server = GetServer(model.Id) ?? new Server(model.Id, this); | |||
server.Name = model.Name; | |||
if (model is ExtendedServerInfo) | |||
{ | |||
var extendedModel = model as ExtendedServerInfo; | |||
server.AFKChannelId = extendedModel.AFKChannelId; | |||
server.AFKTimeout = extendedModel.AFKTimeout; | |||
server.JoinedAt = extendedModel.JoinedAt; | |||
server.OwnerId = extendedModel.OwnerId; | |||
server.Presence = extendedModel.Presence; | |||
server.Region = extendedModel.Region; | |||
server.Roles = extendedModel.Roles; | |||
server.VoiceStates = extendedModel.VoiceStates; | |||
foreach (var channel in extendedModel.Channels) | |||
{ | |||
UpdateChannel(channel, model.Id); | |||
server._channels[channel.Id] = true; | |||
} | |||
foreach (var membership in extendedModel.Members) | |||
{ | |||
UpdateUser(membership.User); | |||
server._members[membership.User.Id] = true; | |||
} | |||
} | |||
_servers[model.Id] = server; | |||
return server; | |||
} | |||
public Channel GetChannel(string id) | |||
{ | |||
if (id == null) return null; | |||
Channel channel = null; | |||
_channels.TryGetValue(id, out channel); | |||
return channel; | |||
} | |||
private Channel UpdateChannel(ChannelInfo model, string serverId) | |||
{ | |||
var channel = GetChannel(model.Id) ?? new Channel(model.Id, serverId, this); | |||
channel.Name = model.Name; | |||
channel.IsPrivate = model.IsPrivate; | |||
channel.PermissionOverwrites = model.PermissionOverwrites; | |||
channel.RecipientId = model.Recipient?.Id; | |||
channel.Type = model.Type; | |||
_channels[model.Id] = channel; | |||
return channel; | |||
} | |||
private Channel DeleteChannel(string id) | |||
{ | |||
Channel channel = null; | |||
if (_channels.TryRemove(id, out channel)) | |||
{ | |||
bool ignored; | |||
channel.Server._channels.TryRemove(id, out ignored); | |||
} | |||
return channel; | |||
} | |||
//TODO: Temporary measure, unsure if we want to store these or not. | |||
private ChatMessageReference GetMessage(string id, string channelId) | |||
{ | |||
if (id == null || channelId == null) return null; | |||
var msg = new ChatMessageReference(id, this); | |||
msg.ChannelId = channelId; | |||
return msg; | |||
} | |||
private ChatMessage UpdateMessage(WebSocketEvents.MessageCreate model) | |||
{ | |||
return new ChatMessage(model.Id, this) | |||
{ | |||
Attachments = model.Attachments, | |||
ChannelId = model.ChannelId, | |||
Text = model.Content, | |||
Embeds = model.Embeds, | |||
IsMentioningEveryone = model.IsMentioningEveryone, | |||
IsTTS = model.IsTextToSpeech, | |||
UserId = model.Author.Id, | |||
Timestamp = model.Timestamp | |||
}; | |||
} | |||
private void CheckReady() | |||
{ | |||
if (!_isReady) | |||
throw new InvalidOperationException("The client is not currently connected to Discord"); | |||
} | |||
} | |||
} |
@@ -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)); | |||
} | |||
} | |||
} |
@@ -0,0 +1,189 @@ | |||
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 volatile ClientWebSocket _webSocket; | |||
private volatile CancellationTokenSource _cancelToken; | |||
private volatile Task _tasks; | |||
private ConcurrentQueue<byte[]> _sendQueue; | |||
private int _heartbeatInterval; | |||
private DateTime _lastHeartbeat; | |||
public async Task ConnectAsync(string url, HttpOptions options) | |||
{ | |||
await DisconnectAsync(); | |||
_sendQueue = new ConcurrentQueue<byte[]>(); | |||
_webSocket = new ClientWebSocket(); | |||
_webSocket.Options.Cookies = options.Cookies; | |||
_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(); | |||
}); | |||
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); | |||
} | |||
public async Task DisconnectAsync() | |||
{ | |||
if (_webSocket != null) | |||
{ | |||
_cancelToken.Cancel(); | |||
await _tasks; | |||
} | |||
} | |||
private async Task ReceiveAsync() | |||
{ | |||
RaiseConnected(); | |||
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); | |||
} | |||
RaiseGotEvent(msg.Type, msg.Payload as JToken); | |||
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 | |||
} | |||
} |
@@ -0,0 +1,142 @@ | |||
using Newtonsoft.Json; | |||
using System; | |||
using System.IO; | |||
using System.IO.Compression; | |||
using System.Net; | |||
using System.Net.Cache; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord.Helpers | |||
{ | |||
internal class HttpOptions | |||
{ | |||
public string UserAgent, Token; | |||
public CookieContainer Cookies; | |||
public HttpOptions(string userAgent = null) | |||
{ | |||
UserAgent = userAgent ?? "DiscordAPI"; | |||
Cookies = new CookieContainer(1); | |||
} | |||
} | |||
internal static class Http | |||
{ | |||
private static readonly RequestCachePolicy _cachePolicy = new RequestCachePolicy(RequestCacheLevel.NoCacheNoStore); | |||
internal static async Task<ResponseT> Get<ResponseT>(string path, object data, HttpOptions options) | |||
where ResponseT : class | |||
{ | |||
string requestJson = JsonConvert.SerializeObject(data); | |||
string responseJson = await SendRequest("GET", path, requestJson, options, true); | |||
return JsonConvert.DeserializeObject<ResponseT>(responseJson); | |||
} | |||
internal static async Task<ResponseT> Get<ResponseT>(string path, HttpOptions options) | |||
where ResponseT : class | |||
{ | |||
string responseJson = await SendRequest("GET", path, null, options, true); | |||
return JsonConvert.DeserializeObject<ResponseT>(responseJson); | |||
} | |||
internal static async Task<ResponseT> Post<ResponseT>(string path, object data, HttpOptions options) | |||
where ResponseT : class | |||
{ | |||
string requestJson = JsonConvert.SerializeObject(data); | |||
string responseJson = await SendRequest("POST", path, requestJson, options, true); | |||
return JsonConvert.DeserializeObject<ResponseT>(responseJson); | |||
} | |||
internal static Task Post(string path, object data, HttpOptions options) | |||
{ | |||
string requestJson = JsonConvert.SerializeObject(data); | |||
return SendRequest("POST", path, requestJson, options, false); | |||
} | |||
internal static async Task<ResponseT> Post<ResponseT>(string path, HttpOptions options) | |||
where ResponseT : class | |||
{ | |||
string responseJson = await SendRequest("POST", path, null, options, true); | |||
return JsonConvert.DeserializeObject<ResponseT>(responseJson); | |||
} | |||
internal static Task Post(string path, HttpOptions options) | |||
{ | |||
return SendRequest("POST", path, null, options, false); | |||
} | |||
internal static Task Delete(string path, HttpOptions options) | |||
{ | |||
return SendRequest("DELETE", path, null, options, false); | |||
} | |||
private static async Task<string> SendRequest(string method, string path, string data, HttpOptions options, bool hasResponse) | |||
{ | |||
options = options ?? new HttpOptions(); | |||
//Create Request | |||
HttpWebRequest request = WebRequest.CreateHttp(path); | |||
request.Accept = "*/*"; | |||
request.Headers[HttpRequestHeader.AcceptLanguage] = "en-US;q=0.8"; | |||
request.Headers[HttpRequestHeader.AcceptEncoding] = "gzip, deflate"; | |||
request.CachePolicy = _cachePolicy; | |||
request.CookieContainer = options.Cookies; | |||
request.Method = method; | |||
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? | |||
if (!string.IsNullOrEmpty(response.ContentEncoding)) | |||
{ | |||
largeBuffer.Position = 0; | |||
using (var decoder = GetDecoder(response.ContentEncoding, largeBuffer)) | |||
using (var decodedStream = new MemoryStream()) | |||
{ | |||
decoder.CopyTo(decodedStream); | |||
return Encoding.UTF8.GetString(decodedStream.GetBuffer(), 0, (int)decodedStream.Length); | |||
} | |||
} | |||
else | |||
return Encoding.UTF8.GetString(largeBuffer.GetBuffer(), 0, (int)largeBuffer.Length); | |||
} | |||
} | |||
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); | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,41 @@ | |||
using Newtonsoft.Json; | |||
namespace Discord.Models | |||
{ | |||
public class Channel | |||
{ | |||
protected readonly DiscordClient _client; | |||
private string _name; | |||
public string Id { get; } | |||
public string Name { get { return !IsPrivate ? _name : '@' + Recipient.Name; } internal set { _name = value; } } | |||
public bool IsPrivate { get; internal set; } | |||
public string Type { get; internal set; } | |||
[JsonIgnore] | |||
public string ServerId { get; } | |||
[JsonIgnore] | |||
public Server Server { get { return ServerId != null ? _client.GetServer(ServerId) : null; } } | |||
[JsonIgnore] | |||
public string RecipientId { get; internal set; } | |||
public User Recipient { get { return _client.GetUser(RecipientId); } } | |||
//Not Implemented | |||
public object[] PermissionOverwrites { get; internal set; } | |||
internal Channel(string id, string serverId, DiscordClient client) | |||
{ | |||
Id = id; | |||
ServerId = serverId; | |||
_client = client; | |||
} | |||
public override string ToString() | |||
{ | |||
return Name; | |||
//return Name + " (" + Id + ")"; | |||
} | |||
} | |||
} |
@@ -0,0 +1,31 @@ | |||
using Newtonsoft.Json; | |||
using System; | |||
namespace Discord.Models | |||
{ | |||
public class ChatMessage : ChatMessageReference | |||
{ | |||
public bool IsMentioningEveryone { get; internal set; } | |||
public bool IsTTS { get; internal set; } | |||
public string Text { get; internal set; } | |||
public DateTime Timestamp { get; internal set; } | |||
[JsonIgnore] | |||
public string UserId { get; internal set; } | |||
public User User { get { return _client.GetUser(UserId); } } | |||
//Not Implemented | |||
public object[] Attachments { get; internal set; } | |||
public object[] Embeds { get; internal set; } | |||
internal ChatMessage(string id, DiscordClient client) | |||
: base(id, client) | |||
{ | |||
} | |||
public override string ToString() | |||
{ | |||
return User.ToString() + ": " + Text; | |||
} | |||
} | |||
} |
@@ -0,0 +1,17 @@ | |||
namespace Discord.Models | |||
{ | |||
public class ChatMessageReference | |||
{ | |||
protected readonly DiscordClient _client; | |||
public string Id { get; } | |||
public string ChannelId { get; internal set; } | |||
public Channel Channel { get { return _client.GetChannel(ChannelId); } } | |||
internal ChatMessageReference(string id, DiscordClient client) | |||
{ | |||
Id = id; | |||
_client = client; | |||
} | |||
} | |||
} |
@@ -0,0 +1,53 @@ | |||
using Newtonsoft.Json; | |||
using System; | |||
using System.Collections.Concurrent; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
namespace Discord.Models | |||
{ | |||
public class Server | |||
{ | |||
protected 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 { get { return _client.GetUser(OwnerId); } } | |||
internal ConcurrentDictionary<string, bool> _members; | |||
[JsonIgnore] | |||
public IEnumerable<string> MemberIds { get { return _members.Keys; } } | |||
public IEnumerable<User> Members { get { return _members.Keys.Select(x => _client.GetUser(x)); } } | |||
internal ConcurrentDictionary<string, bool> _channels; | |||
[JsonIgnore] | |||
public IEnumerable<string> ChannelIds { get { return _channels.Keys; } } | |||
public IEnumerable<Channel> Channels { get { return _channels.Keys.Select(x => _client.GetChannel(x)); } } | |||
//Not Implemented | |||
public object Presence { get; internal set; } | |||
public object[] Roles { get; internal set; } | |||
public object[] VoiceStates { get; internal set; } | |||
internal Server(string id, DiscordClient client) | |||
{ | |||
Id = id; | |||
_client = client; | |||
_members = new ConcurrentDictionary<string, bool>(); | |||
_channels = new ConcurrentDictionary<string, bool>(); | |||
} | |||
public override string ToString() | |||
{ | |||
return Name; | |||
//return Name + " (" + Id + ")"; | |||
} | |||
} | |||
} |
@@ -0,0 +1,40 @@ | |||
using System; | |||
namespace Discord.Models | |||
{ | |||
public class User | |||
{ | |||
protected readonly DiscordClient _client; | |||
public string Id { get; } | |||
public string Name { get; internal set; } | |||
public string Avatar { get; internal set; } | |||
public string Discriminator { get; internal set; } | |||
public string Email { get; internal set; } | |||
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; | |||
//return Name + " (" + Id + ")"; | |||
} | |||
} | |||
} |
@@ -0,0 +1,36 @@ | |||
using System.Reflection; | |||
using System.Runtime.CompilerServices; | |||
using System.Runtime.InteropServices; | |||
// General Information about an assembly is controlled through the following | |||
// set of attributes. Change these attribute values to modify the information | |||
// associated with an assembly. | |||
[assembly: AssemblyTitle("DiscordAPI")] | |||
[assembly: AssemblyDescription("")] | |||
[assembly: AssemblyConfiguration("")] | |||
[assembly: AssemblyCompany("")] | |||
[assembly: AssemblyProduct("DiscordAPI")] | |||
[assembly: AssemblyCopyright("Copyright © 2015")] | |||
[assembly: AssemblyTrademark("")] | |||
[assembly: AssemblyCulture("")] | |||
// Setting ComVisible to false makes the types in this assembly not visible | |||
// to COM components. If you need to access a type in this assembly from | |||
// COM, set the ComVisible attribute to true on that type. | |||
[assembly: ComVisible(false)] | |||
// The following GUID is for the ID of the typelib if this project is exposed to COM | |||
[assembly: Guid("8d23f61b-723c-4966-859d-1119b28bcf19")] | |||
// Version information for an assembly consists of the following four values: | |||
// | |||
// Major Version | |||
// Minor Version | |||
// Build Number | |||
// Revision | |||
// | |||
// You can specify all the values or you can default the Build and Revision Numbers | |||
// by using the '*' as shown below: | |||
// [assembly: AssemblyVersion("1.0.*")] | |||
[assembly: AssemblyVersion("1.0.0.0")] | |||
[assembly: AssemblyFileVersion("1.0.0.0")] |
@@ -0,0 +1,32 @@ | |||
using System; | |||
namespace Discord | |||
{ | |||
public enum Region | |||
{ | |||
US_West, | |||
US_East, | |||
Singapore, | |||
London, | |||
Sydney, | |||
Amsterdam | |||
} | |||
internal static class RegionConverter | |||
{ | |||
public static string Convert(Region region) | |||
{ | |||
switch (region) | |||
{ | |||
case Region.US_West: return "us-west"; | |||
case Region.US_East: return "us-east"; | |||
case Region.Singapore: return "singapore"; | |||
case Region.London: return "london"; | |||
case Region.Sydney: return "sydney"; | |||
case Region.Amsterdam: return "amsterdam"; | |||
default: | |||
throw new ArgumentOutOfRangeException("Unknown server region"); | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,4 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<packages> | |||
<package id="Newtonsoft.Json" version="7.0.1" targetFramework="net452" /> | |||
</packages> |