| @@ -1,15 +1,32 @@ | |||||
| using Newtonsoft.Json; | |||||
| using System; | |||||
| using System; | |||||
| using Newtonsoft.Json; | |||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| namespace Discord.API.Converters | namespace Discord.API.Converters | ||||
| { | { | ||||
| public class LongStringEnumerableConverter : JsonConverter | |||||
| public class LongStringConverter : JsonConverter | |||||
| { | { | ||||
| public override bool CanConvert(Type objectType) | |||||
| { | |||||
| return objectType == typeof(IEnumerable<ulong>); | |||||
| } | |||||
| public override bool CanConvert(Type objectType) | |||||
| => objectType == typeof(ulong); | |||||
| public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) | |||||
| => IdConvert.ToLong((string)reader.Value); | |||||
| public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) | |||||
| => writer.WriteValue(IdConvert.ToString((ulong)value)); | |||||
| } | |||||
| public class NullableLongStringConverter : JsonConverter | |||||
| { | |||||
| public override bool CanConvert(Type objectType) | |||||
| => objectType == typeof(ulong?); | |||||
| public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) | |||||
| => IdConvert.ToNullableLong((string)reader.Value); | |||||
| public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) | |||||
| => writer.WriteValue(IdConvert.ToString((ulong?)value)); | |||||
| } | |||||
| /*public class LongStringEnumerableConverter : JsonConverter | |||||
| { | |||||
| public override bool CanConvert(Type objectType) => objectType == typeof(IEnumerable<ulong>); | |||||
| public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) | public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) | ||||
| { | { | ||||
| List<ulong> result = new List<ulong>(); | List<ulong> result = new List<ulong>(); | ||||
| @@ -36,14 +53,11 @@ namespace Discord.API.Converters | |||||
| writer.WriteEndArray(); | writer.WriteEndArray(); | ||||
| } | } | ||||
| } | } | ||||
| } | |||||
| }*/ | |||||
| internal class LongStringArrayConverter : JsonConverter | internal class LongStringArrayConverter : JsonConverter | ||||
| { | { | ||||
| public override bool CanConvert(Type objectType) | |||||
| { | |||||
| return objectType == typeof(IEnumerable<ulong[]>); | |||||
| } | |||||
| public override bool CanConvert(Type objectType) => objectType == typeof(IEnumerable<ulong[]>); | |||||
| public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) | public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) | ||||
| { | { | ||||
| var result = new List<ulong>(); | var result = new List<ulong>(); | ||||
| @@ -1,37 +0,0 @@ | |||||
| using System; | |||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API.Converters | |||||
| { | |||||
| public class LongStringConverter : JsonConverter | |||||
| { | |||||
| public override bool CanConvert(Type objectType) | |||||
| { | |||||
| return objectType == typeof(ulong); | |||||
| } | |||||
| public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) | |||||
| { | |||||
| return IdConvert.ToLong((string)reader.Value); | |||||
| } | |||||
| public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) | |||||
| { | |||||
| writer.WriteValue(IdConvert.ToString((ulong)value)); | |||||
| } | |||||
| } | |||||
| public class NullableLongStringConverter : JsonConverter | |||||
| { | |||||
| public override bool CanConvert(Type objectType) | |||||
| { | |||||
| return objectType == typeof(ulong?); | |||||
| } | |||||
| public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) | |||||
| { | |||||
| return IdConvert.ToNullableLong((string)reader.Value); | |||||
| } | |||||
| public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) | |||||
| { | |||||
| writer.WriteValue(IdConvert.ToString((ulong?)value)); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -1,54 +0,0 @@ | |||||
| namespace Discord.API | |||||
| { | |||||
| public static class Endpoints | |||||
| { | |||||
| public const string BaseStatusApi = "https://status.discordapp.com/api/v2/"; | |||||
| public const string BaseApi = "https://discordapp.com/api/"; | |||||
| public const string BaseCdn = "https://cdn.discordapp.com/"; | |||||
| public const string Gateway = "gateway"; | |||||
| public const string Auth = "auth"; | |||||
| public const string AuthLogin = "auth/login"; | |||||
| public const string AuthLogout = "auth/logout"; | |||||
| public const string Channels = "channels"; | |||||
| public static string Channel(ulong channelId) => $"channels/{channelId}"; | |||||
| public static string ChannelInvites(ulong channelId) => $"channels/{channelId}/invites"; | |||||
| public static string ChannelMessages(ulong channelId) => $"channels/{channelId}/messages"; | |||||
| public static string ChannelMessages(ulong channelId, int limit) => $"channels/{channelId}/messages?limit={limit}"; | |||||
| public static string ChannelMessages(ulong channelId, int limit, ulong relativeId, string relativeDir) => $"channels/{channelId}/messages?limit={limit}&{relativeDir}={relativeId}"; | |||||
| public static string ChannelMessage(ulong channelId, ulong msgId) => $"channels/{channelId}/messages/{msgId}"; | |||||
| public static string ChannelMessageAck(ulong channelId, ulong msgId) => $"channels/{channelId}/messages/{msgId}/ack"; | |||||
| public static string ChannelPermission(ulong channelId, ulong userOrRoleId) => $"channels/{channelId}/permissions/{userOrRoleId}"; | |||||
| public static string ChannelTyping(ulong channelId) => $"channels/{channelId}/typing"; | |||||
| public const string Servers = "guilds"; | |||||
| public static string Server(ulong serverId) => $"guilds/{serverId}"; | |||||
| public static string ServerBan(ulong serverId, ulong userId) => $"guilds/{serverId}/bans/{userId}"; | |||||
| public static string ServerChannels(ulong serverId) => $"guilds/{serverId}/channels"; | |||||
| public static string ServerInvites(ulong serverId) => $"guilds/{serverId}/invites"; | |||||
| public static string ServerMember(ulong serverId, ulong userId) => $"guilds/{serverId}/members/{userId}"; | |||||
| public static string ServerPrune(ulong serverId, int days) => $"guilds/{serverId}/prune?days={days}"; | |||||
| public static string ServerRoles(ulong serverId) => $"guilds/{serverId}/roles"; | |||||
| public static string ServerRole(ulong serverId, ulong roleId) => $"guilds/{serverId}/roles/{roleId}"; | |||||
| public static string ServerIcon(ulong serverId, string iconId) => BaseCdn + $"icons/{serverId}/{iconId}.jpg"; | |||||
| public const string Invites = "invite"; | |||||
| public static string Invite(ulong inviteId) => $"invite/{inviteId}"; | |||||
| public static string Invite(string inviteIdOrXkcd) => $"invite/{inviteIdOrXkcd}"; | |||||
| public static string InviteUrl(ulong inviteId) => $"https://discord.gg/{inviteId}"; | |||||
| public static string InviteUrl(string inviteIdOrXkcd) => $"https://discord.gg/{inviteIdOrXkcd}"; | |||||
| public const string Users = "users"; | |||||
| public static string UserMe => $"users/@me"; | |||||
| public static string UserChannels(ulong userId) => $"users/{userId}/channels"; | |||||
| public static string UserAvatar(ulong serverId, string avatarId) => BaseCdn + $"avatars/{serverId}/{avatarId}.jpg"; | |||||
| public const string Voice = "voice"; | |||||
| public const string VoiceRegions = "voice/regions"; | |||||
| public const string StatusActiveMaintenance = "scheduled-maintenances/active.json"; | |||||
| public const string StatusUpcomingMaintenance = "scheduled-maintenances/upcoming.json"; | |||||
| } | |||||
| } | |||||
| @@ -1,293 +0,0 @@ | |||||
| using Discord.API; | |||||
| using Discord.Net.Rest; | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.IO; | |||||
| using System.Linq; | |||||
| using System.Threading; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| public enum RelativeDirection { Before, After } | |||||
| /// <summary> A lightweight wrapper around the Discord API. </summary> | |||||
| public class DiscordAPIClient | |||||
| { | |||||
| public static readonly string Version = DiscordClient.Version; | |||||
| private readonly DiscordConfig _config; | |||||
| internal RestClient RestClient => _rest; | |||||
| private readonly RestClient _rest; | |||||
| public DiscordAPIClient(DiscordConfig config = null, Logger logger = null) | |||||
| { | |||||
| _config = config ?? new DiscordConfig(); | |||||
| _rest = new RestClient(_config, logger); | |||||
| } | |||||
| private string _token; | |||||
| public string Token | |||||
| { | |||||
| get { return _token; } | |||||
| set { _token = value; _rest.SetToken(value); } | |||||
| } | |||||
| private CancellationToken _cancelToken; | |||||
| public CancellationToken CancelToken | |||||
| { | |||||
| get { return _cancelToken; } | |||||
| set { _cancelToken = value; _rest.SetCancelToken(value); } | |||||
| } | |||||
| //Auth | |||||
| public Task<GatewayResponse> Gateway() | |||||
| => _rest.Get<GatewayResponse>(Endpoints.Gateway); | |||||
| public async Task<LoginResponse> Login(string email, string password) | |||||
| { | |||||
| if (email == null) throw new ArgumentNullException(nameof(email)); | |||||
| if (password == null) throw new ArgumentNullException(nameof(password)); | |||||
| var request = new LoginRequest { Email = email, Password = password }; | |||||
| return await _rest.Post<LoginResponse>(Endpoints.AuthLogin, request).ConfigureAwait(false); | |||||
| } | |||||
| public Task Logout() | |||||
| => _rest.Post(Endpoints.AuthLogout); | |||||
| //Channels | |||||
| public Task<CreateChannelResponse> CreateChannel(ulong serverId, string name, string channelType) | |||||
| { | |||||
| if (name == null) throw new ArgumentNullException(nameof(name)); | |||||
| if (channelType == null) throw new ArgumentNullException(nameof(channelType)); | |||||
| var request = new CreateChannelRequest { Name = name, Type = channelType }; | |||||
| return _rest.Post<CreateChannelResponse>(Endpoints.ServerChannels(serverId), request); | |||||
| } | |||||
| public Task<CreateChannelResponse> CreatePMChannel(ulong myId, ulong recipientId) | |||||
| { | |||||
| var request = new CreatePMChannelRequest { RecipientId = recipientId }; | |||||
| return _rest.Post<CreateChannelResponse>(Endpoints.UserChannels(myId), request); | |||||
| } | |||||
| public Task<DestroyChannelResponse> DestroyChannel(ulong channelId) | |||||
| { | |||||
| return _rest.Delete<DestroyChannelResponse>(Endpoints.Channel(channelId)); | |||||
| } | |||||
| public Task<EditChannelResponse> EditChannel(ulong channelId, string name = null, string topic = null) | |||||
| { | |||||
| var request = new EditChannelRequest { Name = name, Topic = topic }; | |||||
| return _rest.Patch<EditChannelResponse>(Endpoints.Channel(channelId), request); | |||||
| } | |||||
| public Task ReorderChannels(ulong serverId, IEnumerable<ulong> channelIds, int startPos = 0) | |||||
| { | |||||
| if (channelIds == null) throw new ArgumentNullException(nameof(channelIds)); | |||||
| if (startPos < 0) throw new ArgumentOutOfRangeException(nameof(startPos), "startPos must be a positive integer."); | |||||
| uint pos = (uint)startPos; | |||||
| var channels = channelIds.Select(x => new ReorderChannelsRequest.Channel { Id = x, Position = pos++ }); | |||||
| var request = new ReorderChannelsRequest(channels); | |||||
| return _rest.Patch(Endpoints.ServerChannels(serverId), request); | |||||
| } | |||||
| public Task<GetMessagesResponse> GetMessages(ulong channelId, int count, ulong? relativeMessageId = null, RelativeDirection relativeDir = RelativeDirection.Before) | |||||
| { | |||||
| if (relativeMessageId != null) | |||||
| return _rest.Get<GetMessagesResponse>(Endpoints.ChannelMessages(channelId, count, relativeMessageId.Value, relativeDir == RelativeDirection.Before ? "before" : "after")); | |||||
| else | |||||
| return _rest.Get<GetMessagesResponse>(Endpoints.ChannelMessages(channelId, count)); | |||||
| } | |||||
| //Incidents | |||||
| public Task<GetIncidentsResponse> GetActiveIncidents() | |||||
| { | |||||
| return _rest.Get<GetIncidentsResponse>(Endpoints.StatusActiveMaintenance); | |||||
| } | |||||
| public Task<GetIncidentsResponse> GetUpcomingIncidents() | |||||
| { | |||||
| return _rest.Get<GetIncidentsResponse>(Endpoints.StatusUpcomingMaintenance); | |||||
| } | |||||
| //Invites | |||||
| public Task<CreateInviteResponse> CreateInvite(ulong channelId, int maxAge, int maxUses, bool tempMembership, bool hasXkcd) | |||||
| { | |||||
| var request = new CreateInviteRequest { MaxAge = maxAge, MaxUses = maxUses, IsTemporary = tempMembership, WithXkcdPass = hasXkcd }; | |||||
| return _rest.Post<CreateInviteResponse>(Endpoints.ChannelInvites(channelId), request); | |||||
| } | |||||
| public Task<GetInviteResponse> GetInvite(string inviteIdOrXkcd) | |||||
| { | |||||
| if (inviteIdOrXkcd == null) throw new ArgumentNullException(nameof(inviteIdOrXkcd)); | |||||
| return _rest.Get<GetInviteResponse>(Endpoints.Invite(inviteIdOrXkcd)); | |||||
| } | |||||
| public Task<GetInvitesResponse> GetInvites(ulong serverId) | |||||
| { | |||||
| return _rest.Get<GetInvitesResponse>(Endpoints.ServerInvites(serverId)); | |||||
| } | |||||
| public Task<AcceptInviteResponse> AcceptInvite(string inviteId) | |||||
| { | |||||
| if (inviteId == null) throw new ArgumentNullException(nameof(inviteId)); | |||||
| return _rest.Post<AcceptInviteResponse>(Endpoints.Invite(inviteId)); | |||||
| } | |||||
| public Task DeleteInvite(string inviteId) | |||||
| { | |||||
| if (inviteId == null) throw new ArgumentNullException(nameof(inviteId)); | |||||
| return _rest.Delete(Endpoints.Invite(inviteId)); | |||||
| } | |||||
| //Users | |||||
| public Task EditUser(ulong serverId, ulong userId, bool? mute = null, bool? deaf = null, ulong? voiceChannelId = null, IEnumerable<ulong> roleIds = null) | |||||
| { | |||||
| var request = new EditMemberRequest { Mute = mute, Deaf = deaf, ChannelId = voiceChannelId, Roles = roleIds }; | |||||
| return _rest.Patch(Endpoints.ServerMember(serverId, userId), request); | |||||
| } | |||||
| public Task KickUser(ulong serverId, ulong userId) | |||||
| { | |||||
| return _rest.Delete(Endpoints.ServerMember(serverId, userId)); | |||||
| } | |||||
| public Task BanUser(ulong serverId, ulong userId) | |||||
| { | |||||
| return _rest.Put(Endpoints.ServerBan(serverId, userId)); | |||||
| } | |||||
| public Task UnbanUser(ulong serverId, ulong userId) | |||||
| { | |||||
| return _rest.Delete(Endpoints.ServerBan(serverId, userId)); | |||||
| } | |||||
| public Task<PruneUsersResponse> PruneUsers(ulong serverId, int days, bool simulate) | |||||
| { | |||||
| if (simulate) | |||||
| return _rest.Get<PruneUsersResponse>(Endpoints.ServerPrune(serverId, days)); | |||||
| else | |||||
| return _rest.Post<PruneUsersResponse>(Endpoints.ServerPrune(serverId, days)); | |||||
| } | |||||
| //Messages | |||||
| public Task<SendMessageResponse> SendMessage(ulong channelId, string message, IEnumerable<ulong> mentionedUserIds = null, string nonce = null, bool isTTS = false) | |||||
| { | |||||
| if (message == null) throw new ArgumentNullException(nameof(message)); | |||||
| var request = new SendMessageRequest { Content = message, Mentions = mentionedUserIds ?? new ulong[0], Nonce = nonce, IsTTS = isTTS ? true : false }; | |||||
| return _rest.Post<SendMessageResponse>(Endpoints.ChannelMessages(channelId), request); | |||||
| } | |||||
| public Task<SendMessageResponse> SendFile(ulong channelId, string filename, Stream stream) | |||||
| { | |||||
| if (filename == null) throw new ArgumentNullException(nameof(filename)); | |||||
| if (stream == null) throw new ArgumentNullException(nameof(stream)); | |||||
| return _rest.PostFile<SendMessageResponse>(Endpoints.ChannelMessages(channelId), filename, stream); | |||||
| } | |||||
| public Task DeleteMessage(ulong messageId, ulong channelId) | |||||
| { | |||||
| return _rest.Delete(Endpoints.ChannelMessage(channelId, messageId)); | |||||
| } | |||||
| public Task<EditMessageResponse> EditMessage(ulong messageId, ulong channelId, string message = null, IEnumerable<ulong> mentionedUserIds = null) | |||||
| { | |||||
| var request = new EditMessageRequest { Content = message, Mentions = mentionedUserIds }; | |||||
| return _rest.Patch<EditMessageResponse>(Endpoints.ChannelMessage(channelId, messageId), request); | |||||
| } | |||||
| public Task AckMessage(ulong messageId, ulong channelId) | |||||
| { | |||||
| return _rest.Post(Endpoints.ChannelMessageAck(channelId, messageId)); | |||||
| } | |||||
| public Task SendIsTyping(ulong channelId) | |||||
| { | |||||
| return _rest.Post(Endpoints.ChannelTyping(channelId)); | |||||
| } | |||||
| //Permissions | |||||
| public Task SetChannelPermissions(ulong channelId, ulong userOrRoleId, string idType, uint allow = 0, uint deny = 0) | |||||
| { | |||||
| if (idType == null) throw new ArgumentNullException(nameof(idType)); | |||||
| var request = new SetChannelPermissionsRequest { Id = userOrRoleId, Type = idType, Allow = allow, Deny = deny }; | |||||
| return _rest.Put(Endpoints.ChannelPermission(channelId, userOrRoleId), request); | |||||
| } | |||||
| public Task DeleteChannelPermissions(ulong channelId, ulong userOrRoleId) | |||||
| { | |||||
| return _rest.Delete(Endpoints.ChannelPermission(channelId, userOrRoleId), null); | |||||
| } | |||||
| //Roles | |||||
| public Task<RoleInfo> CreateRole(ulong serverId) | |||||
| { | |||||
| return _rest.Post<RoleInfo>(Endpoints.ServerRoles(serverId)); | |||||
| } | |||||
| public Task DeleteRole(ulong serverId, ulong roleId) | |||||
| { | |||||
| return _rest.Delete(Endpoints.ServerRole(serverId, roleId)); | |||||
| } | |||||
| public Task<RoleInfo> EditRole(ulong serverId, ulong roleId, string name = null, uint? permissions = null, uint? color = null, bool? hoist = null) | |||||
| { | |||||
| var request = new EditRoleRequest { Name = name, Permissions = permissions, Hoist = hoist, Color = color }; | |||||
| return _rest.Patch<RoleInfo>(Endpoints.ServerRole(serverId, roleId), request); | |||||
| } | |||||
| public Task ReorderRoles(ulong serverId, IEnumerable<ulong> roleIds, int startPos = 0) | |||||
| { | |||||
| if (roleIds == null) throw new ArgumentNullException(nameof(roleIds)); | |||||
| if (startPos < 0) throw new ArgumentOutOfRangeException(nameof(startPos), "startPos must be a positive integer."); | |||||
| uint pos = (uint)startPos; | |||||
| var roles = roleIds.Select(x => new ReorderRolesRequest.Role { Id = x, Position = pos++ }); | |||||
| var request = new ReorderRolesRequest(roles); | |||||
| return _rest.Patch(Endpoints.ServerRoles(serverId), request); | |||||
| } | |||||
| //Servers | |||||
| public Task<CreateServerResponse> CreateServer(string name, string region) | |||||
| { | |||||
| if (name == null) throw new ArgumentNullException(nameof(name)); | |||||
| if (region == null) throw new ArgumentNullException(nameof(region)); | |||||
| var request = new CreateServerRequest { Name = name, Region = region }; | |||||
| return _rest.Post<CreateServerResponse>(Endpoints.Servers, request); | |||||
| } | |||||
| public Task LeaveServer(ulong serverId) | |||||
| { | |||||
| return _rest.Delete<DeleteServerResponse>(Endpoints.Server(serverId)); | |||||
| } | |||||
| public Task<EditServerResponse> EditServer(ulong serverId, string name, string region, | |||||
| Stream icon, ImageType iconType, string existingIcon, | |||||
| ulong? afkChannelId, int afkTimeout) | |||||
| { | |||||
| var request = new EditServerRequest { | |||||
| Name = name, | |||||
| Region = region, | |||||
| Icon = Base64Picture(icon, iconType, existingIcon), | |||||
| AFKChannelId = afkChannelId, | |||||
| AFKTimeout = afkTimeout | |||||
| }; | |||||
| return _rest.Patch<EditServerResponse>(Endpoints.Server(serverId), request); | |||||
| } | |||||
| //User | |||||
| public Task<EditUserResponse> EditProfile(string currentPassword, | |||||
| string username, string email, string password, | |||||
| Stream avatar, ImageType avatarType, string existingAvatar) | |||||
| { | |||||
| if (currentPassword == null) throw new ArgumentNullException(nameof(currentPassword)); | |||||
| var request = new EditUserRequest { CurrentPassword = currentPassword, Username = username, | |||||
| Email = email, Password = password, Avatar = Base64Picture(avatar, avatarType, existingAvatar) }; | |||||
| return _rest.Patch<EditUserResponse>(Endpoints.UserMe, request); | |||||
| } | |||||
| //Voice | |||||
| public Task<GetRegionsResponse> GetVoiceRegions() | |||||
| => _rest.Get<GetRegionsResponse>(Endpoints.VoiceRegions); | |||||
| private string Base64Picture(Stream stream, ImageType type, string existingId) | |||||
| { | |||||
| if (type == ImageType.None) | |||||
| return null; | |||||
| else if (stream != null) | |||||
| { | |||||
| byte[] bytes = new byte[stream.Length - stream.Position]; | |||||
| stream.Read(bytes, 0, bytes.Length); | |||||
| string base64 = Convert.ToBase64String(bytes); | |||||
| string imageType = type == ImageType.Jpeg ? "image/jpeg;base64" : "image/png;base64"; | |||||
| return $"data:{imageType},{base64}"; | |||||
| } | |||||
| return existingId; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -1,13 +1,13 @@ | |||||
| using Discord.API; | |||||
| using Discord.API.Client.GatewaySocket; | |||||
| using Discord.API.Client.Rest; | |||||
| using Discord.Net; | using Discord.Net; | ||||
| using Discord.Net.Rest; | |||||
| using Discord.Net.WebSockets; | using Discord.Net.WebSockets; | ||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| using System; | using System; | ||||
| using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.IO; | using System.IO; | ||||
| using System.Reflection; | |||||
| using System.Runtime.ExceptionServices; | |||||
| using System.Security.Cryptography; | using System.Security.Cryptography; | ||||
| using System.Text; | using System.Text; | ||||
| using System.Threading; | using System.Threading; | ||||
| @@ -53,8 +53,6 @@ namespace Discord | |||||
| /// <summary> Provides a connection to the DiscordApp service. </summary> | /// <summary> Provides a connection to the DiscordApp service. </summary> | ||||
| public partial class DiscordClient | public partial class DiscordClient | ||||
| { | { | ||||
| public static readonly string Version = typeof(DiscordClient).GetTypeInfo().Assembly.GetName().Version.ToString(3); | |||||
| private readonly LogService _log; | private readonly LogService _log; | ||||
| private readonly Logger _logger, _restLogger, _cacheLogger; | private readonly Logger _logger, _restLogger, _cacheLogger; | ||||
| private readonly Dictionary<Type, object> _singletons; | private readonly Dictionary<Type, object> _singletons; | ||||
| @@ -63,7 +61,6 @@ namespace Discord | |||||
| private readonly ManualResetEvent _disconnectedEvent; | private readonly ManualResetEvent _disconnectedEvent; | ||||
| private readonly ManualResetEventSlim _connectedEvent; | private readonly ManualResetEventSlim _connectedEvent; | ||||
| private readonly TaskManager _taskManager; | private readonly TaskManager _taskManager; | ||||
| private bool _sentInitialLog; | |||||
| private UserStatus _status; | private UserStatus _status; | ||||
| private int? _gameId; | private int? _gameId; | ||||
| @@ -76,8 +73,8 @@ namespace Discord | |||||
| private ConnectionState _state; | private ConnectionState _state; | ||||
| /// <summary> Gives direct access to the underlying DiscordAPIClient. This can be used to modify objects not in cache. </summary> | /// <summary> Gives direct access to the underlying DiscordAPIClient. This can be used to modify objects not in cache. </summary> | ||||
| public DiscordAPIClient APIClient => _api; | |||||
| private readonly DiscordAPIClient _api; | |||||
| public RestClient Rest => _rest; | |||||
| private readonly RestClient _rest; | |||||
| /// <summary> Returns the internal websocket object. </summary> | /// <summary> Returns the internal websocket object. </summary> | ||||
| public GatewaySocket WebSocket => _webSocket; | public GatewaySocket WebSocket => _webSocket; | ||||
| @@ -145,8 +142,11 @@ namespace Discord | |||||
| _cacheLogger = CreateCacheLogger(); | _cacheLogger = CreateCacheLogger(); | ||||
| //Networking | //Networking | ||||
| _webSocket = new GatewaySocket(this, _log.CreateLogger("WebSocket")); | |||||
| var settings = new JsonSerializerSettings(); | |||||
| _restLogger = CreateRestLogger(); | |||||
| _rest = new RestClient(_config, _restLogger); | |||||
| var webSocketLogger = _log.CreateLogger("WebSocket"); | |||||
| _webSocket = new GatewaySocket(this, webSocketLogger); | |||||
| _webSocket.Connected += (s, e) => | _webSocket.Connected += (s, e) => | ||||
| { | { | ||||
| if (_state == ConnectionState.Connecting) | if (_state == ConnectionState.Connecting) | ||||
| @@ -156,18 +156,15 @@ namespace Discord | |||||
| { | { | ||||
| RaiseDisconnected(e); | RaiseDisconnected(e); | ||||
| }; | }; | ||||
| _webSocket.ReceivedDispatch += (s, e) => OnReceivedEvent(e); | _webSocket.ReceivedDispatch += (s, e) => OnReceivedEvent(e); | ||||
| _api = new DiscordAPIClient(_config); | |||||
| if (Config.UseMessageQueue) | if (Config.UseMessageQueue) | ||||
| _pendingMessages = new ConcurrentQueue<MessageQueueItem>(); | _pendingMessages = new ConcurrentQueue<MessageQueueItem>(); | ||||
| Connected += async (s, e) => | Connected += async (s, e) => | ||||
| { | { | ||||
| _api.CancelToken = _cancelToken; | |||||
| _rest.SetCancelToken(_cancelToken); | |||||
| await SendStatus().ConfigureAwait(false); | await SendStatus().ConfigureAwait(false); | ||||
| }; | }; | ||||
| _restLogger = CreateRestLogger(); | |||||
| //Import/Export | //Import/Export | ||||
| _messageImporter = new JsonSerializer(); | _messageImporter = new JsonSerializer(); | ||||
| @@ -216,7 +213,7 @@ namespace Discord | |||||
| if (_log.Level >= LogSeverity.Verbose) | if (_log.Level >= LogSeverity.Verbose) | ||||
| { | { | ||||
| logger = _log.CreateLogger("Rest"); | logger = _log.CreateLogger("Rest"); | ||||
| _api.RestClient.OnRequest += (s, e) => | |||||
| _rest.OnRequest += (s, e) => | |||||
| { | { | ||||
| if (e.Payload != null) | if (e.Payload != null) | ||||
| logger.Verbose( $"{e.Method} {e.Path}: {Math.Round(e.ElapsedMilliseconds, 2)} ms ({e.Payload})"); | logger.Verbose( $"{e.Method} {e.Path}: {Math.Round(e.ElapsedMilliseconds, 2)} ms ({e.Payload})"); | ||||
| @@ -280,9 +277,6 @@ namespace Discord | |||||
| _lock.WaitOne(); | _lock.WaitOne(); | ||||
| try | try | ||||
| { | { | ||||
| if (!_sentInitialLog) | |||||
| SendInitialLog(); | |||||
| if (State != ConnectionState.Disconnected) | if (State != ConnectionState.Disconnected) | ||||
| await Disconnect().ConfigureAwait(false); | await Disconnect().ConfigureAwait(false); | ||||
| await _taskManager.Stop().ConfigureAwait(false); | await _taskManager.Stop().ConfigureAwait(false); | ||||
| @@ -347,7 +341,8 @@ namespace Discord | |||||
| token = LoadToken(tokenPath, key); | token = LoadToken(tokenPath, key); | ||||
| if (token == null) | if (token == null) | ||||
| { | { | ||||
| var response = await _api.Login(email, password).ConfigureAwait(false); | |||||
| var request = new LoginRequest() { Email = email, Password = password }; | |||||
| var response = await _rest.Send(request).ConfigureAwait(false); | |||||
| token = response.Token; | token = response.Token; | ||||
| SaveToken(tokenPath, key, token); | SaveToken(tokenPath, key, token); | ||||
| useCache = false; | useCache = false; | ||||
| @@ -355,17 +350,18 @@ namespace Discord | |||||
| } | } | ||||
| else | else | ||||
| { | { | ||||
| var response = await _api.Login(email, password).ConfigureAwait(false); | |||||
| var request = new LoginRequest() { Email = email, Password = password }; | |||||
| var response = await _rest.Send(request).ConfigureAwait(false); | |||||
| token = response.Token; | token = response.Token; | ||||
| } | } | ||||
| } | } | ||||
| _token = token; | _token = token; | ||||
| _api.Token = token; | |||||
| _rest.SetToken(token); | |||||
| //Get gateway and check token | //Get gateway and check token | ||||
| try | try | ||||
| { | { | ||||
| var gatewayResponse = await _api.Gateway().ConfigureAwait(false); | |||||
| var gatewayResponse = await _rest.Send(new GatewayRequest()).ConfigureAwait(false); | |||||
| var gateway = gatewayResponse.Url; | var gateway = gatewayResponse.Url; | ||||
| _gateway = gateway; | _gateway = gateway; | ||||
| if (_config.LogLevel >= LogSeverity.Verbose) | if (_config.LogLevel >= LogSeverity.Verbose) | ||||
| @@ -399,7 +395,7 @@ namespace Discord | |||||
| while (_pendingMessages.TryDequeue(out ignored)) { } | while (_pendingMessages.TryDequeue(out ignored)) { } | ||||
| } | } | ||||
| await _api.Logout().ConfigureAwait(false); | |||||
| await _rest.Send(new LogoutRequest()).ConfigureAwait(false); | |||||
| _channels.Clear(); | _channels.Clear(); | ||||
| _users.Clear(); | _users.Clear(); | ||||
| @@ -528,7 +524,7 @@ namespace Discord | |||||
| //Members | //Members | ||||
| case "GUILD_MEMBER_ADD": | case "GUILD_MEMBER_ADD": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<MemberAddEvent>(_webSocket.Serializer); | |||||
| var data = e.Payload.ToObject<GuildMemberAddEvent>(_webSocket.Serializer); | |||||
| var user = _users.GetOrAdd(data.User.Id, data.GuildId); | var user = _users.GetOrAdd(data.User.Id, data.GuildId); | ||||
| user.Update(data); | user.Update(data); | ||||
| user.UpdateActivity(); | user.UpdateActivity(); | ||||
| @@ -537,7 +533,7 @@ namespace Discord | |||||
| break; | break; | ||||
| case "GUILD_MEMBER_UPDATE": | case "GUILD_MEMBER_UPDATE": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<MemberUpdateEvent>(_webSocket.Serializer); | |||||
| var data = e.Payload.ToObject<GuildMemberUpdateEvent>(_webSocket.Serializer); | |||||
| var user = _users[data.User.Id, data.GuildId]; | var user = _users[data.User.Id, data.GuildId]; | ||||
| if (user != null) | if (user != null) | ||||
| { | { | ||||
| @@ -548,7 +544,7 @@ namespace Discord | |||||
| break; | break; | ||||
| case "GUILD_MEMBER_REMOVE": | case "GUILD_MEMBER_REMOVE": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<MemberRemoveEvent>(_webSocket.Serializer); | |||||
| var data = e.Payload.ToObject<GuildMemberRemoveEvent>(_webSocket.Serializer); | |||||
| var user = _users.TryRemove(data.User.Id, data.GuildId); | var user = _users.TryRemove(data.User.Id, data.GuildId); | ||||
| if (user != null) | if (user != null) | ||||
| RaiseUserLeft(user); | RaiseUserLeft(user); | ||||
| @@ -556,7 +552,7 @@ namespace Discord | |||||
| break; | break; | ||||
| case "GUILD_MEMBERS_CHUNK": | case "GUILD_MEMBERS_CHUNK": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<MembersChunkEvent>(_webSocket.Serializer); | |||||
| var data = e.Payload.ToObject<GuildMembersChunkEvent>(_webSocket.Serializer); | |||||
| foreach (var memberData in data.Members) | foreach (var memberData in data.Members) | ||||
| { | { | ||||
| var user = _users.GetOrAdd(memberData.User.Id, memberData.GuildId); | var user = _users.GetOrAdd(memberData.User.Id, memberData.GuildId); | ||||
| @@ -569,7 +565,7 @@ namespace Discord | |||||
| //Roles | //Roles | ||||
| case "GUILD_ROLE_CREATE": | case "GUILD_ROLE_CREATE": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<RoleCreateEvent>(_webSocket.Serializer); | |||||
| var data = e.Payload.ToObject<GuildRoleCreateEvent>(_webSocket.Serializer); | |||||
| var role = _roles.GetOrAdd(data.Data.Id, data.GuildId); | var role = _roles.GetOrAdd(data.Data.Id, data.GuildId); | ||||
| role.Update(data.Data); | role.Update(data.Data); | ||||
| var server = _servers[data.GuildId]; | var server = _servers[data.GuildId]; | ||||
| @@ -580,7 +576,7 @@ namespace Discord | |||||
| break; | break; | ||||
| case "GUILD_ROLE_UPDATE": | case "GUILD_ROLE_UPDATE": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<RoleUpdateEvent>(_webSocket.Serializer); | |||||
| var data = e.Payload.ToObject<GuildRoleUpdateEvent>(_webSocket.Serializer); | |||||
| var role = _roles[data.Data.Id]; | var role = _roles[data.Data.Id]; | ||||
| if (role != null) | if (role != null) | ||||
| { | { | ||||
| @@ -591,7 +587,7 @@ namespace Discord | |||||
| break; | break; | ||||
| case "GUILD_ROLE_DELETE": | case "GUILD_ROLE_DELETE": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<RoleDeleteEvent>(_webSocket.Serializer); | |||||
| var data = e.Payload.ToObject<GuildRoleDeleteEvent>(_webSocket.Serializer); | |||||
| var role = _roles.TryRemove(data.RoleId); | var role = _roles.TryRemove(data.RoleId); | ||||
| if (role != null) | if (role != null) | ||||
| { | { | ||||
| @@ -606,25 +602,23 @@ namespace Discord | |||||
| //Bans | //Bans | ||||
| case "GUILD_BAN_ADD": | case "GUILD_BAN_ADD": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<BanAddEvent>(_webSocket.Serializer); | |||||
| var data = e.Payload.ToObject<GuildBanAddEvent>(_webSocket.Serializer); | |||||
| var server = _servers[data.GuildId]; | var server = _servers[data.GuildId]; | ||||
| if (server != null) | if (server != null) | ||||
| { | { | ||||
| var id = data.User?.Id ?? data.UserId; | |||||
| server.AddBan(id); | |||||
| RaiseUserBanned(id, server); | |||||
| server.AddBan(data.UserId); | |||||
| RaiseUserBanned(data.UserId, server); | |||||
| } | } | ||||
| } | } | ||||
| break; | break; | ||||
| case "GUILD_BAN_REMOVE": | case "GUILD_BAN_REMOVE": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<BanRemoveEvent>(_webSocket.Serializer); | |||||
| var data = e.Payload.ToObject<GuildBanRemoveEvent>(_webSocket.Serializer); | |||||
| var server = _servers[data.GuildId]; | var server = _servers[data.GuildId]; | ||||
| if (server != null) | if (server != null) | ||||
| { | { | ||||
| var id = data.User?.Id ?? data.UserId; | |||||
| if (server.RemoveBan(id)) | |||||
| RaiseUserUnbanned(id, server); | |||||
| if (server.RemoveBan(data.UserId)) | |||||
| RaiseUserUnbanned(data.UserId, server); | |||||
| } | } | ||||
| } | } | ||||
| break; | break; | ||||
| @@ -689,7 +683,7 @@ namespace Discord | |||||
| case "MESSAGE_ACK": | case "MESSAGE_ACK": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<MessageAckEvent>(_webSocket.Serializer); | var data = e.Payload.ToObject<MessageAckEvent>(_webSocket.Serializer); | ||||
| var msg = GetMessage(data.MessageId); | |||||
| var msg = _messages[data.MessageId]; | |||||
| if (msg != null) | if (msg != null) | ||||
| RaiseMessageAcknowledged(msg); | RaiseMessageAcknowledged(msg); | ||||
| } | } | ||||
| @@ -727,8 +721,8 @@ namespace Discord | |||||
| //Voice | //Voice | ||||
| case "VOICE_STATE_UPDATE": | case "VOICE_STATE_UPDATE": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<MemberVoiceStateUpdateEvent>(_webSocket.Serializer); | |||||
| var user = _users[data.UserId, data.GuildId]; | |||||
| var data = e.Payload.ToObject<VoiceStateUpdateEvent>(_webSocket.Serializer); | |||||
| var user = _users[data.User.Id, data.GuildId]; | |||||
| if (user != null) | if (user != null) | ||||
| { | { | ||||
| /*var voiceChannel = user.VoiceChannel; | /*var voiceChannel = user.VoiceChannel; | ||||
| @@ -779,13 +773,6 @@ namespace Discord | |||||
| } | } | ||||
| } | } | ||||
| private void SendInitialLog() | |||||
| { | |||||
| if (_config.LogLevel >= LogSeverity.Verbose) | |||||
| _logger.Verbose( $"Config: {JsonConvert.SerializeObject(_config)}"); | |||||
| _sentInitialLog = true; | |||||
| } | |||||
| #region Async Wrapper | #region Async Wrapper | ||||
| /// <summary> Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications. </summary> | /// <summary> Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications. </summary> | ||||
| public void Run(Func<Task> asyncAction) | public void Run(Func<Task> asyncAction) | ||||
| @@ -946,5 +933,21 @@ namespace Discord | |||||
| _logger.Warning("Failed to cache token", ex); | _logger.Warning("Failed to cache token", ex); | ||||
| } | } | ||||
| } | } | ||||
| private static string Base64Image(ImageType type, Stream stream, string existingId) | |||||
| { | |||||
| if (type == ImageType.None) | |||||
| return null; | |||||
| else if (stream != null) | |||||
| { | |||||
| byte[] bytes = new byte[stream.Length - stream.Position]; | |||||
| stream.Read(bytes, 0, bytes.Length); | |||||
| string base64 = Convert.ToBase64String(bytes); | |||||
| string imageType = type == ImageType.Jpeg ? "image/jpeg;base64" : "image/png;base64"; | |||||
| return $"data:{imageType},{base64}"; | |||||
| } | |||||
| return existingId; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,5 +1,6 @@ | |||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| using System; | using System; | ||||
| using System.Reflection; | |||||
| using System.Text; | using System.Text; | ||||
| namespace Discord | namespace Discord | ||||
| @@ -35,18 +36,28 @@ namespace Discord | |||||
| public class DiscordConfig : BaseConfig<DiscordConfig> | public class DiscordConfig : BaseConfig<DiscordConfig> | ||||
| { | { | ||||
| //Global | |||||
| public static string LibName => "Discord.Net"; | |||||
| public static string LibVersion => typeof(DiscordClient).GetTypeInfo().Assembly.GetName().Version.ToString(3); | |||||
| public static string LibUrl => "https://github.com/RogueException/Discord.Net"; | |||||
| /// <summary> Specifies the minimum log level severity that will be sent to the LogMessage event. Warning: setting this to debug will really hurt performance but should help investigate any internal issues. </summary> | |||||
| public LogSeverity LogLevel { get { return _logLevel; } set { SetValue(ref _logLevel, value); } } | |||||
| private LogSeverity _logLevel = LogSeverity.Info; | |||||
| public static string ClientAPIUrl => "https://discordapp.com/api/"; | |||||
| public static string StatusAPIUrl => "https://status.discordapp.com/api/v2/"; | |||||
| public static string CDNUrl => "https://cdn.discordapp.com/"; | |||||
| public static string InviteUrl => "https://discord.gg/"; | |||||
| /// <summary> Name of your application. </summary> | |||||
| public string AppName { get { return _appName; } set { SetValue(ref _appName, value); UpdateUserAgent(); } } | |||||
| //Global | |||||
| /// <summary> Name of your application. </summary> | |||||
| public string AppName { get { return _appName; } set { SetValue(ref _appName, value); UpdateUserAgent(); } } | |||||
| private string _appName = null; | private string _appName = null; | ||||
| /// <summary> Version of your application. </summary> | /// <summary> Version of your application. </summary> | ||||
| public string AppVersion { get { return _appVersion; } set { SetValue(ref _appVersion, value); UpdateUserAgent(); } } | public string AppVersion { get { return _appVersion; } set { SetValue(ref _appVersion, value); UpdateUserAgent(); } } | ||||
| private string _appVersion = null; | private string _appVersion = null; | ||||
| /// <summary> Specifies the minimum log level severity that will be sent to the LogMessage event. Warning: setting this to debug will really hurt performance but should help investigate any internal issues. </summary> | |||||
| public LogSeverity LogLevel { get { return _logLevel; } set { SetValue(ref _logLevel, value); } } | |||||
| private LogSeverity _logLevel = LogSeverity.Info; | |||||
| /// <summary> User Agent string to use when connecting to Discord. </summary> | /// <summary> User Agent string to use when connecting to Discord. </summary> | ||||
| [JsonIgnore] | [JsonIgnore] | ||||
| public string UserAgent { get { return _userAgent; } } | public string UserAgent { get { return _userAgent; } } | ||||
| @@ -114,7 +125,7 @@ namespace Discord | |||||
| } | } | ||||
| builder.Append(' '); | builder.Append(' '); | ||||
| } | } | ||||
| builder.Append($"DiscordBot (https://github.com/RogueException/Discord.Net, v{DiscordClient.Version})"); | |||||
| builder.Append($"DiscordBot ({LibUrl}, v{LibVersion})"); | |||||
| _userAgent = builder.ToString(); | _userAgent = builder.ToString(); | ||||
| } | } | ||||
| } | } | ||||
| @@ -106,13 +106,7 @@ namespace Discord.Net.WebSockets | |||||
| RaiseBinaryMessage(e.Data); | RaiseBinaryMessage(e.Data); | ||||
| } | } | ||||
| public IEnumerable<Task> GetTasks(CancellationToken cancelToken) | |||||
| { | |||||
| return new Task[] | |||||
| { | |||||
| SendAsync(cancelToken) | |||||
| }; | |||||
| } | |||||
| public IEnumerable<Task> GetTasks(CancellationToken cancelToken) => new Task[] { SendAsync(cancelToken) }; | |||||
| private Task SendAsync(CancellationToken cancelToken) | private Task SendAsync(CancellationToken cancelToken) | ||||
| { | { | ||||
| @@ -1 +0,0 @@ | |||||
| <StyleCopSettings Version="105" /> | |||||