| @@ -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")] | |||
| @@ -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; } | |||
| } | |||
| } | |||
| @@ -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; } | |||
| } | |||
| @@ -14,9 +14,9 @@ namespace Discord.API | |||
| [JsonProperty("content")] | |||
| public Optional<string> Content { get; set; } | |||
| [JsonProperty("timestamp")] | |||
| public Optional<DateTime> Timestamp { get; set; } | |||
| public Optional<DateTimeOffset> Timestamp { get; set; } | |||
| [JsonProperty("edited_timestamp")] | |||
| public Optional<DateTime?> EditedTimestamp { get; set; } | |||
| public Optional<DateTimeOffset?> EditedTimestamp { get; set; } | |||
| [JsonProperty("tts")] | |||
| public Optional<bool> IsTextToSpeech { get; set; } | |||
| [JsonProperty("mention_everyone")] | |||
| @@ -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; } | |||
| } | |||
| } | |||
| @@ -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<ISelfUser, ISelfUser, Task> CurrentUserUpdated; | |||
| public event Func<IChannel, IUser, Task> UserIsTyping; | |||
| public event Func<int, Task> LatencyUpdated; | |||
| //TODO: Add PresenceUpdated? VoiceStateUpdated? | |||
| private readonly ConcurrentQueue<ulong> _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<ulong> _dmChannels; | |||
| private string _sessionId; | |||
| private int _lastSeq; | |||
| private ImmutableDictionary<string, VoiceRegion> _voiceRegions; | |||
| @@ -71,20 +72,14 @@ namespace Discord | |||
| internal DataStore DataStore { get; private set; } | |||
| internal CachedSelfUser CurrentUser => _currentUser as CachedSelfUser; | |||
| internal IReadOnlyCollection<CachedGuild> Guilds | |||
| { | |||
| get | |||
| { | |||
| var guilds = DataStore.Guilds; | |||
| return guilds.ToReadOnlyCollection(guilds); | |||
| } | |||
| } | |||
| internal IReadOnlyCollection<CachedGuild> Guilds => DataStore.Guilds; | |||
| internal IReadOnlyCollection<CachedDMChannel> 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<VoiceRegion> VoiceRegions => _voiceRegions.ToReadOnlyCollection(); | |||
| @@ -136,6 +131,7 @@ namespace Discord | |||
| _voiceRegions = ImmutableDictionary.Create<string, VoiceRegion>(); | |||
| _largeGuilds = new ConcurrentQueue<ulong>(); | |||
| _dmChannels = new ConcurrentHashSet<ulong>(); | |||
| } | |||
| protected override async Task OnLoginAsync() | |||
| @@ -305,11 +301,16 @@ namespace Discord | |||
| { | |||
| return Task.FromResult<IChannel>(DataStore.GetChannel(id)); | |||
| } | |||
| internal CachedDMChannel AddDMChannel(API.Channel model, DataStore dataStore) | |||
| public override Task<IReadOnlyCollection<IDMChannel>> GetDMChannelsAsync() | |||
| { | |||
| return Task.FromResult<IReadOnlyCollection<IDMChannel>>(DMChannels); | |||
| } | |||
| internal CachedDMChannel AddDMChannel(API.Channel model, DataStore dataStore, ConcurrentHashSet<ulong> 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<ReadyEvent>(_serializer); | |||
| var dataStore = _dataStoreProvider(ShardId, _totalShards, data.Guilds.Length, data.PrivateChannels.Length); | |||
| var dmChannels = new ConcurrentHashSet<ulong>(); | |||
| 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); | |||
| } | |||
| @@ -9,13 +9,14 @@ namespace Discord | |||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
| internal class GuildIntegration : Entity<ulong>, 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); | |||
| @@ -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; } | |||
| @@ -5,6 +5,6 @@ namespace Discord | |||
| public interface ISnowflakeEntity : IEntity<ulong> | |||
| { | |||
| /// <summary> Gets when this object was created. </summary> | |||
| DateTime CreatedAt { get; } | |||
| DateTimeOffset CreatedAt { get; } | |||
| } | |||
| } | |||
| @@ -17,6 +17,6 @@ namespace Discord | |||
| /// <summary> Gets the amount of times this invite has been used. </summary> | |||
| int Uses { get; } | |||
| /// <summary> Gets when this invite was created. </summary> | |||
| DateTime CreatedAt { get; } | |||
| DateTimeOffset CreatedAt { get; } | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| } | |||
| @@ -8,7 +8,7 @@ namespace Discord | |||
| public interface IMessage : IDeletable, ISnowflakeEntity, IUpdateable | |||
| { | |||
| /// <summary> Gets the time of this message's last edit, if any. </summary> | |||
| DateTime? EditedTimestamp { get; } | |||
| DateTimeOffset? EditedTimestamp { get; } | |||
| /// <summary> Returns true if this message was sent as a text-to-speech message. </summary> | |||
| bool IsTTS { get; } | |||
| /// <summary> Returns the original, unprocessed text for this message. </summary> | |||
| @@ -16,7 +16,7 @@ namespace Discord | |||
| /// <summary> Returns the text for this message after mention processing. </summary> | |||
| string Text { get; } | |||
| /// <summary> Gets the time this message was sent. </summary> | |||
| DateTime Timestamp { get; } | |||
| DateTimeOffset Timestamp { get; } | |||
| /// <summary> Gets the channel this message was sent to. </summary> | |||
| IMessageChannel Channel { get; } | |||
| @@ -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<User> MentionedUsers { get; private set; } | |||
| public override DiscordClient Discord => (Channel as Entity<ulong>).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; | |||
| @@ -5,7 +5,7 @@ namespace Discord | |||
| internal abstract class SnowflakeEntity : Entity<ulong>, 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) | |||
| @@ -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; | |||
| @@ -13,7 +13,7 @@ namespace Discord | |||
| /// <summary> Returns true if the guild has muted this user. </summary> | |||
| bool IsMute { get; } | |||
| /// <summary> Gets when this user joined this guild. </summary> | |||
| DateTime? JoinedAt { get; } | |||
| DateTimeOffset? JoinedAt { get; } | |||
| /// <summary> Gets the nickname for this user. </summary> | |||
| string Nickname { get; } | |||
| /// <summary> Gets the guild-level permissions granted to this user by their roles. </summary> | |||
| @@ -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; | |||
| } | |||
| @@ -32,7 +32,15 @@ namespace Discord | |||
| public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; | |||
| public CachedGuildUser CurrentUser => GetUser(Discord.CurrentUser.Id); | |||
| public IReadOnlyCollection<ICachedGuildChannel> Channels => _channels.Select(x => GetChannel(x)).ToReadOnlyCollection(_channels); | |||
| public IReadOnlyCollection<ICachedGuildChannel> 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<CachedGuildUser> Members => _members.ToReadOnlyCollection(); | |||
| public CachedGuild(DiscordSocketClient discord, ExtendedModel model, DataStore dataStore) : base(discord, model) | |||
| @@ -11,12 +11,13 @@ namespace Discord.Extensions | |||
| public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TValue, TSource>(this IEnumerable<TValue> query, IReadOnlyCollection<TSource> source) | |||
| => new ConcurrentDictionaryWrapper<TValue, TSource>(source, query); | |||
| } | |||
| internal struct ConcurrentDictionaryWrapper<TValue, TSource> : IReadOnlyCollection<TValue> | |||
| { | |||
| private readonly IReadOnlyCollection<TSource> _source; | |||
| private readonly IEnumerable<TValue> _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<TSource> source, IEnumerable<TValue> query) | |||
| @@ -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; | |||
| } | |||
| } | |||