diff --git a/src/Discord.Net/API/Common/GuildMember.cs b/src/Discord.Net/API/Common/GuildMember.cs index b97775c81..99f97990f 100644 --- a/src/Discord.Net/API/Common/GuildMember.cs +++ b/src/Discord.Net/API/Common/GuildMember.cs @@ -12,7 +12,7 @@ namespace Discord.API [JsonProperty("roles")] public ulong[] Roles { get; set; } [JsonProperty("joined_at")] - public DateTime JoinedAt { get; set; } + public DateTimeOffset JoinedAt { get; set; } [JsonProperty("deaf")] public bool Deaf { get; set; } [JsonProperty("mute")] diff --git a/src/Discord.Net/API/Common/Integration.cs b/src/Discord.Net/API/Common/Integration.cs index fca50a875..7edd70720 100644 --- a/src/Discord.Net/API/Common/Integration.cs +++ b/src/Discord.Net/API/Common/Integration.cs @@ -26,6 +26,6 @@ namespace Discord.API [JsonProperty("account")] public IntegrationAccount Account { get; set; } [JsonProperty("synced_at")] - public DateTime SyncedAt { get; set; } + public DateTimeOffset SyncedAt { get; set; } } } diff --git a/src/Discord.Net/API/Common/InviteMetadata.cs b/src/Discord.Net/API/Common/InviteMetadata.cs index 55eeebeee..fb46795bb 100644 --- a/src/Discord.Net/API/Common/InviteMetadata.cs +++ b/src/Discord.Net/API/Common/InviteMetadata.cs @@ -16,7 +16,7 @@ namespace Discord.API [JsonProperty("temporary")] public bool Temporary { get; set; } [JsonProperty("created_at")] - public DateTime CreatedAt { get; set; } + public DateTimeOffset CreatedAt { get; set; } [JsonProperty("revoked")] public bool Revoked { get; set; } } diff --git a/src/Discord.Net/API/Common/Message.cs b/src/Discord.Net/API/Common/Message.cs index 52950caf2..3e6d69af9 100644 --- a/src/Discord.Net/API/Common/Message.cs +++ b/src/Discord.Net/API/Common/Message.cs @@ -14,9 +14,9 @@ namespace Discord.API [JsonProperty("content")] public Optional Content { get; set; } [JsonProperty("timestamp")] - public Optional Timestamp { get; set; } + public Optional Timestamp { get; set; } [JsonProperty("edited_timestamp")] - public Optional EditedTimestamp { get; set; } + public Optional EditedTimestamp { get; set; } [JsonProperty("tts")] public Optional IsTextToSpeech { get; set; } [JsonProperty("mention_everyone")] diff --git a/src/Discord.Net/API/Gateway/ExtendedGuild.cs b/src/Discord.Net/API/Gateway/ExtendedGuild.cs index 2d91bde7e..a267295cb 100644 --- a/src/Discord.Net/API/Gateway/ExtendedGuild.cs +++ b/src/Discord.Net/API/Gateway/ExtendedGuild.cs @@ -19,6 +19,6 @@ namespace Discord.API.Gateway [JsonProperty("channels")] public Channel[] Channels { get; set; } [JsonProperty("joined_at")] - public DateTime JoinedAt { get; set; } + public DateTimeOffset JoinedAt { get; set; } } } diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index b7a42e748..2a6e31e85 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -10,7 +10,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -38,6 +37,7 @@ namespace Discord public event Func CurrentUserUpdated; public event Func UserIsTyping; public event Func LatencyUpdated; + //TODO: Add PresenceUpdated? VoiceStateUpdated? private readonly ConcurrentQueue _largeGuilds; private readonly Logger _gatewayLogger; @@ -50,6 +50,7 @@ namespace Discord private readonly bool _enablePreUpdateEvents; private readonly int _largeThreshold; private readonly int _totalShards; + private ConcurrentHashSet _dmChannels; private string _sessionId; private int _lastSeq; private ImmutableDictionary _voiceRegions; @@ -71,20 +72,14 @@ namespace Discord internal DataStore DataStore { get; private set; } internal CachedSelfUser CurrentUser => _currentUser as CachedSelfUser; - internal IReadOnlyCollection Guilds - { - get - { - var guilds = DataStore.Guilds; - return guilds.ToReadOnlyCollection(guilds); - } - } + internal IReadOnlyCollection Guilds => DataStore.Guilds; internal IReadOnlyCollection DMChannels { get { - var users = DataStore.Users; - return users.Select(x => x.DMChannel).Where(x => x != null).ToReadOnlyCollection(users); + var dmChannels = _dmChannels; + var store = DataStore; + return dmChannels.Select(x => store.GetChannel(x) as CachedDMChannel).Where(x => x != null).ToReadOnlyCollection(dmChannels); } } internal IReadOnlyCollection VoiceRegions => _voiceRegions.ToReadOnlyCollection(); @@ -136,6 +131,7 @@ namespace Discord _voiceRegions = ImmutableDictionary.Create(); _largeGuilds = new ConcurrentQueue(); + _dmChannels = new ConcurrentHashSet(); } protected override async Task OnLoginAsync() @@ -305,11 +301,16 @@ namespace Discord { return Task.FromResult(DataStore.GetChannel(id)); } - internal CachedDMChannel AddDMChannel(API.Channel model, DataStore dataStore) + public override Task> GetDMChannelsAsync() + { + return Task.FromResult>(DMChannels); + } + internal CachedDMChannel AddDMChannel(API.Channel model, DataStore dataStore, ConcurrentHashSet dmChannels) { var recipient = GetOrAddUser(model.Recipient.Value, dataStore); var channel = recipient.AddDMChannel(model); dataStore.AddChannel(channel); + dmChannels.TryAdd(model.Id); return channel; } internal CachedDMChannel RemoveDMChannel(ulong id) @@ -317,6 +318,7 @@ namespace Discord var dmChannel = DataStore.RemoveChannel(id) as CachedDMChannel; var recipient = dmChannel.Recipient; recipient.RemoveDMChannel(id); + _dmChannels.TryRemove(id); return dmChannel; } @@ -455,6 +457,7 @@ namespace Discord var data = (payload as JToken).ToObject(_serializer); var dataStore = _dataStoreProvider(ShardId, _totalShards, data.Guilds.Length, data.PrivateChannels.Length); + var dmChannels = new ConcurrentHashSet(); var currentUser = new CachedSelfUser(this, data.User); //dataStore.GetOrAddUser(data.User.Id, _ => currentUser); @@ -462,10 +465,11 @@ namespace Discord for (int i = 0; i < data.Guilds.Length; i++) AddGuild(data.Guilds[i], dataStore); for (int i = 0; i < data.PrivateChannels.Length; i++) - AddDMChannel(data.PrivateChannels[i], dataStore); + AddDMChannel(data.PrivateChannels[i], dataStore, dmChannels); _sessionId = data.SessionId; _currentUser = currentUser; + _dmChannels = dmChannels; DataStore = dataStore; await Ready.RaiseAsync().ConfigureAwait(false); @@ -577,7 +581,7 @@ namespace Discord } } else - channel = AddDMChannel(data, DataStore); + channel = AddDMChannel(data, DataStore, _dmChannels); if (channel != null) await ChannelCreated.RaiseAsync(channel).ConfigureAwait(false); } diff --git a/src/Discord.Net/Entities/Guilds/GuildIntegration.cs b/src/Discord.Net/Entities/Guilds/GuildIntegration.cs index 913536fa6..b0de3518a 100644 --- a/src/Discord.Net/Entities/Guilds/GuildIntegration.cs +++ b/src/Discord.Net/Entities/Guilds/GuildIntegration.cs @@ -9,13 +9,14 @@ namespace Discord [DebuggerDisplay(@"{DebuggerDisplay,nq}")] internal class GuildIntegration : Entity, IGuildIntegration { + private long _syncedAtTicks; + public string Name { get; private set; } public string Type { get; private set; } public bool IsEnabled { get; private set; } public bool IsSyncing { get; private set; } public ulong ExpireBehavior { get; private set; } public ulong ExpireGracePeriod { get; private set; } - public DateTime SyncedAt { get; private set; } public Guild Guild { get; private set; } public Role Role { get; private set; } @@ -23,6 +24,7 @@ namespace Discord public IntegrationAccount Account { get; private set; } public override DiscordClient Discord => Guild.Discord; + public DateTimeOffset SyncedAt => DateTimeUtils.FromTicks(_syncedAtTicks); public GuildIntegration(Guild guild, Model model) : base(model.Id) @@ -41,7 +43,7 @@ namespace Discord IsSyncing = model.Syncing; ExpireBehavior = model.ExpireBehavior; ExpireGracePeriod = model.ExpireGracePeriod; - SyncedAt = model.SyncedAt; + _syncedAtTicks = model.SyncedAt.UtcTicks; Role = Guild.GetRole(model.RoleId); User = new User(Discord, model.User); diff --git a/src/Discord.Net/Entities/Guilds/IGuildIntegration.cs b/src/Discord.Net/Entities/Guilds/IGuildIntegration.cs index e90d8ae76..7f6ed6408 100644 --- a/src/Discord.Net/Entities/Guilds/IGuildIntegration.cs +++ b/src/Discord.Net/Entities/Guilds/IGuildIntegration.cs @@ -2,6 +2,7 @@ namespace Discord { + //TODO: Add docstrings public interface IGuildIntegration { ulong Id { get; } @@ -11,7 +12,7 @@ namespace Discord bool IsSyncing { get; } ulong ExpireBehavior { get; } ulong ExpireGracePeriod { get; } - DateTime SyncedAt { get; } + DateTimeOffset SyncedAt { get; } IntegrationAccount Account { get; } IGuild Guild { get; } diff --git a/src/Discord.Net/Entities/ISnowflakeEntity.cs b/src/Discord.Net/Entities/ISnowflakeEntity.cs index 0f0f890cd..60623425c 100644 --- a/src/Discord.Net/Entities/ISnowflakeEntity.cs +++ b/src/Discord.Net/Entities/ISnowflakeEntity.cs @@ -5,6 +5,6 @@ namespace Discord public interface ISnowflakeEntity : IEntity { /// Gets when this object was created. - DateTime CreatedAt { get; } + DateTimeOffset CreatedAt { get; } } } diff --git a/src/Discord.Net/Entities/Invites/IInviteMetadata.cs b/src/Discord.Net/Entities/Invites/IInviteMetadata.cs index eb897c994..1136e1678 100644 --- a/src/Discord.Net/Entities/Invites/IInviteMetadata.cs +++ b/src/Discord.Net/Entities/Invites/IInviteMetadata.cs @@ -17,6 +17,6 @@ namespace Discord /// Gets the amount of times this invite has been used. int Uses { get; } /// Gets when this invite was created. - DateTime CreatedAt { get; } + DateTimeOffset CreatedAt { get; } } } \ No newline at end of file diff --git a/src/Discord.Net/Entities/Invites/InviteMetadata.cs b/src/Discord.Net/Entities/Invites/InviteMetadata.cs index 2f33efdd4..d62148fd7 100644 --- a/src/Discord.Net/Entities/Invites/InviteMetadata.cs +++ b/src/Discord.Net/Entities/Invites/InviteMetadata.cs @@ -5,14 +5,17 @@ namespace Discord { internal class InviteMetadata : Invite, IInviteMetadata { + private long _createdAtTicks; + public bool IsRevoked { get; private set; } public bool IsTemporary { get; private set; } public int? MaxAge { get; private set; } public int? MaxUses { get; private set; } public int Uses { get; private set; } - public DateTime CreatedAt { get; private set; } public IUser Inviter { get; private set; } + public DateTimeOffset CreatedAt => DateTimeUtils.FromTicks(_createdAtTicks); + public InviteMetadata(DiscordClient client, Model model) : base(client, model) { @@ -28,7 +31,7 @@ namespace Discord MaxAge = model.MaxAge != 0 ? model.MaxAge : (int?)null; MaxUses = model.MaxUses; Uses = model.Uses; - CreatedAt = model.CreatedAt; + _createdAtTicks = model.CreatedAt.UtcTicks; } } } diff --git a/src/Discord.Net/Entities/Messages/IMessage.cs b/src/Discord.Net/Entities/Messages/IMessage.cs index 311eb17d5..0faf0837e 100644 --- a/src/Discord.Net/Entities/Messages/IMessage.cs +++ b/src/Discord.Net/Entities/Messages/IMessage.cs @@ -8,7 +8,7 @@ namespace Discord public interface IMessage : IDeletable, ISnowflakeEntity, IUpdateable { /// Gets the time of this message's last edit, if any. - DateTime? EditedTimestamp { get; } + DateTimeOffset? EditedTimestamp { get; } /// Returns true if this message was sent as a text-to-speech message. bool IsTTS { get; } /// Returns the original, unprocessed text for this message. @@ -16,7 +16,7 @@ namespace Discord /// Returns the text for this message after mention processing. string Text { get; } /// Gets the time this message was sent. - DateTime Timestamp { get; } + DateTimeOffset Timestamp { get; } /// Gets the channel this message was sent to. IMessageChannel Channel { get; } diff --git a/src/Discord.Net/Entities/Messages/Message.cs b/src/Discord.Net/Entities/Messages/Message.cs index c9f66bfe0..e8100070d 100644 --- a/src/Discord.Net/Entities/Messages/Message.cs +++ b/src/Discord.Net/Entities/Messages/Message.cs @@ -12,12 +12,12 @@ namespace Discord internal class Message : SnowflakeEntity, IMessage { private bool _isMentioningEveryone; + private long _timestampTicks; + private long? _editedTimestampTicks; - public DateTime? EditedTimestamp { get; private set; } public bool IsTTS { get; private set; } public string RawText { get; private set; } public string Text { get; private set; } - public DateTime Timestamp { get; private set; } public IMessageChannel Channel { get; } public IUser Author { get; } @@ -29,6 +29,8 @@ namespace Discord public ImmutableArray MentionedUsers { get; private set; } public override DiscordClient Discord => (Channel as Entity).Discord; + public DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks); + public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); public Message(IMessageChannel channel, IUser author, Model model) : base(model.Id) @@ -56,9 +58,9 @@ namespace Discord if (model.IsTextToSpeech.IsSpecified) IsTTS = model.IsTextToSpeech.Value; if (model.Timestamp.IsSpecified) - Timestamp = model.Timestamp.Value; + _timestampTicks = model.Timestamp.Value.UtcTicks; if (model.EditedTimestamp.IsSpecified) - EditedTimestamp = model.EditedTimestamp.Value; + _editedTimestampTicks = model.EditedTimestamp.Value?.UtcTicks; if (model.IsMentioningEveryone.IsSpecified) _isMentioningEveryone = model.IsMentioningEveryone.Value; diff --git a/src/Discord.Net/Entities/SnowflakeEntity.cs b/src/Discord.Net/Entities/SnowflakeEntity.cs index 2c1788f5b..36ed8714d 100644 --- a/src/Discord.Net/Entities/SnowflakeEntity.cs +++ b/src/Discord.Net/Entities/SnowflakeEntity.cs @@ -5,7 +5,7 @@ namespace Discord internal abstract class SnowflakeEntity : Entity, ISnowflakeEntity { //TODO: C#7 Candidate for Extension Property. Lets us remove this class. - public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); public SnowflakeEntity(ulong id) : base(id) diff --git a/src/Discord.Net/Entities/Users/GuildUser.cs b/src/Discord.Net/Entities/Users/GuildUser.cs index cc99fcb25..b0b5cfbdc 100644 --- a/src/Discord.Net/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Entities/Users/GuildUser.cs @@ -14,9 +14,10 @@ namespace Discord [DebuggerDisplay("{DebuggerDisplay,nq}")] internal class GuildUser : IGuildUser, ISnowflakeEntity { + private long? _joinedAtTicks; + public bool IsDeaf { get; private set; } public bool IsMute { get; private set; } - public DateTime? JoinedAt { get; private set; } public string Nickname { get; private set; } public GuildPermissions GuildPermissions { get; private set; } @@ -26,7 +27,7 @@ namespace Discord public ulong Id => User.Id; public string AvatarUrl => User.AvatarUrl; - public DateTime CreatedAt => User.CreatedAt; + public DateTimeOffset CreatedAt => User.CreatedAt; public string Discriminator => User.Discriminator; public bool IsAttached => User.IsAttached; public bool IsBot => User.IsBot; @@ -36,6 +37,7 @@ namespace Discord public virtual Game? Game => User.Game; public DiscordClient Discord => Guild.Discord; + public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); public GuildUser(Guild guild, User user) { @@ -62,7 +64,7 @@ namespace Discord //if (model.Mute.IsSpecified) IsMute = model.Mute; //if (model.JoinedAt.IsSpecified) - JoinedAt = model.JoinedAt; + _joinedAtTicks = model.JoinedAt.UtcTicks; if (model.Nick.IsSpecified) Nickname = model.Nick.Value; diff --git a/src/Discord.Net/Entities/Users/IGuildUser.cs b/src/Discord.Net/Entities/Users/IGuildUser.cs index 424313d30..f536b3ade 100644 --- a/src/Discord.Net/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net/Entities/Users/IGuildUser.cs @@ -13,7 +13,7 @@ namespace Discord /// Returns true if the guild has muted this user. bool IsMute { get; } /// Gets when this user joined this guild. - DateTime? JoinedAt { get; } + DateTimeOffset? JoinedAt { get; } /// Gets the nickname for this user. string Nickname { get; } /// Gets the guild-level permissions granted to this user by their roles. diff --git a/src/Discord.Net/Entities/Users/User.cs b/src/Discord.Net/Entities/Users/User.cs index 70dd158c5..aa5bf8629 100644 --- a/src/Discord.Net/Entities/Users/User.cs +++ b/src/Discord.Net/Entities/Users/User.cs @@ -9,14 +9,15 @@ namespace Discord internal class User : SnowflakeEntity, IUser { private string _avatarId; - - public string Discriminator { get; private set; } + private ushort _discriminator; + public bool IsBot { get; private set; } public string Username { get; private set; } public override DiscordClient Discord { get; } public string AvatarUrl => API.CDN.GetUserAvatarUrl(Id, _avatarId); + public string Discriminator => _discriminator.ToString("D4"); public string Mention => MentionUtils.Mention(this, false); public string NicknameMention => MentionUtils.Mention(this, true); public virtual Game? Game => null; @@ -33,7 +34,7 @@ namespace Discord if (source == UpdateSource.Rest && IsAttached) return; _avatarId = model.Avatar; - Discriminator = model.Discriminator; + _discriminator = ushort.Parse(model.Discriminator); IsBot = model.Bot; Username = model.Username; } diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs index ffe03c962..1c096630f 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs @@ -32,7 +32,15 @@ namespace Discord public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; public CachedGuildUser CurrentUser => GetUser(Discord.CurrentUser.Id); - public IReadOnlyCollection Channels => _channels.Select(x => GetChannel(x)).ToReadOnlyCollection(_channels); + public IReadOnlyCollection Channels + { + get + { + var channels = _channels; + var store = Discord.DataStore; + return channels.Select(x => store.GetChannel(x) as ICachedGuildChannel).Where(x => x != null).ToReadOnlyCollection(channels); + } + } public IReadOnlyCollection Members => _members.ToReadOnlyCollection(); public CachedGuild(DiscordSocketClient discord, ExtendedModel model, DataStore dataStore) : base(discord, model) diff --git a/src/Discord.Net/Extensions/CollectionExtensions.cs b/src/Discord.Net/Extensions/CollectionExtensions.cs index 65785f643..6c81fe9cd 100644 --- a/src/Discord.Net/Extensions/CollectionExtensions.cs +++ b/src/Discord.Net/Extensions/CollectionExtensions.cs @@ -11,12 +11,13 @@ namespace Discord.Extensions public static IReadOnlyCollection ToReadOnlyCollection(this IEnumerable query, IReadOnlyCollection source) => new ConcurrentDictionaryWrapper(source, query); } - + internal struct ConcurrentDictionaryWrapper : IReadOnlyCollection { private readonly IReadOnlyCollection _source; private readonly IEnumerable _query; + //It's okay that this count is affected by race conditions - we're wrapping a concurrent collection and that's to be expected public int Count => _source.Count; public ConcurrentDictionaryWrapper(IReadOnlyCollection source, IEnumerable query) diff --git a/src/Discord.Net/Utilities/DateTimeUtils.cs b/src/Discord.Net/Utilities/DateTimeUtils.cs index 92a42e74b..b3496520c 100644 --- a/src/Discord.Net/Utilities/DateTimeUtils.cs +++ b/src/Discord.Net/Utilities/DateTimeUtils.cs @@ -4,15 +4,12 @@ namespace Discord { internal static class DateTimeUtils { - private const ulong EpochTicks = 621355968000000000UL; - private const ulong DiscordEpochMillis = 1420070400000UL; + public static DateTimeOffset FromSnowflake(ulong value) + => DateTimeOffset.FromUnixTimeMilliseconds((long)((value >> 22) + 1420070400000UL)); - public static DateTime FromEpochMilliseconds(ulong value) - => new DateTime((long)(value * TimeSpan.TicksPerMillisecond + EpochTicks), DateTimeKind.Utc); - public static DateTime FromEpochSeconds(ulong value) - => new DateTime((long)(value * TimeSpan.TicksPerSecond + EpochTicks), DateTimeKind.Utc); - - public static DateTime FromSnowflake(ulong value) - => FromEpochMilliseconds((value >> 22) + DiscordEpochMillis); + public static DateTimeOffset FromTicks(long ticks) + => new DateTimeOffset(ticks, TimeSpan.Zero); + public static DateTimeOffset? FromTicks(long? ticks) + => ticks != null ? new DateTimeOffset(ticks.Value, TimeSpan.Zero) : (DateTimeOffset?)null; } }