diff --git a/Discord.Net.Tests/ChannelTests.cs b/Discord.Net.Tests/ChannelTests.cs new file mode 100644 index 000000000..82ca45c41 --- /dev/null +++ b/Discord.Net.Tests/ChannelTests.cs @@ -0,0 +1,48 @@ +using Discord.Models; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord.Net.Tests +{ + [TestClass] + public class ChannelTests + { + private DiscordClient _bot1, _bot2; + + [TestInitialize] + public void Initialize() + { + _bot1 = new DiscordClient(); + _bot2 = new DiscordClient(); + + _bot1.Connect(Settings.Test1_Username, Settings.Test1_Password).Wait(); + _bot2.Connect(Settings.Test2_Username, Settings.Test2_Password).Wait(); + + //Cleanup existing servers + Task.WaitAll(_bot1.Servers.Select(x => _bot1.LeaveServer(x)).ToArray()); + Task.WaitAll(_bot2.Servers.Select(x => _bot2.LeaveServer(x)).ToArray()); + } + + [TestMethod] + public async Task DoNothing() + { + Server server = await _bot1.CreateServer("Discord.Net Testbed", Region.US_East); + Invite invite = await _bot1.CreateInvite(server, 60, 1, false, false); + await _bot2.AcceptInvite(invite); + await _bot2.LeaveServer(server); + } + + [TestCleanup] + public void Cleanup() + { + if (_bot1.IsConnected) + Task.WaitAll(_bot1.Servers.Select(x => _bot1.LeaveServer(x)).ToArray()); + if (_bot2.IsConnected) + Task.WaitAll(_bot2.Servers.Select(x => _bot2.LeaveServer(x)).ToArray()); + + _bot1.Disconnect().Wait(); + _bot2.Disconnect().Wait(); + } + } +} diff --git a/Discord.Net.Tests/Discord.Net.Tests.csproj b/Discord.Net.Tests/Discord.Net.Tests.csproj new file mode 100644 index 000000000..15c96c217 --- /dev/null +++ b/Discord.Net.Tests/Discord.Net.Tests.csproj @@ -0,0 +1,90 @@ + + + + Debug + AnyCPU + {855D6B1D-847B-42DA-BE6A-23683EA89511} + Library + Properties + Discord.Net.Tests + Discord.Net.Tests + v4.5.2 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + {8d23f61b-723c-4966-859d-1119b28bcf19} + Discord.Net + + + + + + + False + + + False + + + False + + + False + + + + + + + + \ No newline at end of file diff --git a/Discord.Net.Tests/Properties/AssemblyInfo.cs b/Discord.Net.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..5b1c7b125 --- /dev/null +++ b/Discord.Net.Tests/Properties/AssemblyInfo.cs @@ -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("Discord.Net.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Discord.Net.Tests")] +[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("855d6b1d-847b-42da-be6a-23683ea89511")] + +// 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")] diff --git a/Discord.Net.sln b/Discord.Net.sln index cdfa8301a..16724d0a9 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -11,6 +11,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution LICENSE = LICENSE EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Tests", "Discord.Net.Tests\Discord.Net.Tests.csproj", "{855D6B1D-847B-42DA-BE6A-23683EA89511}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +23,10 @@ Global {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 + {855D6B1D-847B-42DA-BE6A-23683EA89511}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {855D6B1D-847B-42DA-BE6A-23683EA89511}.Debug|Any CPU.Build.0 = Debug|Any CPU + {855D6B1D-847B-42DA-BE6A-23683EA89511}.Release|Any CPU.ActiveCfg = Release|Any CPU + {855D6B1D-847B-42DA-BE6A-23683EA89511}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Discord.Net/API/DiscordAPI.cs b/Discord.Net/API/DiscordAPI.cs index 745875d17..dd50bcfe9 100644 --- a/Discord.Net/API/DiscordAPI.cs +++ b/Discord.Net/API/DiscordAPI.cs @@ -6,17 +6,18 @@ namespace Discord.API { internal static class DiscordAPI { - public static async Task LoginAnonymous(string username, HttpOptions options) + //Auth + public static async Task LoginAnonymous(string username, HttpOptions options) { - var fingerprintResponse = await Http.Post(Endpoints.AuthFingerprint, options); - var registerRequest = new AuthRegisterRequest { Fingerprint = fingerprintResponse.Fingerprint, Username = username }; - var registerResponse = await Http.Post(Endpoints.AuthRegister, registerRequest, options); + var fingerprintResponse = await Http.Post(Endpoints.AuthFingerprint, options); + var registerRequest = new APIRequests.AuthRegisterRequest { Fingerprint = fingerprintResponse.Fingerprint, Username = username }; + var registerResponse = await Http.Post(Endpoints.AuthRegister, registerRequest, options); return registerResponse; } - public static async Task Login(string email, string password, HttpOptions options) + public static async Task Login(string email, string password, HttpOptions options) { - var request = new AuthLoginRequest { Email = email, Password = password }; - var response = await Http.Post(Endpoints.AuthLogin, request, options); + var request = new APIRequests.AuthLogin { Email = email, Password = password }; + var response = await Http.Post(Endpoints.AuthLogin, request, options); options.Token = response.Token; return response; } @@ -25,37 +26,61 @@ namespace Discord.API return Http.Post(Endpoints.AuthLogout, options); } - public static Task CreateServer(string name, string region, HttpOptions options) + //Servers + public static Task CreateServer(string name, string region, HttpOptions options) { - var request = new CreateServerRequest { Name = name, Region = region }; - return Http.Post(Endpoints.Servers, request, options); + var request = new APIRequests.CreateServer { Name = name, Region = region }; + return Http.Post(Endpoints.Servers, request, options); + } + public static Task LeaveServer(string id, HttpOptions options) + { + return Http.Delete(Endpoints.Server(id), options); } - public static Task DeleteServer(string id, HttpOptions options) + + //Channels + public static Task GetMessages(string channelId, HttpOptions options) { - return Http.Delete(Endpoints.Server(id), options); - } + return Http.Get(Endpoints.ChannelMessages(channelId, 50), options); + } - public static Task GetInvite(string id, HttpOptions options) + //Invites + public static Task CreateInvite(string channelId, int maxAge, int maxUses, bool isTemporary, bool hasXkcdPass, HttpOptions options) { - return Http.Get(Endpoints.Invite(id), options); + var request = new APIRequests.CreateInvite { MaxAge = maxAge, MaxUses = maxUses, IsTemporary = isTemporary, HasXkcdPass = hasXkcdPass }; + return Http.Post(Endpoints.ChannelInvites(channelId), request, options); + } + public static Task GetInvite(string id, HttpOptions options) + { + return Http.Get(Endpoints.Invite(id), options); } public static Task AcceptInvite(string id, HttpOptions options) { - return Http.Post(Endpoints.Invite(id), 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) + + //Chat + public static Task SendMessage(string channelId, string message, string[] mentions, HttpOptions options) + { + var request = new APIRequests.SendMessage { Content = message, Mentions = mentions }; + return Http.Post(Endpoints.ChannelMessages(channelId), request, options); + } + public static Task SendIsTyping(string channelId, HttpOptions options) { return Http.Post(Endpoints.ChannelTyping(channelId), options); } - public static Task SendMessage(string channelId, string message, string[] mentions, HttpOptions options) + + //Voice + public static Task GetVoiceRegions(HttpOptions options) { - var request = new SendMessageRequest { Content = message, Mentions = mentions }; - return Http.Post(Endpoints.ChannelMessages(channelId), request, options); + return Http.Get(Endpoints.VoiceRegions, options); + } + public static Task GetVoiceIce(HttpOptions options) + { + return Http.Get(Endpoints.VoiceIce, options); } - } + } } diff --git a/Discord.Net/API/Endpoints.cs b/Discord.Net/API/Endpoints.cs index bbc8eea7a..e505ceb46 100644 --- a/Discord.Net/API/Endpoints.cs +++ b/Discord.Net/API/Endpoints.cs @@ -2,29 +2,43 @@ { 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 BaseUrl = "discordapp.com"; + public static readonly string BaseHttps = $"https://{BaseUrl}"; + + // /api + public static readonly string BaseApi = $"{BaseHttps}/api"; + public static readonly string Track = $"{BaseApi}/track"; - public static readonly string Auth = $"{BaseHttps}/api/auth"; + // /api/auth + public static readonly string Auth = $"{BaseApi}/auth"; public static readonly string AuthFingerprint = $"{Auth}fingerprint"; public static readonly string AuthRegister = $"{Auth}/register"; public static readonly string AuthLogin = $"{Auth}/login"; public static readonly string AuthLogout = $"{Auth}/logout"; - public static readonly string Servers = $"{BaseHttps}/api/guilds"; + // /api/guilds + public static readonly string Servers = $"{BaseApi}/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"; + // /api/guilds + public static readonly string Invites = $"{BaseApi}/invite"; public static string Invite(string id) { return $"{Invites}/{id}"; } - public static readonly string Channels = $"{BaseHttps}/api/channels"; + // /api/channels + public static readonly string Channels = $"{BaseApi}/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 string ChannelMessages(string id, int limit) { return $"{Channels}/{id}/messages?limit={limit}"; } + public static string ChannelInvites(string id) { return $"{Channels}/{id}/invites"; } - public static readonly string WebSocket_Hub = BaseWss + "hub"; - + // /api/voice + public static readonly string Voice = $"{BaseApi}/voice"; + public static readonly string VoiceRegions = $"{Voice}/regions"; + public static readonly string VoiceIce = $"{Voice}/ice"; + + //Web Sockets + public static readonly string BaseWss = "wss://" + BaseUrl; + public static readonly string WebSocket_Hub = $"{BaseWss}/hub"; } } diff --git a/Discord.Net/API/Models/APIResponses.cs b/Discord.Net/API/Models/APIResponses.cs new file mode 100644 index 000000000..70447f4e8 --- /dev/null +++ b/Discord.Net/API/Models/APIResponses.cs @@ -0,0 +1,91 @@ +//Ignore unused/unassigned variable warnings +#pragma warning disable CS0649 +#pragma warning disable CS0169 + +using Newtonsoft.Json; +using System; + +namespace Discord.API.Models +{ + + internal static class APIResponses + { + public class AuthFingerprint + { + [JsonProperty(PropertyName = "fingerprint")] + public string Fingerprint; + } + public class AuthRegister : AuthLogin { } + public class AuthLogin + { + [JsonProperty(PropertyName = "token")] + public string Token; + } + + public class CreateServer : ServerInfo { } + public class DeleteServer : ServerInfo { } + + public class CreateInvite : GetInvite + { + [JsonProperty(PropertyName = "max_age")] + public int MaxAge; + [JsonProperty(PropertyName = "max_uses")] + public int MaxUses; + [JsonProperty(PropertyName = "revoked")] + public bool IsRevoked; + [JsonProperty(PropertyName = "temporary")] + public bool IsTemporary; + [JsonProperty(PropertyName = "uses")] + public int Uses; + [JsonProperty(PropertyName = "created_at")] + public DateTime CreatedAt; + } + + public class GetInvite + { + [JsonProperty(PropertyName = "inviter")] + public UserReference Inviter; + [JsonProperty(PropertyName = "guild")] + public ServerReference Server; + [JsonProperty(PropertyName = "channel")] + public ChannelReference Channel; + [JsonProperty(PropertyName = "code")] + public string Code; + [JsonProperty(PropertyName = "xkcdpass")] + public string XkcdPass; + } + public class AcceptInvite : GetInvite { } + + public class GetMessages : Message { } + + public class GetRegions + { + [JsonProperty(PropertyName = "sample_hostname")] + public string Hostname; + [JsonProperty(PropertyName = "sample_port")] + public int Port; + [JsonProperty(PropertyName = "id")] + public string Id; + [JsonProperty(PropertyName = "name")] + public string Name; + } + + public class GetIce + { + [JsonProperty(PropertyName = "ttl")] + public string TTL; + [JsonProperty(PropertyName = "servers")] + public Server[] Servers; + + public class Server + { + [JsonProperty(PropertyName = "url")] + public string URL; + [JsonProperty(PropertyName = "username")] + public string Username; + [JsonProperty(PropertyName = "credential")] + public string Credential; + } + } + } +} diff --git a/Discord.Net/API/Models/ApiRequests.cs b/Discord.Net/API/Models/ApiRequests.cs index 882491300..96b0be614 100644 --- a/Discord.Net/API/Models/ApiRequests.cs +++ b/Discord.Net/API/Models/ApiRequests.cs @@ -6,61 +6,49 @@ using Newtonsoft.Json; namespace Discord.API.Models { - public class AuthFingerprintResponse + internal static class APIRequests { - [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 AuthRegisterRequest + { + [JsonProperty(PropertyName = "fingerprint")] + public string Fingerprint; + [JsonProperty(PropertyName = "username")] + public string Username; + } + public class AuthLogin + { + [JsonProperty(PropertyName = "email")] + public string Email; + [JsonProperty(PropertyName = "password")] + public string Password; + } - public class CreateServerRequest - { - [JsonProperty(PropertyName = "name")] - public string Name; - [JsonProperty(PropertyName = "region")] - public string Region; - } + public class CreateServer + { + [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 CreateInvite + { + [JsonProperty(PropertyName = "max_age")] + public int MaxAge; + [JsonProperty(PropertyName = "max_uses")] + public int MaxUses; + [JsonProperty(PropertyName = "temporary")] + public bool IsTemporary; + [JsonProperty(PropertyName = "xkcdpass")] + public bool HasXkcdPass; + } - public class SendMessageRequest - { - [JsonProperty(PropertyName = "content")] - public string Content; - [JsonProperty(PropertyName = "mentions")] - public string[] Mentions; + public class SendMessage + { + [JsonProperty(PropertyName = "content")] + public string Content; + [JsonProperty(PropertyName = "mentions")] + public string[] Mentions; + } } } diff --git a/Discord.Net/API/Models/General.cs b/Discord.Net/API/Models/Common.cs similarity index 62% rename from Discord.Net/API/Models/General.cs rename to Discord.Net/API/Models/Common.cs index 46c487d8d..37cf91c22 100644 --- a/Discord.Net/API/Models/General.cs +++ b/Discord.Net/API/Models/Common.cs @@ -32,7 +32,8 @@ namespace Discord.API.Models } } - public class UserInfo + //Users + internal class UserReference { [JsonProperty(PropertyName = "username")] public string Username; @@ -43,14 +44,14 @@ namespace Discord.API.Models [JsonProperty(PropertyName = "avatar")] public string Avatar; } - public class SelfUserInfo : UserInfo + internal class SelfUserInfo : UserReference { [JsonProperty(PropertyName = "email")] public string Email; [JsonProperty(PropertyName = "verified")] public bool IsVerified; } - public class PresenceUserInfo : UserInfo + internal class PresenceUserInfo : UserReference { [JsonProperty(PropertyName = "game_id")] public string GameId; @@ -58,78 +59,115 @@ namespace Discord.API.Models 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 + //Channels + internal class ChannelReference { [JsonProperty(PropertyName = "id")] public string Id; + [JsonProperty(PropertyName = "guild_id")] + public string GuildId; [JsonProperty(PropertyName = "name")] public string Name; + [JsonProperty(PropertyName = "type")] + public string Type; + } + internal class ChannelInfo : ChannelReference + { [JsonProperty(PropertyName = "last_message_id")] public string LastMessageId; [JsonProperty(PropertyName = "is_private")] public bool IsPrivate; - [JsonProperty(PropertyName = "type")] - public string Type; [JsonProperty(PropertyName = "permission_overwrites")] public object[] PermissionOverwrites; [JsonProperty(PropertyName = "recipient")] - public UserInfo Recipient; + public UserReference Recipient; } - public class ServerInfo + //Servers + internal class ServerReference { [JsonProperty(PropertyName = "id")] public string Id; [JsonProperty(PropertyName = "name")] public string Name; } - public class ExtendedServerInfo : ServerInfo + internal class ServerInfo : ServerReference { [JsonProperty(PropertyName = "afk_channel_id")] public string AFKChannelId; [JsonProperty(PropertyName = "afk_timeout")] public int AFKTimeout; - [JsonProperty(PropertyName = "channels")] - public ChannelInfo[] Channels; + [JsonProperty(PropertyName = "embed_channel_id")] + public string EmbedChannelId; + [JsonProperty(PropertyName = "embed_enabled")] + public bool EmbedEnabled; [JsonProperty(PropertyName = "joined_at")] - public DateTime JoinedAt; - [JsonProperty(PropertyName = "members")] - public MembershipInfo[] Members; + public DateTime? JoinedAt; [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; + public Role[] Roles; + } + internal class ExtendedServerInfo : ServerInfo + { + public class Membership + { + [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 UserReference User; + } + + [JsonProperty(PropertyName = "channels")] + public ChannelInfo[] Channels; + [JsonProperty(PropertyName = "members")] + public Membership[] Members; + [JsonProperty(PropertyName = "presence")] + public object[] Presence; [JsonProperty(PropertyName = "voice_states")] public object[] VoiceStates; } + //Messages internal class MessageReference { - [JsonProperty(PropertyName = "message_id")] - public string MessageId; + [JsonProperty(PropertyName = "id")] + public string Id; [JsonProperty(PropertyName = "channel_id")] public string ChannelId; + [JsonProperty(PropertyName = "message_id")] + public string MessageId { get { return Id; } set { Id = value; } } + } + internal class Message : MessageReference + { + [JsonProperty(PropertyName = "tts")] + public bool IsTextToSpeech; + [JsonProperty(PropertyName = "mention_everyone")] + public bool IsMentioningEveryone; + [JsonProperty(PropertyName = "timestamp")] + public DateTime Timestamp; + [JsonProperty(PropertyName = "mentions")] + public UserReference[] Mentions; + [JsonProperty(PropertyName = "embeds")] + public object[] Embeds; + [JsonProperty(PropertyName = "attachments")] + public object[] Attachments; + [JsonProperty(PropertyName = "content")] + public string Content; + [JsonProperty(PropertyName = "author")] + public UserReference Author; } - internal class Role + //Roles + internal class Role { [JsonProperty(PropertyName = "permissions")] public int Permissions; diff --git a/Discord.Net/API/Models/WebSocketEvents.cs b/Discord.Net/API/Models/WebSocketEvents.cs index 827d87a87..d423226e0 100644 --- a/Discord.Net/API/Models/WebSocketEvents.cs +++ b/Discord.Net/API/Models/WebSocketEvents.cs @@ -25,65 +25,72 @@ namespace Discord.API.Models public int HeartbeatInterval; } + //Servers internal sealed class GuildCreate : ExtendedServerInfo { } internal sealed class GuildDelete : ExtendedServerInfo { } + //Channels internal sealed class ChannelCreate : ChannelInfo { } internal sealed class ChannelDelete : ChannelInfo { } internal sealed class ChannelUpdate : ChannelInfo { } - internal sealed class GuildMemberAdd : GuildMemberUpdate + //Memberships + internal abstract class GuildMemberEvent + { + [JsonProperty(PropertyName = "user")] + public UserReference User; + [JsonProperty(PropertyName = "guild_id")] + public string GuildId; + } + internal sealed class GuildMemberAdd : GuildMemberEvent { [JsonProperty(PropertyName = "joined_at")] public DateTime JoinedAt; + [JsonProperty(PropertyName = "roles")] + public object[] Roles; } - internal class GuildMemberUpdate + internal sealed class GuildMemberUpdate : GuildMemberEvent { - [JsonProperty(PropertyName = "user")] - public UserInfo User; [JsonProperty(PropertyName = "roles")] public object[] Roles; - [JsonProperty(PropertyName = "guild_id")] - public string GuildId; } - internal sealed class GuildMemberRemove + internal sealed class GuildMemberRemove : GuildMemberEvent { } + + //Roles + internal abstract class GuildRoleEvent { - [JsonProperty(PropertyName = "user")] - public UserInfo User; [JsonProperty(PropertyName = "guild_id")] public string GuildId; } - - internal sealed class GuildRoleCreateUpdate + internal sealed class GuildRoleCreateUpdate : GuildRoleEvent { [JsonProperty(PropertyName = "role")] public Role Role; - [JsonProperty(PropertyName = "guild_id")] - public string GuildId; } - internal sealed class GuildRoleDelete + internal sealed class GuildRoleDelete : GuildRoleEvent { [JsonProperty(PropertyName = "role_id")] public string RoleId; - [JsonProperty(PropertyName = "guild_id")] - public string GuildId; } - internal sealed class GuildBanAddRemove + //Bans + internal abstract class GuildBanEvent { - [JsonProperty(PropertyName = "user")] - public UserInfo User; [JsonProperty(PropertyName = "guild_id")] public string GuildId; } - internal sealed class GuildBanRemove + internal sealed class GuildBanAddRemove : GuildBanEvent + { + [JsonProperty(PropertyName = "user")] + public UserReference User; + } + internal sealed class GuildBanRemove : GuildBanEvent { [JsonProperty(PropertyName = "user_id")] public string UserId; - [JsonProperty(PropertyName = "guild_id")] - public string GuildId; } + //User internal sealed class UserUpdate : SelfUserInfo { } internal sealed class PresenceUpdate : PresenceUserInfo { } internal sealed class VoiceStateUpdate @@ -107,35 +114,11 @@ namespace Discord.API.Models [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 + + //Chat + internal sealed class MessageCreate : Message { } + internal sealed class MessageUpdate : MessageReference { - [JsonProperty(PropertyName = "id")] - public string Id; - [JsonProperty(PropertyName = "channel_id")] - public string ChannelId; [JsonProperty(PropertyName = "embeds")] public object[] Embeds; } @@ -150,5 +133,14 @@ namespace Discord.API.Models [JsonProperty(PropertyName = "timestamp")] public int Timestamp; } + + //Voice + internal sealed class VoiceServerUpdate + { + [JsonProperty(PropertyName = "guild_id")] + public string ServerId; + [JsonProperty(PropertyName = "endpoint")] + public string Endpoint; + } } } diff --git a/Discord.Net/Discord.Net.csproj b/Discord.Net/Discord.Net.csproj index eda49edcd..e0f31cce0 100644 --- a/Discord.Net/Discord.Net.csproj +++ b/Discord.Net/Discord.Net.csproj @@ -45,13 +45,15 @@ - - + + + + + - - + diff --git a/Discord.Net/DiscordClient.Events.cs b/Discord.Net/DiscordClient.Events.cs index 62349c566..8ab4ea21c 100644 --- a/Discord.Net/DiscordClient.Events.cs +++ b/Discord.Net/DiscordClient.Events.cs @@ -11,7 +11,6 @@ namespace Discord public readonly string Message; internal LogMessageEventArgs(string msg) { Message = msg; } } - public event EventHandler DebugMessage; private void RaiseOnDebugMessage(string message) { @@ -26,7 +25,6 @@ namespace Discord if (Connected != null) Connected(this, EventArgs.Empty); } - public event EventHandler Disconnected; private void RaiseDisconnected() { @@ -34,12 +32,12 @@ namespace Discord Disconnected(this, EventArgs.Empty); } - public event EventHandler LoggedIn; + /*public event EventHandler LoggedIn; private void RaiseLoggedIn() { if (LoggedIn != null) LoggedIn(this, EventArgs.Empty); - } + }*/ //Server public sealed class ServerEventArgs : EventArgs @@ -54,7 +52,6 @@ namespace Discord if (ServerCreated != null) ServerCreated(this, new ServerEventArgs(server)); } - public event EventHandler ServerDestroyed; private void RaiseServerDestroyed(Server server) { @@ -75,14 +72,12 @@ namespace Discord if (ChannelCreated != null) ChannelCreated(this, new ChannelEventArgs(channel)); } - public event EventHandler ChannelDestroyed; private void RaiseChannelDestroyed(Channel channel) { if (ChannelDestroyed != null) ChannelDestroyed(this, new ChannelEventArgs(channel)); } - public event EventHandler ChannelUpdated; private void RaiseChannelUpdated(Channel channel) { @@ -98,40 +93,32 @@ namespace Discord } //Message - 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 readonly Message Message; + internal MessageEventArgs(Message msg) { Message = msg; } } - public event EventHandler MessageCreated; - private void RaiseMessageCreated(ChatMessage msg) + public event EventHandler MessageCreated; + private void RaiseMessageCreated(Message msg) { if (MessageCreated != null) - MessageCreated(this, new MessageCreateEventArgs(msg)); + MessageCreated(this, new MessageEventArgs(msg)); } - public event EventHandler MessageDeleted; - private void RaiseMessageDeleted(ChatMessageReference msg) + private void RaiseMessageDeleted(Message msg) { if (MessageDeleted != null) MessageDeleted(this, new MessageEventArgs(msg)); } - public event EventHandler MessageUpdated; - private void RaiseMessageUpdated(ChatMessageReference msg) + private void RaiseMessageUpdated(Message msg) { if (MessageUpdated != null) MessageUpdated(this, new MessageEventArgs(msg)); } - public event EventHandler MessageAcknowledged; - private void RaiseMessageAcknowledged(ChatMessageReference msg) + private void RaiseMessageAcknowledged(Message msg) { if (MessageAcknowledged != null) MessageAcknowledged(this, new MessageEventArgs(msg)); @@ -150,14 +137,12 @@ namespace Discord if (RoleCreated != null) RoleCreated(this, new RoleEventArgs(role)); } - public event EventHandler RoleUpdated; private void RaiseRoleDeleted(Role role) { if (RoleDeleted != null) RoleDeleted(this, new RoleEventArgs(role)); } - public event EventHandler RoleDeleted; private void RaiseRoleUpdated(Role role) { @@ -183,7 +168,6 @@ namespace Discord if (BanAdded != null) BanAdded(this, new BanEventArgs(user, server)); } - public event EventHandler BanRemoved; private void RaiseBanRemoved(User user, Server server) { @@ -209,14 +193,12 @@ namespace Discord if (MemberAdded != null) MemberAdded(this, new MemberEventArgs(user, server)); } - public event EventHandler MemberRemoved; private void RaiseMemberRemoved(User user, Server server) { if (MemberRemoved != null) MemberRemoved(this, new MemberEventArgs(user, server)); } - public event EventHandler MemberUpdated; private void RaiseMemberUpdated(User user, Server server) { @@ -242,19 +224,36 @@ namespace Discord if (PresenceUpdated != null) PresenceUpdated(this, new UserEventArgs(user)); } - public event EventHandler VoiceStateUpdated; private void RaiseVoiceStateUpdated(User user) { if (VoiceStateUpdated != null) VoiceStateUpdated(this, new UserEventArgs(user)); } - public event EventHandler UserTyping; private void RaiseUserTyping(User user, Channel channel) { if (UserTyping != null) UserTyping(this, new UserTypingEventArgs(user, channel)); } + + //Voice + public sealed class VoiceServerUpdatedEventArgs : EventArgs + { + public readonly Server Server; + public readonly string Endpoint; + internal VoiceServerUpdatedEventArgs(Server server, string endpoint) + { + Server = server; + Endpoint = endpoint; + } + } + + public event EventHandler VoiceServerUpdated; + private void RaiseVoiceServerUpdated(Server server, string endpoint) + { + if (VoiceServerUpdated != null) + VoiceServerUpdated(this, new VoiceServerUpdatedEventArgs(server, endpoint)); + } } } diff --git a/Discord.Net/DiscordClient.cs b/Discord.Net/DiscordClient.cs index ff51ae9f3..a8c86e343 100644 --- a/Discord.Net/DiscordClient.cs +++ b/Discord.Net/DiscordClient.cs @@ -3,10 +3,12 @@ using Discord.API.Models; using Discord.Helpers; using Discord.Models; using System; -using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Reflection; +using System.Threading; using System.Threading.Tasks; +using Message = Discord.Models.Message; using Role = Discord.Models.Role; namespace Discord @@ -19,26 +21,144 @@ namespace Discord private HttpOptions _httpOptions; private bool _isClosing, _isReady; - public string SelfId { get; private set; } - public User Self { get { return GetUser(SelfId); } } + public string UserId { get; private set; } + public User User { get { return _users[UserId]; } } - public IEnumerable Users { get { return _users.Values; } } - private ConcurrentDictionary _users; + public IEnumerable Users { get { return _users; } } + private AsyncCache _users; - public IEnumerable Servers { get { return _servers.Values; } } - private ConcurrentDictionary _servers; + public IEnumerable Servers { get { return _servers; } } + private AsyncCache _servers; - public IEnumerable Channels { get { return _channels.Values; } } - private ConcurrentDictionary _channels; + public IEnumerable Channels { get { return _channels; } } + private AsyncCache _channels; + + public IEnumerable Messages { get { return _messages; } } + private AsyncCache _messages; + + public IEnumerable Roles { get { return _roles; } } + private AsyncCache _roles; + + public bool IsConnected { get { return _isReady; } } 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(); - _servers = new ConcurrentDictionary(); - _channels = new ConcurrentDictionary(); + _servers = new AsyncCache( + (key, parentKey) => new Server(key, this), + (server, model) => + { + 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 ?? DateTime.MinValue; + server.OwnerId = extendedModel.OwnerId; + server.Presence = extendedModel.Presence; + server.Region = extendedModel.Region; + server.VoiceStates = extendedModel.VoiceStates; + + foreach (var role in extendedModel.Roles) + _roles.Update(role.Id, model.Id, role); + foreach (var channel in extendedModel.Channels) + { + _channels.Update(channel.Id, model.Id, channel); + if (channel.Type == "text") + { + try + { + var messages = DiscordAPI.GetMessages(channel.Id, _httpOptions).Result.OrderBy(x => x.Timestamp); + foreach (var message in messages) + { + var msg = _messages.Update(message.Id, message.ChannelId, message); + if (msg.User != null) + msg.User.UpdateActivity(message.Timestamp); + } + } + catch { } //Bad Permissions? + } + } + foreach (var membership in extendedModel.Members) + { + _users.Update(membership.User.Id, membership.User); + server.AddMember(membership.User.Id); + } + } + }, + server => { } + ); + + _channels = new AsyncCache( + (key, parentKey) => new Channel(key, parentKey, this), + (channel, model) => + { + channel.Name = model.Name; + channel.Type = model.Type; + if (model is ChannelInfo) + { + var extendedModel = model as ChannelInfo; + channel.PermissionOverwrites = extendedModel.PermissionOverwrites; + channel.RecipientId = extendedModel.Recipient?.Id; + } + }, + channel => { }); + _messages = new AsyncCache( + (key, parentKey) => new Message(key, parentKey, this), + (message, model) => + { + if (model is API.Models.Message) + { + var extendedModel = model as API.Models.Message; + message.Attachments = extendedModel.Attachments; + message.Text = extendedModel.Content; + message.Embeds = extendedModel.Embeds; + message.IsMentioningEveryone = extendedModel.IsMentioningEveryone; + message.IsTTS = extendedModel.IsTextToSpeech; + message.UserId = extendedModel.Author.Id; + message.Timestamp = extendedModel.Timestamp; + } + if (model is WebSocketEvents.MessageUpdate) + { + var extendedModel = model as WebSocketEvents.MessageUpdate; + message.Embeds = extendedModel.Embeds; + } + }, + message => { } + ); + _roles = new AsyncCache( + (key, parentKey) => new Role(key, parentKey, this), + (role, model) => + { + role.Permissions = model.Permissions; + }, + role => { } + ); + _users = new AsyncCache( + (key, parentKey) => new User(key, this), + (user, model) => + { + 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; + } + }, + user => { } + ); _webSocket = new DiscordWebSocket(); _webSocket.Connected += (s,e) => RaiseConnected(); @@ -65,14 +185,12 @@ namespace Discord _channels.Clear(); _users.Clear(); - SelfId = data.User.Id; - UpdateUser(data.User); + UserId = data.User.Id; + _users.Update(data.User.Id, data.User); foreach (var server in data.Guilds) - UpdateServer(server); + _servers.Update(server.Id, server); foreach (var channel in data.PrivateChannels) - UpdateChannel(channel as ChannelInfo, null); - - RaiseLoggedIn(); + _channels.Update(channel.Id, null, channel); } break; @@ -80,15 +198,15 @@ namespace Discord case "GUILD_CREATE": { var data = e.Event.ToObject(); - var server = UpdateServer(data); + var server = _servers.Update(data.Id, data); RaiseServerCreated(server); } break; case "GUILD_DELETE": { var data = e.Event.ToObject(); - Server server; - if (_servers.TryRemove(data.Id, out server)) + var server = _servers.Remove(data.Id); + if (server != null) RaiseServerDestroyed(server); } break; @@ -97,52 +215,53 @@ namespace Discord case "CHANNEL_CREATE": { var data = e.Event.ToObject(); - var channel = UpdateChannel(data, null); + var channel = _channels.Update(data.Id, data.GuildId, data); RaiseChannelCreated(channel); } break; - case "CHANNEL_DELETE": - { - var data = e.Event.ToObject(); - var channel = DeleteChannel(data.Id); - RaiseChannelDestroyed(channel); - } - break; case "CHANNEL_UPDATE": { var data = e.Event.ToObject(); - var channel = DeleteChannel(data.Id); + var channel = _channels.Update(data.Id, data.GuildId, data); RaiseChannelUpdated(channel); } break; + case "CHANNEL_DELETE": + { + var data = e.Event.ToObject(); + var channel = _channels.Remove(data.Id); + if (channel != null) + RaiseChannelDestroyed(channel); + } + break; //Members case "GUILD_MEMBER_ADD": { var data = e.Event.ToObject(); - var user = UpdateUser(data.User); - var server = GetServer(data.GuildId); + var user = _users.Update(data.User.Id, data.User); + var server = _servers[data.GuildId]; server._members[user.Id] = true; RaiseMemberAdded(user, server); } break; - case "GUILD_MEMBER_REMOVE": - { - var data = e.Event.ToObject(); - var user = UpdateUser(data.User); - var server = GetServer(data.GuildId); - server._members[user.Id] = true; - RaiseMemberRemoved(user, server); - } - break; case "GUILD_MEMBER_UPDATE": { var data = e.Event.ToObject(); - var user = UpdateUser(data.User); - var server = GetServer(data.GuildId); + var user = _users.Update(data.User.Id, data.User); + var server = _servers[data.GuildId]; RaiseMemberUpdated(user, server); } break; + case "GUILD_MEMBER_REMOVE": + { + var data = e.Event.ToObject(); + var user = _users.Update(data.User.Id, data.User); + var server = _servers[data.GuildId]; + if (server != null && server.RemoveMember(user.Id)) + RaiseMemberRemoved(user, server); + } + break; //Roles case "GUILD_ROLE_CREATE": @@ -152,13 +271,6 @@ namespace Discord RaiseRoleCreated(role); } break; - case "GUILD_ROLE_DELETE": - { - var data = e.Event.ToObject(); - var role = GetRole(data.RoleId, data.GuildId); - RaiseRoleDeleted(role); - } - break; case "GUILD_ROLE_UPDATE": { var data = e.Event.ToObject(); @@ -166,22 +278,31 @@ namespace Discord RaiseRoleUpdated(role); } break; + case "GUILD_ROLE_DELETE": + { + var data = e.Event.ToObject(); + var role = _roles.Remove(data.RoleId); + if (role != null) + RaiseRoleDeleted(role); + } + break; - //Roles + //Bans case "GUILD_BAN_ADD": { var data = e.Event.ToObject(); - var user = UpdateUser(data.User); - var server = GetServer(data.GuildId); + var user = _users.Update(data.User.Id, data.User); + var server = _servers[data.GuildId]; RaiseBanAdded(user, server); } break; case "GUILD_BAN_REMOVE": { var data = e.Event.ToObject(); - var user = UpdateUser(data.User); - var server = GetServer(data.GuildId); - RaiseBanRemoved(user, server); + var user = _users.Update(data.User.Id, data.User); + var server = _servers[data.GuildId]; + if (server != null && server.RemoveBan(user.Id)) + RaiseBanRemoved(user, server); } break; @@ -189,7 +310,7 @@ namespace Discord case "MESSAGE_CREATE": { var data = e.Event.ToObject(); - var msg = UpdateMessage(data); + var msg = _messages.Update(data.Id, data.ChannelId, data); msg.User.UpdateActivity(data.Timestamp); RaiseMessageCreated(msg); } @@ -197,21 +318,21 @@ namespace Discord case "MESSAGE_UPDATE": { var data = e.Event.ToObject(); - var msg = GetMessage(data.Id, data.ChannelId); + var msg = _messages.Update(data.Id, data); RaiseMessageUpdated(msg); } break; case "MESSAGE_DELETE": { var data = e.Event.ToObject(); - var msg = GetMessage(data.MessageId, data.ChannelId); - RaiseMessageDeleted(msg); + var msg = GetMessage(data.MessageId); + _messages.Remove(msg.Id); } break; case "MESSAGE_ACK": { var data = e.Event.ToObject(); - var msg = GetMessage(data.MessageId, data.ChannelId); + var msg = GetMessage(data.MessageId); RaiseMessageAcknowledged(msg); } break; @@ -220,25 +341,36 @@ namespace Discord case "PRESENCE_UPDATE": { var data = e.Event.ToObject(); - var user = UpdateUser(data); + var user = _users.Update(data.Id, data); RaisePresenceUpdated(user); } break; case "VOICE_STATE_UPDATE": { var data = e.Event.ToObject(); - var user = GetUser(data.UserId); //TODO: Don't ignore this + var user = _users[data.UserId]; //TODO: Don't ignore this RaiseVoiceStateUpdated(user); } break; case "TYPING_START": { var data = e.Event.ToObject(); - var channel = GetChannel(data.ChannelId); - var user = GetUser(data.UserId); + var channel = _channels[data.ChannelId]; + var user = _users[data.UserId]; RaiseUserTyping(user, channel); } break; + + //Voice + case "VOICE_SERVER_UPDATE": + { + var data = e.Event.ToObject(); + var server = _servers[data.ServerId]; + RaiseVoiceServerUpdated(server, data.Endpoint); + } + break; + + //Others default: RaiseOnDebugMessage("Unknown WebSocket message type: " + e.Type); break; @@ -247,6 +379,7 @@ namespace Discord _webSocket.OnDebugMessage += (s, e) => RaiseOnDebugMessage(e.Message); } + //Auth public async Task Connect(string email, string password) { _isClosing = false; @@ -271,21 +404,67 @@ namespace Discord _isClosing = false; } - public Task CreateServer(string name, string region) + //Servers + public async Task CreateServer(string name, string region) { CheckReady(); - return DiscordAPI.CreateServer(name, region, _httpOptions); + var response = await DiscordAPI.CreateServer(name, region, _httpOptions); + return _servers.Update(response.Id, response); + } + public Task LeaveServer(Server server) + { + return LeaveServer(server.Id); } - public Task DeleteServer(string id) + public async Task LeaveServer(string id) { CheckReady(); - return DiscordAPI.DeleteServer(id, _httpOptions); + await DiscordAPI.LeaveServer(id, _httpOptions); + return _servers.Remove(id); } - public Task GetInvite(string id) + //Invites + public Task CreateInvite(Server server, int maxAge, int maxUses, bool isTemporary, bool hasXkcdPass) + { + return CreateInvite(server.DefaultChannelId, maxAge, maxUses, isTemporary, hasXkcdPass); + } + public Task CreateInvite(Channel channel, int maxAge, int maxUses, bool isTemporary, bool hasXkcdPass) + { + return CreateInvite(channel, maxAge, maxUses, isTemporary, hasXkcdPass); + } + public async Task CreateInvite(string channelId, int maxAge, int maxUses, bool isTemporary, bool hasXkcdPass) + { + CheckReady(); + var response = await DiscordAPI.CreateInvite(channelId, maxAge, maxUses, isTemporary, hasXkcdPass, _httpOptions); + _channels.Update(response.Channel.Id, response.Server.Id, response.Channel); + _servers.Update(response.Server.Id, response.Server); + _users.Update(response.Inviter.Id, response.Inviter); + return new Invite(response.Code, response.XkcdPass, this) + { + ChannelId = response.Channel.Id, + InviterId = response.Inviter.Id, + ServerId = response.Server.Id, + IsRevoked = response.IsRevoked, + IsTemporary = response.IsTemporary, + MaxAge = response.MaxAge, + MaxUses = response.MaxUses, + Uses = response.Uses + }; + } + public async Task GetInvite(string id) + { + CheckReady(); + var response = await DiscordAPI.GetInvite(id, _httpOptions); + return new Invite(response.Code, response.XkcdPass, this) + { + ChannelId = response.Channel.Id, + InviterId = response.Inviter.Id, + ServerId = response.Server.Id + }; + } + public Task AcceptInvite(Invite invite) { CheckReady(); - return DiscordAPI.GetInvite(id, _httpOptions); + return DiscordAPI.AcceptInvite(invite.Code, _httpOptions); } public async Task AcceptInvite(string id) { @@ -302,6 +481,7 @@ namespace Discord await DiscordAPI.DeleteInvite(response.Code, _httpOptions); } + //Chat public Task SendMessage(string channelId, string text) { return SendMessage(channelId, text, new string[0]); @@ -323,136 +503,27 @@ namespace Discord } } - 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, bool addNew = true) - { - 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; - } - - if (addNew) - _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; + return _servers[id]; } - private Server UpdateServer(ServerInfo model, bool addNew = true) - { - 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, addNew); - server._channels[channel.Id] = true; - } - foreach (var membership in extendedModel.Members) - { - UpdateUser(membership.User, addNew); - server._members[membership.User.Id] = true; - } - } - - if (addNew) - _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; + return _channels[id]; } - private Channel UpdateChannel(ChannelInfo model, string serverId, bool addNew = true) - { - 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; - - if (addNew) - _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) + public User GetUser(string id) { - if (id == null || channelId == null) return null; - return new ChatMessageReference(id, channelId, this); + return _users[id]; } - private ChatMessage UpdateMessage(WebSocketEvents.MessageCreate model, bool addNew = true) + public Models.Message GetMessage(string id) { - return new ChatMessage(model.Id, model.ChannelId, this) - { - Attachments = model.Attachments, - Text = model.Content, - Embeds = model.Embeds, - IsMentioningEveryone = model.IsMentioningEveryone, - IsTTS = model.IsTextToSpeech, - UserId = model.Author.Id, - Timestamp = model.Timestamp - }; + return _messages[id]; } - - private Role GetRole(string id, string serverId) + public Role GetRole(string id) { - if (id == null || serverId == null) return null; - return new Role(id, serverId, this); + return _roles[id]; } + private Role UpdateRole(WebSocketEvents.GuildRoleCreateUpdate role, bool addNew = true) { return new Role(role.Role.Id, role.GuildId, this) @@ -467,5 +538,12 @@ namespace Discord if (!_isReady) throw new InvalidOperationException("The client is not currently connected to Discord"); } + public void Block() + { + //Blocking call for console apps + //TODO: Improve this + while (!_isClosing) + Thread.Sleep(1000); + } } } diff --git a/Discord.Net/DiscordWebSocket.cs b/Discord.Net/DiscordWebSocket.cs index 8bd2b6d80..566ad1980 100644 --- a/Discord.Net/DiscordWebSocket.cs +++ b/Discord.Net/DiscordWebSocket.cs @@ -22,11 +22,13 @@ namespace Discord private ConcurrentQueue _sendQueue; private int _heartbeatInterval; private DateTime _lastHeartbeat; + private AutoResetEvent _connectWaitOnLogin; public async Task ConnectAsync(string url, HttpOptions options) { await DisconnectAsync(); + _connectWaitOnLogin = new AutoResetEvent(false); _sendQueue = new ConcurrentQueue(); _webSocket = new ClientWebSocket(); @@ -62,7 +64,9 @@ namespace Discord msg.Payload.Properties["$referrer"] = ""; msg.Payload.Properties["$referring_domain"] = ""; SendMessage(msg, cancelToken); - } + + _connectWaitOnLogin.WaitOne(); + } public async Task DisconnectAsync() { if (_webSocket != null) @@ -112,6 +116,8 @@ namespace Discord SendMessage(new WebSocketCommands.KeepAlive(), cancelToken); } RaiseGotEvent(msg.Type, msg.Payload as JToken); + if (msg.Type == "READY") + _connectWaitOnLogin.Set(); break; default: RaiseOnDebugMessage("Unknown WebSocket operation ID: " + msg.Operation); diff --git a/Discord.Net/Helpers/AsyncCache.cs b/Discord.Net/Helpers/AsyncCache.cs new file mode 100644 index 000000000..2fefbb985 --- /dev/null +++ b/Discord.Net/Helpers/AsyncCache.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace Discord.Helpers +{ + public class AsyncCache : IEnumerable + where TValue : class + where TModel : class + { + protected readonly ConcurrentDictionary _dictionary; + private readonly Func _onCreate; + private readonly Action _onUpdate; + private readonly Action _onRemove; + + public AsyncCache(Func onCreate, Action onUpdate, Action onRemove) + { + _dictionary = new ConcurrentDictionary(); + _onCreate = onCreate; + _onUpdate = onUpdate; + _onRemove = onRemove; + } + + public TValue this[string key] + { + get + { + if (key == null) + return null; + TValue value = null; + _dictionary.TryGetValue(key, out value); + return value; + } + } + + public TValue Update(string key, TModel model) + { + return Update(key, null, model); + } + public TValue Update(string key, string parentKey, TModel model) + { + if (key == null) + return null; + while (true) + { + bool isNew; + TValue value; + isNew = !_dictionary.TryGetValue(key, out value); + if (isNew) + value = _onCreate(key, parentKey); + _onUpdate(value, model); + if (isNew) + { + //If this fails, repeat as an update instead of an add + if (_dictionary.TryAdd(key, value)) + return value; + } + else + { + _dictionary[key] = value; + return value; + } + } + } + + public TValue Remove(string key) + { + TValue value = null; + if (_dictionary.TryRemove(key, out value)) + return value; + else + return null; + } + + public void Clear() + { + _dictionary.Clear(); + } + + public IEnumerator GetEnumerator() + { + return _dictionary.Values.GetEnumerator(); + } + IEnumerator IEnumerable.GetEnumerator() + { + return _dictionary.Values.GetEnumerator(); + } + } +} diff --git a/Discord.Net/Helpers/Http.cs b/Discord.Net/Helpers/Http.cs index 5a122c017..72775b231 100644 --- a/Discord.Net/Helpers/Http.cs +++ b/Discord.Net/Helpers/Http.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using System; using System.IO; using System.IO.Compression; @@ -24,47 +25,93 @@ namespace Discord.Helpers internal static class Http { private static readonly RequestCachePolicy _cachePolicy = new RequestCachePolicy(RequestCacheLevel.NoCacheNoStore); +#if DEBUG + private const bool _isDebug = true; +#else + private const bool _isDebug = false; +#endif + //GET internal static async Task Get(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(responseJson); + var response = JsonConvert.DeserializeObject(responseJson); +#if DEBUG + CheckResponse(responseJson, response); +#endif + return response; } internal static async Task Get(string path, HttpOptions options) where ResponseT : class { string responseJson = await SendRequest("GET", path, null, options, true); - return JsonConvert.DeserializeObject(responseJson); + var response = JsonConvert.DeserializeObject(responseJson); +#if DEBUG + CheckResponse(responseJson, response); +#endif + return response; } + //POST internal static async Task Post(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(responseJson); + var response = JsonConvert.DeserializeObject(responseJson); +#if DEBUG + CheckResponse(responseJson, response); +#endif + return response; } - internal static Task Post(string path, object data, HttpOptions options) + internal static async Task Post(string path, object data, HttpOptions options) { string requestJson = JsonConvert.SerializeObject(data); - return SendRequest("POST", path, requestJson, options, false); + string responseJson = await SendRequest("POST", path, requestJson, options, _isDebug); +#if DEBUG + CheckEmptyResponse(responseJson); +#endif + return responseJson; } internal static async Task Post(string path, HttpOptions options) where ResponseT : class { string responseJson = await SendRequest("POST", path, null, options, true); - return JsonConvert.DeserializeObject(responseJson); + var response = JsonConvert.DeserializeObject(responseJson); +#if DEBUG + CheckResponse(responseJson, response); +#endif + return response; } - internal static Task Post(string path, HttpOptions options) + internal static async Task Post(string path, HttpOptions options) { - return SendRequest("POST", path, null, options, false); + string responseJson = await SendRequest("POST", path, null, options, _isDebug); +#if DEBUG + CheckEmptyResponse(responseJson); +#endif + return responseJson; } - internal static Task Delete(string path, HttpOptions options) + //DELETE + internal static async Task Delete(string path, HttpOptions options) + where ResponseT : class + { + string responseJson = await SendRequest("DELETE", path, null, options, true); + var response = JsonConvert.DeserializeObject(responseJson); +#if DEBUG + CheckResponse(responseJson, response); +#endif + return response; + } + internal static async Task Delete(string path, HttpOptions options) { - return SendRequest("DELETE", path, null, options, false); + string responseJson = await SendRequest("DELETE", path, null, options, _isDebug); +#if DEBUG + CheckEmptyResponse(responseJson); +#endif + return responseJson; } private static async Task SendRequest(string method, string path, string data, HttpOptions options, bool hasResponse) @@ -124,6 +171,7 @@ namespace Discord.Helpers else return null; } + } private static Stream GetDecoder(string contentEncoding, MemoryStream encodedStream) @@ -138,5 +186,21 @@ namespace Discord.Helpers throw new ArgumentOutOfRangeException("Unknown encoding: " + contentEncoding); } } + +#if DEBUG + private static void CheckResponse(string json, T obj) + { + /*JToken token = JToken.Parse(json); + JToken token2 = JToken.FromObject(obj); + if (!JToken.DeepEquals(token, token2)) + throw new Exception("API check failed: Objects do not match.");*/ + } + + private static void CheckEmptyResponse(string json) + { + if (!string.IsNullOrEmpty(json)) + throw new Exception("API check failed: Response is not empty."); + } +#endif } } diff --git a/Discord.Net/Models/Channel.cs b/Discord.Net/Models/Channel.cs index 23919e192..09e449a80 100644 --- a/Discord.Net/Models/Channel.cs +++ b/Discord.Net/Models/Channel.cs @@ -1,16 +1,19 @@ using Newtonsoft.Json; +using System.Collections.Generic; +using System.Linq; namespace Discord.Models { public sealed class Channel { private readonly DiscordClient _client; - private string _name; public string Id { get; } + + private string _name; public string Name { get { return !IsPrivate ? _name : '@' + Recipient.Name; } internal set { _name = value; } } - public bool IsPrivate { get; internal set; } + public bool IsPrivate { get; } public string Type { get; internal set; } public string ServerId { get; } @@ -21,6 +24,8 @@ namespace Discord.Models public string RecipientId { get; internal set; } public User Recipient { get { return _client.GetUser(RecipientId); } } + public IEnumerable Messages { get { return _client.Messages.Where(x => x.ChannelId == Id); } } + //Not Implemented public object[] PermissionOverwrites { get; internal set; } @@ -28,6 +33,7 @@ namespace Discord.Models { Id = id; ServerId = serverId; + IsPrivate = serverId == null; _client = client; } diff --git a/Discord.Net/Models/ChatMessageReference.cs b/Discord.Net/Models/ChatMessageReference.cs deleted file mode 100644 index d3c800cf3..000000000 --- a/Discord.Net/Models/ChatMessageReference.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.Models -{ - public class ChatMessageReference - { - protected readonly DiscordClient _client; - - public string Id { get; } - - public string ChannelId { get; } - [JsonIgnore] - public Channel Channel { get { return _client.GetChannel(ChannelId); } } - - internal ChatMessageReference(string id, string channelId, DiscordClient client) - { - Id = id; - ChannelId = channelId; - _client = client; - } - } -} diff --git a/Discord.Net/Models/Invite.cs b/Discord.Net/Models/Invite.cs new file mode 100644 index 000000000..972b8f1fe --- /dev/null +++ b/Discord.Net/Models/Invite.cs @@ -0,0 +1,32 @@ +using Newtonsoft.Json; + +namespace Discord.Models +{ + public sealed class Invite + { + private readonly DiscordClient _client; + + public int MaxAge, Uses, MaxUses; + public bool IsRevoked, IsTemporary; + public readonly string Code, XkcdPass; + + public string InviterId { get; internal set; } + [JsonIgnore] + public User Inviter { get { return _client.GetUser(InviterId); } } + + public string ServerId { get; internal set; } + [JsonIgnore] + public Server Server { get { return _client.GetServer(ServerId); } } + + public string ChannelId { get; internal set; } + [JsonIgnore] + public Channel Channel { get { return _client.GetChannel(ChannelId); } } + + internal Invite(string code, string xkcdPass, DiscordClient client) + { + Code = code; + XkcdPass = xkcdPass; + _client = client; + } + } +} diff --git a/Discord.Net/Models/ChatMessage.cs b/Discord.Net/Models/Message.cs similarity index 69% rename from Discord.Net/Models/ChatMessage.cs rename to Discord.Net/Models/Message.cs index 1b8d95d90..462bcaa5f 100644 --- a/Discord.Net/Models/ChatMessage.cs +++ b/Discord.Net/Models/Message.cs @@ -3,8 +3,13 @@ using System; namespace Discord.Models { - public sealed class ChatMessage : ChatMessageReference + public sealed class Message { + private readonly DiscordClient _client; + + public string Id { get; } + public string ChannelId { get; } + public bool IsMentioningEveryone { get; internal set; } public bool IsTTS { get; internal set; } public string Text { get; internal set; } @@ -18,10 +23,12 @@ namespace Discord.Models public object[] Attachments { get; internal set; } public object[] Embeds { get; internal set; } - internal ChatMessage(string id, string channelId, DiscordClient client) - : base(id, channelId, client) + internal Message(string id, string channelId, DiscordClient client) { - } + Id = id; + ChannelId = channelId; + _client = client; + } public override string ToString() { diff --git a/Discord.Net/Models/Server.cs b/Discord.Net/Models/Server.cs index 667f0cc66..36840f8b1 100644 --- a/Discord.Net/Models/Server.cs +++ b/Discord.Net/Models/Server.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using Discord.Helpers; +using Newtonsoft.Json; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -20,20 +21,22 @@ namespace Discord.Models public string OwnerId { get; internal set; } public User Owner { get { return _client.GetUser(OwnerId); } } + public bool IsOwner { get { return _client.UserId == OwnerId; } } + + public string DefaultChannelId { get { return Id; } } + public Channel DefaultChannel { get { return _client.GetChannel(DefaultChannelId); } } internal ConcurrentDictionary _members; - public IEnumerable MemberIds { get { return _members.Keys; } } - [JsonIgnore] public IEnumerable Members { get { return _members.Keys.Select(x => _client.GetUser(x)); } } - internal ConcurrentDictionary _channels; - public IEnumerable ChannelIds { get { return _channels.Keys; } } - [JsonIgnore] - public IEnumerable Channels { get { return _channels.Keys.Select(x => _client.GetChannel(x)); } } + internal ConcurrentDictionary _bans; + public IEnumerable Bans { get { return _bans.Keys.Select(x => _client.GetUser(x)); } } + + public IEnumerable Channels { get { return _client.Channels.Where(x => x.ServerId == Id); } } + public IEnumerable Roles { get { return _client.Roles.Where(x => x.ServerId == Id); } } //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) @@ -41,12 +44,32 @@ namespace Discord.Models Id = id; _client = client; _members = new ConcurrentDictionary(); - _channels = new ConcurrentDictionary(); + _bans = new ConcurrentDictionary(); } public override string ToString() { return Name; } + + internal void AddMember(string id) + { + _members.TryAdd(id, true); + } + internal bool RemoveMember(string id) + { + bool ignored; + return _members.TryRemove(id, out ignored); + } + + internal void AddBan(string id) + { + _bans.TryAdd(id, true); + } + internal bool RemoveBan(string id) + { + bool ignored; + return _bans.TryRemove(id, out ignored); + } } } diff --git a/Discord.Net/Properties/AssemblyInfo.cs b/Discord.Net/Properties/AssemblyInfo.cs index db7498e6c..a375c505f 100644 --- a/Discord.Net/Properties/AssemblyInfo.cs +++ b/Discord.Net/Properties/AssemblyInfo.cs @@ -1,7 +1,7 @@ using System.Reflection; [assembly: AssemblyTitle("Discord.Net")] -[assembly: AssemblyDescription("A .Net API Wrapper for the Discord client")] +[assembly: AssemblyDescription("A .Net API wrapper for the Discord client")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("RogueException")] [assembly: AssemblyProduct("Discord.Net")] @@ -9,5 +9,5 @@ [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] -[assembly: AssemblyVersion("0.1.0.0")] -[assembly: AssemblyFileVersion("0.1.0.0")] +[assembly: AssemblyVersion("0.2.0.0")] +[assembly: AssemblyFileVersion("0.2.0.0")]