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