@@ -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; | |||
} | |||
} |