From 7b29abc2bf5e79eac4dfc3181e6c80b01ee0bf14 Mon Sep 17 00:00:00 2001 From: Brandon Smith Date: Tue, 4 Aug 2015 14:34:50 -0300 Subject: [PATCH] Initial Commit - V0.1.0 --- Discord.Net.sln | 28 ++ Discord.Net/API/DiscordAPI.cs | 61 +++ Discord.Net/API/Endpoints.cs | 30 ++ Discord.Net/API/Models/ApiRequests.cs | 66 ++++ Discord.Net/API/Models/General.cs | 131 +++++++ Discord.Net/API/Models/WebSocketCommands.cs | 41 ++ Discord.Net/API/Models/WebSocketEvents.cs | 120 ++++++ Discord.Net/Discord.Net.csproj | 78 ++++ Discord.Net/DiscordClient.Events.cs | 145 +++++++ Discord.Net/DiscordClient.cs | 400 ++++++++++++++++++++ Discord.Net/DiscordWebSocket.Events.cs | 46 +++ Discord.Net/DiscordWebSocket.cs | 189 +++++++++ Discord.Net/Helpers/Http.cs | 142 +++++++ Discord.Net/Models/Channel.cs | 41 ++ Discord.Net/Models/ChatMessage.cs | 31 ++ Discord.Net/Models/ChatMessageReference.cs | 17 + Discord.Net/Models/Server.cs | 53 +++ Discord.Net/Models/User.cs | 40 ++ Discord.Net/Properties/AssemblyInfo.cs | 36 ++ Discord.Net/Region.cs | 32 ++ Discord.Net/packages.config | 4 + 21 files changed, 1731 insertions(+) create mode 100644 Discord.Net.sln create mode 100644 Discord.Net/API/DiscordAPI.cs create mode 100644 Discord.Net/API/Endpoints.cs create mode 100644 Discord.Net/API/Models/ApiRequests.cs create mode 100644 Discord.Net/API/Models/General.cs create mode 100644 Discord.Net/API/Models/WebSocketCommands.cs create mode 100644 Discord.Net/API/Models/WebSocketEvents.cs create mode 100644 Discord.Net/Discord.Net.csproj create mode 100644 Discord.Net/DiscordClient.Events.cs create mode 100644 Discord.Net/DiscordClient.cs create mode 100644 Discord.Net/DiscordWebSocket.Events.cs create mode 100644 Discord.Net/DiscordWebSocket.cs create mode 100644 Discord.Net/Helpers/Http.cs create mode 100644 Discord.Net/Models/Channel.cs create mode 100644 Discord.Net/Models/ChatMessage.cs create mode 100644 Discord.Net/Models/ChatMessageReference.cs create mode 100644 Discord.Net/Models/Server.cs create mode 100644 Discord.Net/Models/User.cs create mode 100644 Discord.Net/Properties/AssemblyInfo.cs create mode 100644 Discord.Net/Region.cs create mode 100644 Discord.Net/packages.config diff --git a/Discord.Net.sln b/Discord.Net.sln new file mode 100644 index 000000000..cdfa8301a --- /dev/null +++ b/Discord.Net.sln @@ -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 diff --git a/Discord.Net/API/DiscordAPI.cs b/Discord.Net/API/DiscordAPI.cs new file mode 100644 index 000000000..5affb594c --- /dev/null +++ b/Discord.Net/API/DiscordAPI.cs @@ -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 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); + return registerResponse; + } + 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); + 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 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); + } + 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); + } + } +} diff --git a/Discord.Net/API/Endpoints.cs b/Discord.Net/API/Endpoints.cs new file mode 100644 index 000000000..bbc8eea7a --- /dev/null +++ b/Discord.Net/API/Endpoints.cs @@ -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"; + + } +} diff --git a/Discord.Net/API/Models/ApiRequests.cs b/Discord.Net/API/Models/ApiRequests.cs new file mode 100644 index 000000000..882491300 --- /dev/null +++ b/Discord.Net/API/Models/ApiRequests.cs @@ -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; + } +} diff --git a/Discord.Net/API/Models/General.cs b/Discord.Net/API/Models/General.cs new file mode 100644 index 000000000..cb3a6fdc8 --- /dev/null +++ b/Discord.Net/API/Models/General.cs @@ -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 : 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(); } 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; + } +} diff --git a/Discord.Net/API/Models/WebSocketCommands.cs b/Discord.Net/API/Models/WebSocketCommands.cs new file mode 100644 index 000000000..641c8ad85 --- /dev/null +++ b/Discord.Net/API/Models/WebSocketCommands.cs @@ -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 + { + 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 + { + public Login() : base(2) { } + public class Data + { + [JsonProperty(PropertyName = "token")] + public string Token; + [JsonProperty(PropertyName = "properties")] + public Dictionary Properties = new Dictionary(); + } + } + internal sealed class UpdateStatus : WebSocketMessage + { + public UpdateStatus() : base(3) { } + public class Data + { + [JsonProperty(PropertyName = "idle_since")] + public string IdleSince; + [JsonProperty(PropertyName = "game_id")] + public string GameId; + } + } + } +} diff --git a/Discord.Net/API/Models/WebSocketEvents.cs b/Discord.Net/API/Models/WebSocketEvents.cs new file mode 100644 index 000000000..085e89ae2 --- /dev/null +++ b/Discord.Net/API/Models/WebSocketEvents.cs @@ -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; + } + } +} diff --git a/Discord.Net/Discord.Net.csproj b/Discord.Net/Discord.Net.csproj new file mode 100644 index 000000000..901b9cd85 --- /dev/null +++ b/Discord.Net/Discord.Net.csproj @@ -0,0 +1,78 @@ + + + + + Debug + AnyCPU + {8D23F61B-723C-4966-859D-1119B28BCF19} + Library + Properties + Discord + Discord.Net + v4.5.2 + 512 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Discord.Net/DiscordClient.Events.cs b/Discord.Net/DiscordClient.Events.cs new file mode 100644 index 000000000..d5f9b78e6 --- /dev/null +++ b/Discord.Net/DiscordClient.Events.cs @@ -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 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 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 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 MessageCreated; + public event EventHandler 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 PresenceUpdated; + private void RaisePresenceUpdated(User user) + { + 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)); + } + } +} diff --git a/Discord.Net/DiscordClient.cs b/Discord.Net/DiscordClient.cs new file mode 100644 index 000000000..881cf12fd --- /dev/null +++ b/Discord.Net/DiscordClient.cs @@ -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 Users { get { return _users.Values; } } + private ConcurrentDictionary _users; + + public IEnumerable Servers { get { return _servers.Values; } } + private ConcurrentDictionary _servers; + + public IEnumerable Channels { get { return _channels.Values; } } + private ConcurrentDictionary _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(); + _servers = new ConcurrentDictionary(); + _channels = new ConcurrentDictionary(); + + _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(); + + _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(); + var server = UpdateServer(data); + RaiseServerCreated(server); + } + break; + case "GUILD_DELETE": + { + var data = e.Event.ToObject(); + Server server; + if (_servers.TryRemove(data.Id, out server)) + RaiseServerDestroyed(server); + } + break; + + //Channels + case "CHANNEL_CREATE": + { + var data = e.Event.ToObject(); + var channel = UpdateChannel(data, null); + RaiseChannelCreated(channel); + } + break; + case "CHANNEL_DELETE": + { + var data = e.Event.ToObject(); + var channel = DeleteChannel(data.Id); + RaiseChannelDestroyed(channel); + } + break; + + //Members + case "GUILD_MEMBER_ADD": + { + var data = e.Event.ToObject(); + 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(); + 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(); + var user = UpdateUser(data); + RaisePresenceUpdated(user); + } + break; + case "VOICE_STATE_UPDATE": + { + var data = e.Event.ToObject(); + var user = GetUser(data.UserId); //TODO: Don't ignore this + RaiseVoiceStateUpdated(user); + } + break; + + //Messages + case "MESSAGE_CREATE": + { + var data = e.Event.ToObject(); + var msg = UpdateMessage(data); + msg.User.UpdateActivity(data.Timestamp); + RaiseMessageCreated(msg); + } + break; + case "MESSAGE_UPDATE": + { + var data = e.Event.ToObject(); + var msg = GetMessage(data.Id, data.ChannelId); + RaiseMessageUpdated(msg); + } + break; + case "MESSAGE_DELETE": + { + var data = e.Event.ToObject(); + var msg = GetMessage(data.MessageId, data.ChannelId); + RaiseMessageDeleted(msg); + } + break; + case "MESSAGE_ACK": + { + var data = e.Event.ToObject(); + var msg = GetMessage(data.MessageId, data.ChannelId); + RaiseMessageAcknowledged(msg); + } + break; + case "TYPING_START": + { + var data = e.Event.ToObject(); + 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 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"); + } + } +} diff --git a/Discord.Net/DiscordWebSocket.Events.cs b/Discord.Net/DiscordWebSocket.Events.cs new file mode 100644 index 000000000..cb3c175bc --- /dev/null +++ b/Discord.Net/DiscordWebSocket.Events.cs @@ -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 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 OnDebugMessage; + private void RaiseOnDebugMessage(string message) + { + if (OnDebugMessage != null) + OnDebugMessage(this, new DiscordClient.LogMessageEventArgs(message)); + } + } +} diff --git a/Discord.Net/DiscordWebSocket.cs b/Discord.Net/DiscordWebSocket.cs new file mode 100644 index 000000000..8bd2b6d80 --- /dev/null +++ b/Discord.Net/DiscordWebSocket.cs @@ -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 _sendQueue; + private int _heartbeatInterval; + private DateTime _lastHeartbeat; + + public async Task ConnectAsync(string url, HttpOptions options) + { + await DisconnectAsync(); + + _sendQueue = new ConcurrentQueue(); + + _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(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(builder.ToString()); + switch (msg.Operation) + { + case 0: + if (msg.Type == "READY") + { + var payload = (msg.Payload as JToken).ToObject(); + _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(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 + } +} diff --git a/Discord.Net/Helpers/Http.cs b/Discord.Net/Helpers/Http.cs new file mode 100644 index 000000000..5a122c017 --- /dev/null +++ b/Discord.Net/Helpers/Http.cs @@ -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 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); + } + 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); + } + + 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); + } + 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 Post(string path, HttpOptions options) + where ResponseT : class + { + string responseJson = await SendRequest("POST", path, null, options, true); + return JsonConvert.DeserializeObject(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 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); + } + } + } +} diff --git a/Discord.Net/Models/Channel.cs b/Discord.Net/Models/Channel.cs new file mode 100644 index 000000000..252d9b88b --- /dev/null +++ b/Discord.Net/Models/Channel.cs @@ -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 + ")"; + } + } +} diff --git a/Discord.Net/Models/ChatMessage.cs b/Discord.Net/Models/ChatMessage.cs new file mode 100644 index 000000000..67e948a74 --- /dev/null +++ b/Discord.Net/Models/ChatMessage.cs @@ -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; + } + } +} diff --git a/Discord.Net/Models/ChatMessageReference.cs b/Discord.Net/Models/ChatMessageReference.cs new file mode 100644 index 000000000..5358c2636 --- /dev/null +++ b/Discord.Net/Models/ChatMessageReference.cs @@ -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; + } + } +} diff --git a/Discord.Net/Models/Server.cs b/Discord.Net/Models/Server.cs new file mode 100644 index 000000000..fadf49187 --- /dev/null +++ b/Discord.Net/Models/Server.cs @@ -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 _members; + [JsonIgnore] + public IEnumerable MemberIds { get { return _members.Keys; } } + public IEnumerable Members { get { return _members.Keys.Select(x => _client.GetUser(x)); } } + + internal ConcurrentDictionary _channels; + [JsonIgnore] + public IEnumerable ChannelIds { get { return _channels.Keys; } } + public IEnumerable 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(); + _channels = new ConcurrentDictionary(); + } + + public override string ToString() + { + return Name; + //return Name + " (" + Id + ")"; + } + } +} diff --git a/Discord.Net/Models/User.cs b/Discord.Net/Models/User.cs new file mode 100644 index 000000000..c913b3222 --- /dev/null +++ b/Discord.Net/Models/User.cs @@ -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 + ")"; + } + } +} diff --git a/Discord.Net/Properties/AssemblyInfo.cs b/Discord.Net/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..35fe96e86 --- /dev/null +++ b/Discord.Net/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("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")] diff --git a/Discord.Net/Region.cs b/Discord.Net/Region.cs new file mode 100644 index 000000000..bbe93767a --- /dev/null +++ b/Discord.Net/Region.cs @@ -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"); + } + } + } +} diff --git a/Discord.Net/packages.config b/Discord.Net/packages.config new file mode 100644 index 000000000..64b5d8efe --- /dev/null +++ b/Discord.Net/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file