| @@ -94,9 +94,6 @@ | |||||
| <Compile Include="..\Discord.Net\API\Enums\PermissionTarget.cs"> | <Compile Include="..\Discord.Net\API\Enums\PermissionTarget.cs"> | ||||
| <Link>API\Enums\PermissionTarget.cs</Link> | <Link>API\Enums\PermissionTarget.cs</Link> | ||||
| </Compile> | </Compile> | ||||
| <Compile Include="..\Discord.Net\API\Enums\Region.cs"> | |||||
| <Link>API\Enums\Region.cs</Link> | |||||
| </Compile> | |||||
| <Compile Include="..\Discord.Net\API\Enums\StringEnum.cs"> | <Compile Include="..\Discord.Net\API\Enums\StringEnum.cs"> | ||||
| <Link>API\Enums\StringEnum.cs</Link> | <Link>API\Enums\StringEnum.cs</Link> | ||||
| </Compile> | </Compile> | ||||
| @@ -259,6 +256,9 @@ | |||||
| <Compile Include="..\Discord.Net\Models\Permissions.cs"> | <Compile Include="..\Discord.Net\Models\Permissions.cs"> | ||||
| <Link>Models\Permissions.cs</Link> | <Link>Models\Permissions.cs</Link> | ||||
| </Compile> | </Compile> | ||||
| <Compile Include="..\Discord.Net\Models\Region.cs"> | |||||
| <Link>Models\Region.cs</Link> | |||||
| </Compile> | |||||
| <Compile Include="..\Discord.Net\Models\Role.cs"> | <Compile Include="..\Discord.Net\Models\Role.cs"> | ||||
| <Link>Models\Role.cs</Link> | <Link>Models\Role.cs</Link> | ||||
| </Compile> | </Compile> | ||||
| @@ -0,0 +1,11 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.API.Converters | |||||
| { | |||||
| public class StringEnumConverter | |||||
| { | |||||
| } | |||||
| } | |||||
| @@ -6,6 +6,8 @@ namespace Discord | |||||
| { | { | ||||
| public class DiscordAPIClientConfig | public class DiscordAPIClientConfig | ||||
| { | { | ||||
| internal static readonly string UserAgent = $"Discord.Net/{DiscordClient.Version} (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> | /// <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 LogMessageSeverity LogLevel { get { return _logLevel; } set { SetValue(ref _logLevel, value); } } | public LogMessageSeverity LogLevel { get { return _logLevel; } set { SetValue(ref _logLevel, value); } } | ||||
| private LogMessageSeverity _logLevel = LogMessageSeverity.Info; | private LogMessageSeverity _logLevel = LogMessageSeverity.Info; | ||||
| @@ -21,15 +23,6 @@ namespace Discord | |||||
| public NetworkCredential ProxyCredentials { get { return _proxyCredentials; } set { SetValue(ref _proxyCredentials, value); } } | public NetworkCredential ProxyCredentials { get { return _proxyCredentials; } set { SetValue(ref _proxyCredentials, value); } } | ||||
| private NetworkCredential _proxyCredentials = null; | private NetworkCredential _proxyCredentials = null; | ||||
| internal string UserAgent | |||||
| { | |||||
| get | |||||
| { | |||||
| string version = typeof(DiscordClientConfig).GetTypeInfo().Assembly.GetName().Version.ToString(2); | |||||
| return $"Discord.Net/{version} (https://github.com/RogueException/Discord.Net)"; | |||||
| } | |||||
| } | |||||
| //Lock | //Lock | ||||
| protected bool _isLocked; | protected bool _isLocked; | ||||
| internal void Lock() { _isLocked = true; } | internal void Lock() { _isLocked = true; } | ||||
| @@ -261,7 +261,7 @@ namespace Discord | |||||
| } | } | ||||
| /// <summary> Deserializes messages from JSON format and imports them into the message cache.</summary> | /// <summary> Deserializes messages from JSON format and imports them into the message cache.</summary> | ||||
| public IEnumerable<Message> ImportMessages(string json) | |||||
| public IEnumerable<Message> ImportMessages(Channel channel, string json) | |||||
| { | { | ||||
| if (json == null) throw new ArgumentNullException(nameof(json)); | if (json == null) throw new ArgumentNullException(nameof(json)); | ||||
| @@ -269,8 +269,8 @@ namespace Discord | |||||
| .Select(x => | .Select(x => | ||||
| { | { | ||||
| var msg = new Message(this, | var msg = new Message(this, | ||||
| x["Id"].Value<long>(), | |||||
| x["ChannelId"].Value<long>(), | |||||
| x["Id"].Value<long>(), | |||||
| channel.Id, | |||||
| x["UserId"].Value<long>()); | x["UserId"].Value<long>()); | ||||
| var reader = x.CreateReader(); | var reader = x.CreateReader(); | ||||
| @@ -86,19 +86,19 @@ namespace Discord | |||||
| if (region == null) throw new ArgumentNullException(nameof(region)); | if (region == null) throw new ArgumentNullException(nameof(region)); | ||||
| CheckReady(); | CheckReady(); | ||||
| var response = await _api.CreateServer(name, region.Value).ConfigureAwait(false); | |||||
| var response = await _api.CreateServer(name, region.Id).ConfigureAwait(false); | |||||
| var server = _servers.GetOrAdd(response.Id); | var server = _servers.GetOrAdd(response.Id); | ||||
| server.Update(response); | server.Update(response); | ||||
| return server; | return server; | ||||
| } | } | ||||
| /// <summary> Edits the provided server, changing only non-null attributes. </summary> | /// <summary> Edits the provided server, changing only non-null attributes. </summary> | ||||
| public async Task EditServer(Server server, string name = null, Region region = null, ImageType iconType = ImageType.Png, byte[] icon = null) | |||||
| public async Task EditServer(Server server, string name = null, string region = null, ImageType iconType = ImageType.Png, byte[] icon = null) | |||||
| { | { | ||||
| if (server == null) throw new ArgumentNullException(nameof(server)); | if (server == null) throw new ArgumentNullException(nameof(server)); | ||||
| CheckReady(); | CheckReady(); | ||||
| var response = await _api.EditServer(server.Id, name: name ?? server.Name, region: region.Value, iconType: iconType, icon: icon).ConfigureAwait(false); | |||||
| var response = await _api.EditServer(server.Id, name: name ?? server.Name, region: region, iconType: iconType, icon: icon).ConfigureAwait(false); | |||||
| server.Update(response); | server.Update(response); | ||||
| } | } | ||||
| @@ -112,5 +112,12 @@ namespace Discord | |||||
| catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } | catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } | ||||
| //return _servers.TryRemove(server.Id); | //return _servers.TryRemove(server.Id); | ||||
| } | } | ||||
| public async Task<IEnumerable<Region>> GetVoiceRegions() | |||||
| { | |||||
| CheckReady(); | |||||
| return (await _api.GetVoiceRegions()).Select(x => new Region { Id = x.Id, Name = x.Name, Hostname = x.Hostname, Port = x.Port }); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -5,6 +5,7 @@ using System; | |||||
| using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Linq; | using System.Linq; | ||||
| using System.Reflection; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| namespace Discord | namespace Discord | ||||
| @@ -12,12 +13,14 @@ namespace Discord | |||||
| /// <summary> Provides a connection to the DiscordApp service. </summary> | /// <summary> Provides a connection to the DiscordApp service. </summary> | ||||
| public sealed partial class DiscordClient : DiscordWSClient | public sealed partial class DiscordClient : DiscordWSClient | ||||
| { | { | ||||
| public static readonly string Version = typeof(DiscordClientConfig).GetTypeInfo().Assembly.GetName().Version.ToString(3); | |||||
| private readonly DiscordAPIClient _api; | private readonly DiscordAPIClient _api; | ||||
| private readonly Random _rand; | private readonly Random _rand; | ||||
| private readonly JsonSerializer _socketSerializer, _messageImporter; | |||||
| private readonly JsonSerializer _messageImporter; | |||||
| private readonly ConcurrentQueue<Message> _pendingMessages; | private readonly ConcurrentQueue<Message> _pendingMessages; | ||||
| private readonly ConcurrentDictionary<long, DiscordWSClient> _voiceClients; | private readonly ConcurrentDictionary<long, DiscordWSClient> _voiceClients; | ||||
| private readonly Dictionary<Type, IService> _services; | |||||
| private readonly Dictionary<Type, object> _singletons; | |||||
| private bool _sentInitialLog; | private bool _sentInitialLog; | ||||
| private uint _nextVoiceClientId; | private uint _nextVoiceClientId; | ||||
| private UserStatus _status; | private UserStatus _status; | ||||
| @@ -47,7 +50,7 @@ namespace Discord | |||||
| _roles = new Roles(this, cacheLock); | _roles = new Roles(this, cacheLock); | ||||
| _servers = new Servers(this, cacheLock); | _servers = new Servers(this, cacheLock); | ||||
| _globalUsers = new GlobalUsers(this, cacheLock); | _globalUsers = new GlobalUsers(this, cacheLock); | ||||
| _services = new Dictionary<Type, IService>(); | |||||
| _singletons = new Dictionary<Type, object>(); | |||||
| _status = UserStatus.Online; | _status = UserStatus.Online; | ||||
| @@ -162,16 +165,9 @@ namespace Discord | |||||
| if (Config.UseMessageQueue) | if (Config.UseMessageQueue) | ||||
| _pendingMessages = new ConcurrentQueue<Message>(); | _pendingMessages = new ConcurrentQueue<Message>(); | ||||
| _socketSerializer = new JsonSerializer(); | |||||
| _socketSerializer.DateTimeZoneHandling = DateTimeZoneHandling.Utc; | |||||
| #if TEST_RESPONSES | |||||
| _serializer.CheckAdditionalContent = true; | |||||
| _serializer.MissingMemberHandling = MissingMemberHandling.Error; | |||||
| #endif | |||||
| _messageImporter = new JsonSerializer(); | _messageImporter = new JsonSerializer(); | ||||
| _messageImporter.ContractResolver = new MessageImporterResolver(); | |||||
| _messageImporter.ContractResolver = new Message.ImportResolver(); | |||||
| } | } | ||||
| internal override VoiceWebSocket CreateVoiceSocket() | internal override VoiceWebSocket CreateVoiceSocket() | ||||
| { | { | ||||
| @@ -276,25 +272,34 @@ namespace Discord | |||||
| _privateUser = null; | _privateUser = null; | ||||
| } | } | ||||
| public T AddSingleton<T>(T obj) | |||||
| where T : class | |||||
| { | |||||
| _singletons.Add(typeof(T), obj); | |||||
| return obj; | |||||
| } | |||||
| public T GetSingleton<T>(bool required = true) | |||||
| where T : class | |||||
| { | |||||
| object singleton; | |||||
| T singletonT = null; | |||||
| if (_singletons.TryGetValue(typeof(T), out singleton)) | |||||
| singletonT = singleton as T; | |||||
| if (singletonT == null && required) | |||||
| throw new InvalidOperationException($"This operation requires {nameof(T)} to be added to {nameof(DiscordClient)}."); | |||||
| return singletonT; | |||||
| } | |||||
| public T AddService<T>(T obj) | public T AddService<T>(T obj) | ||||
| where T : class, IService | where T : class, IService | ||||
| { | { | ||||
| _services.Add(typeof(T), obj); | |||||
| AddSingleton(obj); | |||||
| obj.Install(this); | obj.Install(this); | ||||
| return obj; | return obj; | ||||
| } | } | ||||
| public T GetService<T>(bool required = true) | public T GetService<T>(bool required = true) | ||||
| where T : class, IService | where T : class, IService | ||||
| { | |||||
| IService service; | |||||
| T serviceT = null; | |||||
| if (_services.TryGetValue(typeof(T), out service)) | |||||
| serviceT = service as T; | |||||
| if (serviceT == null && required) | |||||
| throw new InvalidOperationException($"This operation requires {nameof(T)} to be added to {nameof(DiscordClient)}."); | |||||
| return serviceT; | |||||
| } | |||||
| => GetSingleton<T>(required); | |||||
| protected override IEnumerable<Task> GetTasks() | protected override IEnumerable<Task> GetTasks() | ||||
| { | { | ||||
| @@ -314,7 +319,7 @@ namespace Discord | |||||
| case "READY": //Resync | case "READY": //Resync | ||||
| { | { | ||||
| base.OnReceivedEvent(e).Wait(); //This cannot be an await, or we'll get later messages before we're ready | base.OnReceivedEvent(e).Wait(); //This cannot be an await, or we'll get later messages before we're ready | ||||
| var data = e.Payload.ToObject<ReadyEvent>(_socketSerializer); | |||||
| var data = e.Payload.ToObject<ReadyEvent>(_dataSocketSerializer); | |||||
| _privateUser = _users.GetOrAdd(data.User.Id, null); | _privateUser = _users.GetOrAdd(data.User.Id, null); | ||||
| _privateUser.Update(data.User); | _privateUser.Update(data.User); | ||||
| _privateUser.Global.Update(data.User); | _privateUser.Global.Update(data.User); | ||||
| @@ -339,7 +344,7 @@ namespace Discord | |||||
| //Servers | //Servers | ||||
| case "GUILD_CREATE": | case "GUILD_CREATE": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<GuildCreateEvent>(_socketSerializer); | |||||
| var data = e.Payload.ToObject<GuildCreateEvent>(_dataSocketSerializer); | |||||
| if (data.Unavailable != true) | if (data.Unavailable != true) | ||||
| { | { | ||||
| var server = _servers.GetOrAdd(data.Id); | var server = _servers.GetOrAdd(data.Id); | ||||
| @@ -353,7 +358,7 @@ namespace Discord | |||||
| break; | break; | ||||
| case "GUILD_UPDATE": | case "GUILD_UPDATE": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<GuildUpdateEvent>(_socketSerializer); | |||||
| var data = e.Payload.ToObject<GuildUpdateEvent>(_dataSocketSerializer); | |||||
| var server = _servers[data.Id]; | var server = _servers[data.Id]; | ||||
| if (server != null) | if (server != null) | ||||
| { | { | ||||
| @@ -364,7 +369,7 @@ namespace Discord | |||||
| break; | break; | ||||
| case "GUILD_DELETE": | case "GUILD_DELETE": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<GuildDeleteEvent>(_socketSerializer); | |||||
| var data = e.Payload.ToObject<GuildDeleteEvent>(_dataSocketSerializer); | |||||
| var server = _servers.TryRemove(data.Id); | var server = _servers.TryRemove(data.Id); | ||||
| if (server != null) | if (server != null) | ||||
| { | { | ||||
| @@ -379,7 +384,7 @@ namespace Discord | |||||
| //Channels | //Channels | ||||
| case "CHANNEL_CREATE": | case "CHANNEL_CREATE": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<ChannelCreateEvent>(_socketSerializer); | |||||
| var data = e.Payload.ToObject<ChannelCreateEvent>(_dataSocketSerializer); | |||||
| Channel channel; | Channel channel; | ||||
| if (data.IsPrivate) | if (data.IsPrivate) | ||||
| { | { | ||||
| @@ -395,7 +400,7 @@ namespace Discord | |||||
| break; | break; | ||||
| case "CHANNEL_UPDATE": | case "CHANNEL_UPDATE": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<ChannelUpdateEvent>(_socketSerializer); | |||||
| var data = e.Payload.ToObject<ChannelUpdateEvent>(_dataSocketSerializer); | |||||
| var channel = _channels[data.Id]; | var channel = _channels[data.Id]; | ||||
| if (channel != null) | if (channel != null) | ||||
| { | { | ||||
| @@ -406,7 +411,7 @@ namespace Discord | |||||
| break; | break; | ||||
| case "CHANNEL_DELETE": | case "CHANNEL_DELETE": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<ChannelDeleteEvent>(_socketSerializer); | |||||
| var data = e.Payload.ToObject<ChannelDeleteEvent>(_dataSocketSerializer); | |||||
| var channel = _channels.TryRemove(data.Id); | var channel = _channels.TryRemove(data.Id); | ||||
| if (channel != null) | if (channel != null) | ||||
| RaiseChannelDestroyed(channel); | RaiseChannelDestroyed(channel); | ||||
| @@ -416,7 +421,7 @@ namespace Discord | |||||
| //Members | //Members | ||||
| case "GUILD_MEMBER_ADD": | case "GUILD_MEMBER_ADD": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<MemberAddEvent>(_socketSerializer); | |||||
| var data = e.Payload.ToObject<MemberAddEvent>(_dataSocketSerializer); | |||||
| var user = _users.GetOrAdd(data.User.Id, data.GuildId); | var user = _users.GetOrAdd(data.User.Id, data.GuildId); | ||||
| user.Update(data); | user.Update(data); | ||||
| if (Config.TrackActivity) | if (Config.TrackActivity) | ||||
| @@ -426,7 +431,7 @@ namespace Discord | |||||
| break; | break; | ||||
| case "GUILD_MEMBER_UPDATE": | case "GUILD_MEMBER_UPDATE": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<MemberUpdateEvent>(_socketSerializer); | |||||
| var data = e.Payload.ToObject<MemberUpdateEvent>(_dataSocketSerializer); | |||||
| var user = _users[data.User.Id, data.GuildId]; | var user = _users[data.User.Id, data.GuildId]; | ||||
| if (user != null) | if (user != null) | ||||
| { | { | ||||
| @@ -437,7 +442,7 @@ namespace Discord | |||||
| break; | break; | ||||
| case "GUILD_MEMBER_REMOVE": | case "GUILD_MEMBER_REMOVE": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<MemberRemoveEvent>(_socketSerializer); | |||||
| var data = e.Payload.ToObject<MemberRemoveEvent>(_dataSocketSerializer); | |||||
| var user = _users.TryRemove(data.UserId, data.GuildId); | var user = _users.TryRemove(data.UserId, data.GuildId); | ||||
| if (user != null) | if (user != null) | ||||
| RaiseUserLeft(user); | RaiseUserLeft(user); | ||||
| @@ -445,7 +450,7 @@ namespace Discord | |||||
| break; | break; | ||||
| case "GUILD_MEMBERS_CHUNK": | case "GUILD_MEMBERS_CHUNK": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<MembersChunkEvent>(_socketSerializer); | |||||
| var data = e.Payload.ToObject<MembersChunkEvent>(_dataSocketSerializer); | |||||
| 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); | ||||
| @@ -458,7 +463,7 @@ namespace Discord | |||||
| //Roles | //Roles | ||||
| case "GUILD_ROLE_CREATE": | case "GUILD_ROLE_CREATE": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<RoleCreateEvent>(_socketSerializer); | |||||
| var data = e.Payload.ToObject<RoleCreateEvent>(_dataSocketSerializer); | |||||
| 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]; | ||||
| @@ -469,7 +474,7 @@ namespace Discord | |||||
| break; | break; | ||||
| case "GUILD_ROLE_UPDATE": | case "GUILD_ROLE_UPDATE": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<RoleUpdateEvent>(_socketSerializer); | |||||
| var data = e.Payload.ToObject<RoleUpdateEvent>(_dataSocketSerializer); | |||||
| var role = _roles[data.Data.Id]; | var role = _roles[data.Data.Id]; | ||||
| if (role != null) | if (role != null) | ||||
| { | { | ||||
| @@ -480,7 +485,7 @@ namespace Discord | |||||
| break; | break; | ||||
| case "GUILD_ROLE_DELETE": | case "GUILD_ROLE_DELETE": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<RoleDeleteEvent>(_socketSerializer); | |||||
| var data = e.Payload.ToObject<RoleDeleteEvent>(_dataSocketSerializer); | |||||
| var role = _roles.TryRemove(data.RoleId); | var role = _roles.TryRemove(data.RoleId); | ||||
| if (role != null) | if (role != null) | ||||
| { | { | ||||
| @@ -495,7 +500,7 @@ namespace Discord | |||||
| //Bans | //Bans | ||||
| case "GUILD_BAN_ADD": | case "GUILD_BAN_ADD": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<BanAddEvent>(_socketSerializer); | |||||
| var data = e.Payload.ToObject<BanAddEvent>(_dataSocketSerializer); | |||||
| var server = _servers[data.GuildId]; | var server = _servers[data.GuildId]; | ||||
| if (server != null) | if (server != null) | ||||
| { | { | ||||
| @@ -507,7 +512,7 @@ namespace Discord | |||||
| break; | break; | ||||
| case "GUILD_BAN_REMOVE": | case "GUILD_BAN_REMOVE": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<BanRemoveEvent>(_socketSerializer); | |||||
| var data = e.Payload.ToObject<BanRemoveEvent>(_dataSocketSerializer); | |||||
| var server = _servers[data.GuildId]; | var server = _servers[data.GuildId]; | ||||
| if (server != null) | if (server != null) | ||||
| { | { | ||||
| @@ -521,7 +526,7 @@ namespace Discord | |||||
| //Messages | //Messages | ||||
| case "MESSAGE_CREATE": | case "MESSAGE_CREATE": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<MessageCreateEvent>(_socketSerializer); | |||||
| var data = e.Payload.ToObject<MessageCreateEvent>(_dataSocketSerializer); | |||||
| Message msg = null; | Message msg = null; | ||||
| bool isAuthor = data.Author.Id == _userId; | bool isAuthor = data.Author.Id == _userId; | ||||
| @@ -548,7 +553,7 @@ namespace Discord | |||||
| break; | break; | ||||
| case "MESSAGE_UPDATE": | case "MESSAGE_UPDATE": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<MessageUpdateEvent>(_socketSerializer); | |||||
| var data = e.Payload.ToObject<MessageUpdateEvent>(_dataSocketSerializer); | |||||
| var msg = _messages[data.Id]; | var msg = _messages[data.Id]; | ||||
| if (msg != null) | if (msg != null) | ||||
| { | { | ||||
| @@ -559,7 +564,7 @@ namespace Discord | |||||
| break; | break; | ||||
| case "MESSAGE_DELETE": | case "MESSAGE_DELETE": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<MessageDeleteEvent>(_socketSerializer); | |||||
| var data = e.Payload.ToObject<MessageDeleteEvent>(_dataSocketSerializer); | |||||
| var msg = _messages.TryRemove(data.Id); | var msg = _messages.TryRemove(data.Id); | ||||
| if (msg != null) | if (msg != null) | ||||
| RaiseMessageDeleted(msg); | RaiseMessageDeleted(msg); | ||||
| @@ -567,7 +572,7 @@ namespace Discord | |||||
| break; | break; | ||||
| case "MESSAGE_ACK": | case "MESSAGE_ACK": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<MessageAckEvent>(_socketSerializer); | |||||
| var data = e.Payload.ToObject<MessageAckEvent>(_dataSocketSerializer); | |||||
| var msg = GetMessage(data.MessageId); | var msg = GetMessage(data.MessageId); | ||||
| if (msg != null) | if (msg != null) | ||||
| RaiseMessageReadRemotely(msg); | RaiseMessageReadRemotely(msg); | ||||
| @@ -577,7 +582,7 @@ namespace Discord | |||||
| //Statuses | //Statuses | ||||
| case "PRESENCE_UPDATE": | case "PRESENCE_UPDATE": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<PresenceUpdateEvent>(_socketSerializer); | |||||
| var data = e.Payload.ToObject<PresenceUpdateEvent>(_dataSocketSerializer); | |||||
| var user = _users.GetOrAdd(data.User.Id, data.GuildId); | var user = _users.GetOrAdd(data.User.Id, data.GuildId); | ||||
| if (user != null) | if (user != null) | ||||
| { | { | ||||
| @@ -588,7 +593,7 @@ namespace Discord | |||||
| break; | break; | ||||
| case "TYPING_START": | case "TYPING_START": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<TypingStartEvent>(_socketSerializer); | |||||
| var data = e.Payload.ToObject<TypingStartEvent>(_dataSocketSerializer); | |||||
| var channel = _channels[data.ChannelId]; | var channel = _channels[data.ChannelId]; | ||||
| if (channel != null) | if (channel != null) | ||||
| { | { | ||||
| @@ -614,7 +619,7 @@ namespace Discord | |||||
| //Voice | //Voice | ||||
| case "VOICE_STATE_UPDATE": | case "VOICE_STATE_UPDATE": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<MemberVoiceStateUpdateEvent>(_socketSerializer); | |||||
| var data = e.Payload.ToObject<MemberVoiceStateUpdateEvent>(_dataSocketSerializer); | |||||
| var user = _users[data.UserId, data.GuildId]; | var user = _users[data.UserId, data.GuildId]; | ||||
| if (user != null) | if (user != null) | ||||
| { | { | ||||
| @@ -633,7 +638,7 @@ namespace Discord | |||||
| //Settings | //Settings | ||||
| case "USER_UPDATE": | case "USER_UPDATE": | ||||
| { | { | ||||
| var data = e.Payload.ToObject<UserUpdateEvent>(_socketSerializer); | |||||
| var data = e.Payload.ToObject<UserUpdateEvent>(_dataSocketSerializer); | |||||
| var user = _globalUsers[data.Id]; | var user = _globalUsers[data.Id]; | ||||
| if (user != null) | if (user != null) | ||||
| { | { | ||||
| @@ -1,5 +1,6 @@ | |||||
| using Discord.Net; | using Discord.Net; | ||||
| using Discord.Net.WebSockets; | using Discord.Net.WebSockets; | ||||
| using Newtonsoft.Json; | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Linq; | using System.Linq; | ||||
| @@ -23,9 +24,9 @@ namespace Discord | |||||
| protected readonly DiscordWSClientConfig _config; | protected readonly DiscordWSClientConfig _config; | ||||
| protected readonly ManualResetEvent _disconnectedEvent; | protected readonly ManualResetEvent _disconnectedEvent; | ||||
| protected readonly ManualResetEventSlim _connectedEvent; | protected readonly ManualResetEventSlim _connectedEvent; | ||||
| protected ExceptionDispatchInfo _disconnectReason; | |||||
| internal readonly DataWebSocket _dataSocket; | internal readonly DataWebSocket _dataSocket; | ||||
| internal readonly VoiceWebSocket _voiceSocket; | internal readonly VoiceWebSocket _voiceSocket; | ||||
| protected ExceptionDispatchInfo _disconnectReason; | |||||
| protected string _gateway, _token; | protected string _gateway, _token; | ||||
| protected long? _userId, _voiceServerId; | protected long? _userId, _voiceServerId; | ||||
| private Task _runTask; | private Task _runTask; | ||||
| @@ -44,6 +45,10 @@ namespace Discord | |||||
| private CancellationTokenSource _cancelTokenSource; | private CancellationTokenSource _cancelTokenSource; | ||||
| protected CancellationToken _cancelToken; | protected CancellationToken _cancelToken; | ||||
| internal JsonSerializer DataSocketSerializer => _dataSocketSerializer; | |||||
| internal JsonSerializer VoiceSocketSerializer => _voiceSocketSerializer; | |||||
| protected readonly JsonSerializer _dataSocketSerializer, _voiceSocketSerializer; | |||||
| /// <summary> Initializes a new instance of the DiscordClient class. </summary> | /// <summary> Initializes a new instance of the DiscordClient class. </summary> | ||||
| public DiscordWSClient(DiscordWSClientConfig config = null) | public DiscordWSClient(DiscordWSClientConfig config = null) | ||||
| { | { | ||||
| @@ -55,6 +60,32 @@ namespace Discord | |||||
| _disconnectedEvent = new ManualResetEvent(true); | _disconnectedEvent = new ManualResetEvent(true); | ||||
| _connectedEvent = new ManualResetEventSlim(false); | _connectedEvent = new ManualResetEventSlim(false); | ||||
| _dataSocketSerializer = new JsonSerializer(); | |||||
| _dataSocketSerializer.DateTimeZoneHandling = DateTimeZoneHandling.Utc; | |||||
| #if TEST_RESPONSES | |||||
| _dataSocketSerializer.CheckAdditionalContent = true; | |||||
| _dataSocketSerializer.MissingMemberHandling = MissingMemberHandling.Error; | |||||
| #else | |||||
| _dataSocketSerializer.Error += (s, e) => | |||||
| { | |||||
| e.ErrorContext.Handled = true; | |||||
| RaiseOnLog(LogMessageSeverity.Error, LogMessageSource.DataWebSocket, "Serialization Failed", e.ErrorContext.Error); | |||||
| }; | |||||
| #endif | |||||
| _voiceSocketSerializer = new JsonSerializer(); | |||||
| _voiceSocketSerializer.DateTimeZoneHandling = DateTimeZoneHandling.Utc; | |||||
| #if TEST_RESPONSES | |||||
| _voiceSocketSerializer.CheckAdditionalContent = true; | |||||
| _voiceSocketSerializer.MissingMemberHandling = MissingMemberHandling.Error; | |||||
| #else | |||||
| _voiceSocketSerializer.Error += (s, e) => | |||||
| { | |||||
| e.ErrorContext.Handled = true; | |||||
| RaiseOnLog(LogMessageSeverity.Error, LogMessageSource.VoiceWebSocket, "Serialization Failed", e.ErrorContext.Error); | |||||
| }; | |||||
| #endif | |||||
| _dataSocket = CreateDataSocket(); | _dataSocket = CreateDataSocket(); | ||||
| if (_config.EnableVoice) | if (_config.EnableVoice) | ||||
| _voiceSocket = CreateVoiceSocket(); | _voiceSocket = CreateVoiceSocket(); | ||||
| @@ -84,15 +84,21 @@ namespace Discord | |||||
| lock (_writerLock) | lock (_writerLock) | ||||
| { | { | ||||
| TValue newItem = createFunc(); | |||||
| result = _dictionary.GetOrAdd(key, newItem); | |||||
| if (result == newItem) | |||||
| if (!_dictionary.ContainsKey(key)) | |||||
| { | { | ||||
| result.Cache(); | |||||
| RaiseItemCreated(result); | |||||
| result = createFunc(); | |||||
| if (result.Cache()) | |||||
| { | |||||
| _dictionary.TryAdd(key, result); | |||||
| RaiseItemCreated(result); | |||||
| } | |||||
| else | |||||
| result.Uncache(); | |||||
| return result; | |||||
| } | } | ||||
| else | |||||
| return _dictionary[key]; | |||||
| } | } | ||||
| return result; | |||||
| } | } | ||||
| protected void Import(IEnumerable<KeyValuePair<TKey, TValue>> items) | protected void Import(IEnumerable<KeyValuePair<TKey, TValue>> items) | ||||
| { | { | ||||
| @@ -101,9 +107,13 @@ namespace Discord | |||||
| foreach (var pair in items) | foreach (var pair in items) | ||||
| { | { | ||||
| var value = pair.Value; | var value = pair.Value; | ||||
| _dictionary.TryAdd(pair.Key, value); | |||||
| value.Cache(); | |||||
| RaiseItemCreated(value); | |||||
| if (value.Cache()) | |||||
| { | |||||
| _dictionary.TryAdd(pair.Key, value); | |||||
| RaiseItemCreated(value); | |||||
| } | |||||
| else | |||||
| value.Uncache(); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -31,10 +31,14 @@ namespace Discord | |||||
| _client = client; | _client = client; | ||||
| } | } | ||||
| internal void Cache() | |||||
| internal bool Cache() | |||||
| { | { | ||||
| LoadReferences(); | |||||
| _isCached = true; | |||||
| if (LoadReferences()) | |||||
| { | |||||
| _isCached = true; | |||||
| return true; | |||||
| } | |||||
| return false; | |||||
| } | } | ||||
| internal void Uncache() | internal void Uncache() | ||||
| { | { | ||||
| @@ -44,7 +48,7 @@ namespace Discord | |||||
| _isCached = false; | _isCached = false; | ||||
| } | } | ||||
| } | } | ||||
| internal abstract void LoadReferences(); | |||||
| internal abstract bool LoadReferences(); | |||||
| internal abstract void UnloadReferences(); | internal abstract void UnloadReferences(); | ||||
| } | } | ||||
| } | } | ||||
| @@ -41,9 +41,9 @@ namespace Discord | |||||
| } | } | ||||
| } | } | ||||
| public T Load() | |||||
| public bool Load() | |||||
| { | { | ||||
| return Value; //Used for precaching | |||||
| return Value != null; //Used for precaching | |||||
| } | } | ||||
| public void Unload() | public void Unload() | ||||
| @@ -124,12 +124,12 @@ namespace Discord | |||||
| if (client.Config.MessageCacheLength > 0) | if (client.Config.MessageCacheLength > 0) | ||||
| _messages = new ConcurrentDictionary<long, Message>(); | _messages = new ConcurrentDictionary<long, Message>(); | ||||
| } | } | ||||
| internal override void LoadReferences() | |||||
| internal override bool LoadReferences() | |||||
| { | { | ||||
| if (IsPrivate) | if (IsPrivate) | ||||
| _recipient.Load(); | |||||
| return _recipient.Load(); | |||||
| else | else | ||||
| _server.Load(); | |||||
| return _server.Load(); | |||||
| } | } | ||||
| internal override void UnloadReferences() | internal override void UnloadReferences() | ||||
| { | { | ||||
| @@ -44,7 +44,7 @@ namespace Discord | |||||
| { | { | ||||
| _users = new ConcurrentDictionary<long, User>(); | _users = new ConcurrentDictionary<long, User>(); | ||||
| } | } | ||||
| internal override void LoadReferences() { } | |||||
| internal override bool LoadReferences() { return true; } | |||||
| internal override void UnloadReferences() | internal override void UnloadReferences() | ||||
| { | { | ||||
| //Don't need to clean _users - they're considered owned by server | //Don't need to clean _users - they're considered owned by server | ||||
| @@ -82,7 +82,7 @@ namespace Discord | |||||
| { | { | ||||
| XkcdCode = xkcdPass; | XkcdCode = xkcdPass; | ||||
| } | } | ||||
| internal override void LoadReferences() { } | |||||
| internal override bool LoadReferences() { return true; } | |||||
| internal override void UnloadReferences() { } | internal override void UnloadReferences() { } | ||||
| internal void Update(InviteReference model) | internal void Update(InviteReference model) | ||||
| @@ -15,24 +15,25 @@ namespace Discord | |||||
| Queued, | Queued, | ||||
| Failed | Failed | ||||
| } | } | ||||
| internal class MessageImporterResolver : DefaultContractResolver | |||||
| public sealed class Message : CachedObject<long> | |||||
| { | { | ||||
| protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) | |||||
| internal class ImportResolver : DefaultContractResolver | |||||
| { | { | ||||
| var property = base.CreateProperty(member, memberSerialization); | |||||
| if (member is PropertyInfo) | |||||
| protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) | |||||
| { | { | ||||
| if (!(member as PropertyInfo).CanWrite) | |||||
| return null; | |||||
| var property = base.CreateProperty(member, memberSerialization); | |||||
| if (member is PropertyInfo) | |||||
| { | |||||
| if (member.Name == nameof(ChannelId) || !(member as PropertyInfo).CanWrite) | |||||
| return null; | |||||
| property.Writable = true; //Handles private setters | |||||
| property.Writable = true; //Handles private setters | |||||
| } | |||||
| return property; | |||||
| } | } | ||||
| return property; | |||||
| } | } | ||||
| } | |||||
| public sealed class Message : CachedObject<long> | |||||
| { | |||||
| public sealed class Attachment : File | public sealed class Attachment : File | ||||
| { | { | ||||
| /// <summary> Unique identifier for this file. </summary> | /// <summary> Unique identifier for this file. </summary> | ||||
| @@ -173,6 +174,8 @@ namespace Discord | |||||
| x => | x => | ||||
| { | { | ||||
| var channel = Channel; | var channel = Channel; | ||||
| if (channel == null) return null; | |||||
| if (!channel.IsPrivate) | if (!channel.IsPrivate) | ||||
| return _client.Users[x, channel.Server.Id]; | return _client.Users[x, channel.Server.Id]; | ||||
| else | else | ||||
| @@ -181,10 +184,9 @@ namespace Discord | |||||
| Attachments = _initialAttachments; | Attachments = _initialAttachments; | ||||
| Embeds = _initialEmbeds; | Embeds = _initialEmbeds; | ||||
| } | } | ||||
| internal override void LoadReferences() | |||||
| internal override bool LoadReferences() | |||||
| { | { | ||||
| _channel.Load(); | |||||
| _user.Load(); | |||||
| return _channel.Load() && _user.Load(); | |||||
| } | } | ||||
| internal override void UnloadReferences() | internal override void UnloadReferences() | ||||
| { | { | ||||
| @@ -0,0 +1,10 @@ | |||||
| namespace Discord | |||||
| { | |||||
| public sealed class Region | |||||
| { | |||||
| public string Hostname; | |||||
| public int Port; | |||||
| public string Id; | |||||
| public string Name; | |||||
| } | |||||
| } | |||||
| @@ -47,9 +47,9 @@ namespace Discord | |||||
| Color = new Color(0); | Color = new Color(0); | ||||
| Color.Lock(); | Color.Lock(); | ||||
| } | } | ||||
| internal override void LoadReferences() | |||||
| internal override bool LoadReferences() | |||||
| { | { | ||||
| _server.Load(); | |||||
| return _server.Load(); | |||||
| } | } | ||||
| internal override void UnloadReferences() | internal override void UnloadReferences() | ||||
| { | { | ||||
| @@ -103,9 +103,10 @@ namespace Discord | |||||
| //Local Cache | //Local Cache | ||||
| _bans = new ConcurrentDictionary<long, bool>(); | _bans = new ConcurrentDictionary<long, bool>(); | ||||
| } | } | ||||
| internal override void LoadReferences() | |||||
| internal override bool LoadReferences() | |||||
| { | { | ||||
| _afkChannel.Load(); | _afkChannel.Load(); | ||||
| return true; | |||||
| } | } | ||||
| internal override void UnloadReferences() | internal override void UnloadReferences() | ||||
| { | { | ||||
| @@ -148,10 +148,10 @@ namespace Discord | |||||
| if (serverId == null) | if (serverId == null) | ||||
| UpdateRoles(null); | UpdateRoles(null); | ||||
| } | } | ||||
| internal override void LoadReferences() | |||||
| internal override bool LoadReferences() | |||||
| { | { | ||||
| _globalUser.Load(); | |||||
| _server.Load(); | |||||
| return _globalUser.Load() && | |||||
| (IsPrivate || _server.Load()); | |||||
| } | } | ||||
| internal override void UnloadReferences() | internal override void UnloadReferences() | ||||
| { | { | ||||
| @@ -20,7 +20,7 @@ namespace Discord.Net.Rest | |||||
| { | { | ||||
| PreAuthenticate = false, | PreAuthenticate = false, | ||||
| ReadWriteTimeout = _config.APITimeout, | ReadWriteTimeout = _config.APITimeout, | ||||
| UserAgent = _config.UserAgent | |||||
| UserAgent = DiscordAPIClientConfig.UserAgent | |||||
| }; | }; | ||||
| if (_config.ProxyUrl != null) | if (_config.ProxyUrl != null) | ||||
| _client.Proxy = new WebProxy(_config.ProxyUrl, true, new string[0], _config.ProxyCredentials); | _client.Proxy = new WebProxy(_config.ProxyUrl, true, new string[0], _config.ProxyCredentials); | ||||
| @@ -82,13 +82,13 @@ namespace Discord.Net.WebSockets | |||||
| JToken token = msg.Payload as JToken; | JToken token = msg.Payload as JToken; | ||||
| if (msg.Type == "READY") | if (msg.Type == "READY") | ||||
| { | { | ||||
| var payload = token.ToObject<ReadyEvent>(); | |||||
| var payload = token.ToObject<ReadyEvent>(_client.DataSocketSerializer); | |||||
| _sessionId = payload.SessionId; | _sessionId = payload.SessionId; | ||||
| _heartbeatInterval = payload.HeartbeatInterval; | _heartbeatInterval = payload.HeartbeatInterval; | ||||
| } | } | ||||
| else if (msg.Type == "RESUMED") | else if (msg.Type == "RESUMED") | ||||
| { | { | ||||
| var payload = token.ToObject<ResumedEvent>(); | |||||
| var payload = token.ToObject<ResumedEvent>(_client.DataSocketSerializer); | |||||
| _heartbeatInterval = payload.HeartbeatInterval; | _heartbeatInterval = payload.HeartbeatInterval; | ||||
| } | } | ||||
| RaiseReceivedEvent(msg.Type, token); | RaiseReceivedEvent(msg.Type, token); | ||||
| @@ -98,7 +98,7 @@ namespace Discord.Net.WebSockets | |||||
| break; | break; | ||||
| case 7: //Redirect | case 7: //Redirect | ||||
| { | { | ||||
| var payload = (msg.Payload as JToken).ToObject<RedirectEvent>(); | |||||
| var payload = (msg.Payload as JToken).ToObject<RedirectEvent>(_client.DataSocketSerializer); | |||||
| if (payload.Url != null) | if (payload.Url != null) | ||||
| { | { | ||||
| Host = payload.Url; | Host = payload.Url; | ||||
| @@ -443,7 +443,7 @@ namespace Discord.Net.WebSockets | |||||
| { | { | ||||
| if (_state != (int)WebSocketState.Connected) | if (_state != (int)WebSocketState.Connected) | ||||
| { | { | ||||
| var payload = (msg.Payload as JToken).ToObject<VoiceReadyEvent>(); | |||||
| var payload = (msg.Payload as JToken).ToObject<VoiceReadyEvent>(_client.VoiceSocketSerializer); | |||||
| _heartbeatInterval = payload.HeartbeatInterval; | _heartbeatInterval = payload.HeartbeatInterval; | ||||
| _ssrc = payload.SSRC; | _ssrc = payload.SSRC; | ||||
| _endpoint = new IPEndPoint((await Dns.GetHostAddressesAsync(Host.Replace("wss://", "")).ConfigureAwait(false)).FirstOrDefault(), payload.Port); | _endpoint = new IPEndPoint((await Dns.GetHostAddressesAsync(Host.Replace("wss://", "")).ConfigureAwait(false)).FirstOrDefault(), payload.Port); | ||||
| @@ -486,7 +486,7 @@ namespace Discord.Net.WebSockets | |||||
| break; | break; | ||||
| case 4: //SESSION_DESCRIPTION | case 4: //SESSION_DESCRIPTION | ||||
| { | { | ||||
| var payload = (msg.Payload as JToken).ToObject<JoinServerEvent>(); | |||||
| var payload = (msg.Payload as JToken).ToObject<JoinServerEvent>(_client.VoiceSocketSerializer); | |||||
| _secretKey = payload.SecretKey; | _secretKey = payload.SecretKey; | ||||
| SendIsTalking(true); | SendIsTalking(true); | ||||
| EndConnect(); | EndConnect(); | ||||
| @@ -494,7 +494,7 @@ namespace Discord.Net.WebSockets | |||||
| break; | break; | ||||
| case 5: | case 5: | ||||
| { | { | ||||
| var payload = (msg.Payload as JToken).ToObject<IsTalkingEvent>(); | |||||
| var payload = (msg.Payload as JToken).ToObject<IsTalkingEvent>(_client.VoiceSocketSerializer); | |||||
| RaiseIsSpeaking(payload.UserId, payload.IsSpeaking); | RaiseIsSpeaking(payload.UserId, payload.IsSpeaking); | ||||
| } | } | ||||
| break; | break; | ||||