| @@ -15,12 +15,12 @@ namespace Discord.Data | |||
| private readonly ConcurrentDictionary<ulong, ICachedChannel> _channels; | |||
| private readonly ConcurrentDictionary<ulong, CachedDMChannel> _dmChannels; | |||
| private readonly ConcurrentDictionary<ulong, CachedGuild> _guilds; | |||
| private readonly ConcurrentDictionary<ulong, CachedPublicUser> _users; | |||
| private readonly ConcurrentDictionary<ulong, CachedGlobalUser> _users; | |||
| internal override IReadOnlyCollection<ICachedChannel> Channels => _channels.ToReadOnlyCollection(); | |||
| internal override IReadOnlyCollection<CachedDMChannel> DMChannels => _dmChannels.ToReadOnlyCollection(); | |||
| internal override IReadOnlyCollection<CachedGuild> Guilds => _guilds.ToReadOnlyCollection(); | |||
| internal override IReadOnlyCollection<CachedPublicUser> Users => _users.ToReadOnlyCollection(); | |||
| internal override IReadOnlyCollection<CachedGlobalUser> Users => _users.ToReadOnlyCollection(); | |||
| public DefaultDataStore(int guildCount, int dmChannelCount) | |||
| { | |||
| @@ -29,7 +29,7 @@ namespace Discord.Data | |||
| _channels = new ConcurrentDictionary<ulong, ICachedChannel>(CollectionConcurrencyLevel, (int)(estimatedChannelCount * CollectionMultiplier)); | |||
| _dmChannels = new ConcurrentDictionary<ulong, CachedDMChannel>(CollectionConcurrencyLevel, (int)(dmChannelCount * CollectionMultiplier)); | |||
| _guilds = new ConcurrentDictionary<ulong, CachedGuild>(CollectionConcurrencyLevel, (int)(guildCount * CollectionMultiplier)); | |||
| _users = new ConcurrentDictionary<ulong, CachedPublicUser>(CollectionConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier)); | |||
| _users = new ConcurrentDictionary<ulong, CachedGlobalUser>(CollectionConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier)); | |||
| } | |||
| internal override ICachedChannel GetChannel(ulong id) | |||
| @@ -94,20 +94,20 @@ namespace Discord.Data | |||
| return null; | |||
| } | |||
| internal override CachedPublicUser GetUser(ulong id) | |||
| internal override CachedGlobalUser GetUser(ulong id) | |||
| { | |||
| CachedPublicUser user; | |||
| CachedGlobalUser user; | |||
| if (_users.TryGetValue(id, out user)) | |||
| return user; | |||
| return null; | |||
| } | |||
| internal override CachedPublicUser GetOrAddUser(ulong id, Func<ulong, CachedPublicUser> userFactory) | |||
| internal override CachedGlobalUser GetOrAddUser(ulong id, Func<ulong, CachedGlobalUser> userFactory) | |||
| { | |||
| return _users.GetOrAdd(id, userFactory); | |||
| } | |||
| internal override CachedPublicUser RemoveUser(ulong id) | |||
| internal override CachedGlobalUser RemoveUser(ulong id) | |||
| { | |||
| CachedPublicUser user; | |||
| CachedGlobalUser user; | |||
| if (_users.TryRemove(id, out user)) | |||
| return user; | |||
| return null; | |||
| @@ -8,7 +8,7 @@ namespace Discord.Data | |||
| internal abstract IReadOnlyCollection<ICachedChannel> Channels { get; } | |||
| internal abstract IReadOnlyCollection<CachedDMChannel> DMChannels { get; } | |||
| internal abstract IReadOnlyCollection<CachedGuild> Guilds { get; } | |||
| internal abstract IReadOnlyCollection<CachedPublicUser> Users { get; } | |||
| internal abstract IReadOnlyCollection<CachedGlobalUser> Users { get; } | |||
| internal abstract ICachedChannel GetChannel(ulong id); | |||
| internal abstract void AddChannel(ICachedChannel channel); | |||
| @@ -22,8 +22,8 @@ namespace Discord.Data | |||
| internal abstract void AddGuild(CachedGuild guild); | |||
| internal abstract CachedGuild RemoveGuild(ulong id); | |||
| internal abstract CachedPublicUser GetUser(ulong id); | |||
| internal abstract CachedPublicUser GetOrAddUser(ulong userId, Func<ulong, CachedPublicUser> userFactory); | |||
| internal abstract CachedPublicUser RemoveUser(ulong id); | |||
| internal abstract CachedGlobalUser GetUser(ulong id); | |||
| internal abstract CachedGlobalUser GetOrAddUser(ulong userId, Func<ulong, CachedGlobalUser> userFactory); | |||
| internal abstract CachedGlobalUser RemoveUser(ulong id); | |||
| } | |||
| } | |||
| @@ -13,7 +13,6 @@ using System.Collections.Immutable; | |||
| using System.Linq; | |||
| using System.Threading; | |||
| using System.Threading.Tasks; | |||
| using System.Diagnostics; | |||
| namespace Discord | |||
| { | |||
| @@ -51,15 +50,17 @@ 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; | |||
| private TaskCompletionSource<bool> _connectTask; | |||
| private CancellationTokenSource _heartbeatCancelToken; | |||
| private Task _heartbeatTask, _reconnectTask; | |||
| private CancellationTokenSource _cancelToken; | |||
| private Task _heartbeatTask, _guildDownloadTask, _reconnectTask; | |||
| private long _heartbeatTime; | |||
| private bool _isReconnecting; | |||
| private int _unavailableGuilds; | |||
| private long _lastGuildAvailableTime; | |||
| /// <summary> Gets the shard if of this client. </summary> | |||
| public int ShardId { get; } | |||
| @@ -74,15 +75,7 @@ namespace Discord | |||
| internal CachedSelfUser CurrentUser => _currentUser as CachedSelfUser; | |||
| internal IReadOnlyCollection<CachedGuild> Guilds => DataStore.Guilds; | |||
| internal IReadOnlyCollection<CachedDMChannel> DMChannels | |||
| { | |||
| get | |||
| { | |||
| var dmChannels = _dmChannels; | |||
| var store = DataStore; | |||
| return dmChannels.Select(x => store.GetChannel(x) as CachedDMChannel).Where(x => x != null).ToReadOnlyCollection(dmChannels); | |||
| } | |||
| } | |||
| internal IReadOnlyCollection<CachedDMChannel> DMChannels => DataStore.DMChannels; | |||
| internal IReadOnlyCollection<VoiceRegion> VoiceRegions => _voiceRegions.ToReadOnlyCollection(); | |||
| /// <summary> Creates a new REST/WebSocket discord client. </summary> | |||
| @@ -132,7 +125,6 @@ namespace Discord | |||
| _voiceRegions = ImmutableDictionary.Create<string, VoiceRegion>(); | |||
| _largeGuilds = new ConcurrentQueue<ulong>(); | |||
| _dmChannels = new ConcurrentHashSet<ulong>(); | |||
| } | |||
| protected override async Task OnLoginAsync() | |||
| @@ -169,10 +161,11 @@ namespace Discord | |||
| try | |||
| { | |||
| _connectTask = new TaskCompletionSource<bool>(); | |||
| _heartbeatCancelToken = new CancellationTokenSource(); | |||
| _cancelToken = new CancellationTokenSource(); | |||
| await ApiClient.ConnectAsync().ConfigureAwait(false); | |||
| await _connectTask.Task.ConfigureAwait(false); | |||
| ConnectionState = ConnectionState.Connected; | |||
| await _gatewayLogger.InfoAsync("Connected"); | |||
| } | |||
| @@ -203,9 +196,24 @@ namespace Discord | |||
| ConnectionState = ConnectionState.Disconnecting; | |||
| await _gatewayLogger.InfoAsync("Disconnecting"); | |||
| try { _heartbeatCancelToken.Cancel(); } catch { } | |||
| //Signal tasks to complete | |||
| try { _cancelToken.Cancel(); } catch { } | |||
| //Disconnect from server | |||
| await ApiClient.DisconnectAsync().ConfigureAwait(false); | |||
| await _heartbeatTask.ConfigureAwait(false); | |||
| //Wait for tasks to complete | |||
| var heartbeatTask = _heartbeatTask; | |||
| if (heartbeatTask != null) | |||
| await heartbeatTask.ConfigureAwait(false); | |||
| _heartbeatTask = null; | |||
| var guildDownloadTask = _guildDownloadTask; | |||
| if (guildDownloadTask != null) | |||
| await guildDownloadTask.ConfigureAwait(false); | |||
| _guildDownloadTask = null; | |||
| //Clear large guild queue | |||
| while (_largeGuilds.TryDequeue(out guildId)) { } | |||
| ConnectionState = ConnectionState.Disconnected; | |||
| @@ -216,22 +224,21 @@ namespace Discord | |||
| private async Task StartReconnectAsync() | |||
| { | |||
| //TODO: Is this thread-safe? | |||
| while (true) | |||
| await _log.InfoAsync("Debug", "Trying to reconnect...").ConfigureAwait(false); | |||
| if (_reconnectTask != null) return; | |||
| await _connectionLock.WaitAsync().ConfigureAwait(false); | |||
| try | |||
| { | |||
| if (_reconnectTask != null) return; | |||
| await _connectionLock.WaitAsync().ConfigureAwait(false); | |||
| try | |||
| { | |||
| if (_reconnectTask != null) return; | |||
| _isReconnecting = true; | |||
| _reconnectTask = ReconnectInternalAsync(); | |||
| } | |||
| finally { _connectionLock.Release(); } | |||
| _isReconnecting = true; | |||
| _reconnectTask = ReconnectInternalAsync(); | |||
| } | |||
| finally { _connectionLock.Release(); } | |||
| } | |||
| private async Task ReconnectInternalAsync() | |||
| { | |||
| await _log.InfoAsync("Debug", "Reconnecting...").ConfigureAwait(false); | |||
| try | |||
| { | |||
| int nextReconnectDelay = 1000; | |||
| @@ -255,13 +262,18 @@ namespace Discord | |||
| catch (Exception ex) | |||
| { | |||
| await _gatewayLogger.WarningAsync("Reconnect failed", ex).ConfigureAwait(false); | |||
| } } | |||
| } | |||
| } | |||
| } | |||
| finally | |||
| { | |||
| _isReconnecting = false; | |||
| _reconnectTask = null; | |||
| await _connectionLock.WaitAsync().ConfigureAwait(false); | |||
| try | |||
| { | |||
| _isReconnecting = false; | |||
| _reconnectTask = null; | |||
| } | |||
| finally { _connectionLock.Release(); } | |||
| } | |||
| } | |||
| @@ -318,20 +330,22 @@ namespace Discord | |||
| { | |||
| return Task.FromResult<IReadOnlyCollection<IDMChannel>>(DMChannels); | |||
| } | |||
| internal CachedDMChannel AddDMChannel(API.Channel model, DataStore dataStore, ConcurrentHashSet<ulong> dmChannels) | |||
| internal CachedDMChannel AddDMChannel(API.Channel model, DataStore dataStore) | |||
| { | |||
| var recipient = GetOrAddUser(model.Recipient.Value, dataStore); | |||
| var channel = recipient.AddDMChannel(this, model); | |||
| dataStore.AddChannel(channel); | |||
| dmChannels.TryAdd(model.Id); | |||
| var channel = new CachedDMChannel(this, new CachedDMUser(recipient), model); | |||
| recipient.AddRef(); | |||
| dataStore.AddDMChannel(channel); | |||
| return channel; | |||
| } | |||
| internal CachedDMChannel RemoveDMChannel(ulong id) | |||
| { | |||
| var dmChannel = DataStore.RemoveChannel(id) as CachedDMChannel; | |||
| var recipient = dmChannel.Recipient; | |||
| recipient.RemoveDMChannel(id); | |||
| _dmChannels.TryRemove(id); | |||
| var dmChannel = DataStore.RemoveDMChannel(id); | |||
| if (dmChannel != null) | |||
| { | |||
| var recipient = dmChannel.Recipient; | |||
| recipient.User.RemoveRef(this); | |||
| } | |||
| return dmChannel; | |||
| } | |||
| @@ -345,13 +359,13 @@ namespace Discord | |||
| { | |||
| return Task.FromResult<IUser>(DataStore.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault()); | |||
| } | |||
| internal CachedPublicUser GetOrAddUser(API.User model, DataStore dataStore) | |||
| internal CachedGlobalUser GetOrAddUser(API.User model, DataStore dataStore) | |||
| { | |||
| var user = dataStore.GetOrAddUser(model.Id, _ => new CachedPublicUser(model)); | |||
| var user = dataStore.GetOrAddUser(model.Id, _ => new CachedGlobalUser(model)); | |||
| user.AddRef(); | |||
| return user; | |||
| } | |||
| internal CachedPublicUser RemoveUser(ulong id) | |||
| internal CachedGlobalUser RemoveUser(ulong id) | |||
| { | |||
| return DataStore.RemoveUser(id); | |||
| } | |||
| @@ -425,7 +439,7 @@ namespace Discord | |||
| else | |||
| await ApiClient.SendIdentifyAsync().ConfigureAwait(false); | |||
| _heartbeatTime = 0; | |||
| _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _heartbeatCancelToken.Token); | |||
| _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _cancelToken.Token); | |||
| } | |||
| break; | |||
| case GatewayOpCode.Heartbeat: | |||
| @@ -439,12 +453,16 @@ namespace Discord | |||
| { | |||
| await _gatewayLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false); | |||
| var latency = (int)(Environment.TickCount - _heartbeatTime); | |||
| _heartbeatTime = 0; | |||
| await _gatewayLogger.DebugAsync($"Latency = {latency} ms").ConfigureAwait(false); | |||
| Latency = latency; | |||
| var heartbeatTime = _heartbeatTime; | |||
| if (heartbeatTime != 0) | |||
| { | |||
| var latency = (int)(Environment.TickCount - _heartbeatTime); | |||
| _heartbeatTime = 0; | |||
| await _gatewayLogger.VerboseAsync($"Latency = {latency} ms").ConfigureAwait(false); | |||
| Latency = latency; | |||
| await LatencyUpdated.RaiseAsync(latency).ConfigureAwait(false); | |||
| await LatencyUpdated.RaiseAsync(latency).ConfigureAwait(false); | |||
| } | |||
| } | |||
| break; | |||
| case GatewayOpCode.InvalidSession: | |||
| @@ -475,21 +493,29 @@ 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); | |||
| int unavailableGuilds = 0; | |||
| //dataStore.GetOrAddUser(data.User.Id, _ => currentUser); | |||
| for (int i = 0; i < data.Guilds.Length; i++) | |||
| AddGuild(data.Guilds[i], dataStore); | |||
| { | |||
| var model = data.Guilds[i]; | |||
| AddGuild(model, dataStore); | |||
| if (model.Unavailable == true) | |||
| unavailableGuilds++; | |||
| } | |||
| for (int i = 0; i < data.PrivateChannels.Length; i++) | |||
| AddDMChannel(data.PrivateChannels[i], dataStore, dmChannels); | |||
| AddDMChannel(data.PrivateChannels[i], dataStore); | |||
| _sessionId = data.SessionId; | |||
| _currentUser = currentUser; | |||
| _dmChannels = dmChannels; | |||
| _unavailableGuilds = unavailableGuilds; | |||
| _lastGuildAvailableTime = Environment.TickCount; | |||
| DataStore = dataStore; | |||
| _guildDownloadTask = WaitForGuildsAsync(_cancelToken.Token); | |||
| await Ready.RaiseAsync().ConfigureAwait(false); | |||
| _connectTask.TrySetResult(true); //Signal the .Connect() call to complete | |||
| @@ -503,7 +529,10 @@ namespace Discord | |||
| var data = (payload as JToken).ToObject<ExtendedGuild>(_serializer); | |||
| if (data.Unavailable == false) | |||
| { | |||
| type = "GUILD_AVAILABLE"; | |||
| _lastGuildAvailableTime = Environment.TickCount; | |||
| } | |||
| await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); | |||
| CachedGuild guild; | |||
| @@ -511,6 +540,7 @@ namespace Discord | |||
| { | |||
| guild = AddGuild(data, DataStore); | |||
| await JoinedGuild.RaiseAsync(guild).ConfigureAwait(false); | |||
| await _gatewayLogger.InfoAsync($"Joined {data.Name}").ConfigureAwait(false); | |||
| } | |||
| else | |||
| { | |||
| @@ -526,7 +556,7 @@ namespace Discord | |||
| if (data.Unavailable != true) | |||
| { | |||
| await _gatewayLogger.InfoAsync($"Connected to {data.Name}").ConfigureAwait(false); | |||
| await _gatewayLogger.VerboseAsync($"Connected to {data.Name}").ConfigureAwait(false); | |||
| await GuildAvailable.RaiseAsync(guild).ConfigureAwait(false); | |||
| } | |||
| } | |||
| @@ -564,7 +594,7 @@ namespace Discord | |||
| member.User.RemoveRef(this); | |||
| await GuildUnavailable.RaiseAsync(guild).ConfigureAwait(false); | |||
| await _gatewayLogger.InfoAsync($"Disconnected from {data.Name}").ConfigureAwait(false); | |||
| await _gatewayLogger.VerboseAsync($"Disconnected from {data.Name}").ConfigureAwait(false); | |||
| if (data.Unavailable != true) | |||
| { | |||
| await LeftGuild.RaiseAsync(guild).ConfigureAwait(false); | |||
| @@ -587,7 +617,7 @@ namespace Discord | |||
| var data = (payload as JToken).ToObject<API.Channel>(_serializer); | |||
| ICachedChannel channel = null; | |||
| if (data.GuildId.IsSpecified) | |||
| if (!data.IsPrivate) | |||
| { | |||
| var guild = DataStore.GetGuild(data.GuildId.Value); | |||
| if (guild != null) | |||
| @@ -599,7 +629,7 @@ namespace Discord | |||
| } | |||
| } | |||
| else | |||
| channel = AddDMChannel(data, DataStore, _dmChannels); | |||
| channel = AddDMChannel(data, DataStore); | |||
| if (channel != null) | |||
| await ChannelCreated.RaiseAsync(channel).ConfigureAwait(false); | |||
| } | |||
| @@ -629,7 +659,7 @@ namespace Discord | |||
| ICachedChannel channel = null; | |||
| var data = (payload as JToken).ToObject<API.Channel>(_serializer); | |||
| if (data.GuildId.IsSpecified) | |||
| if (!data.IsPrivate) | |||
| { | |||
| var guild = DataStore.GetGuild(data.GuildId.Value); | |||
| if (guild != null) | |||
| @@ -975,9 +1005,9 @@ namespace Discord | |||
| } | |||
| else | |||
| { | |||
| var user = DataStore.GetUser(data.User.Id); | |||
| if (user == null) | |||
| user.Update(data, UpdateSource.WebSocket); | |||
| var channel = DataStore.GetDMChannel(data.User.Id); | |||
| if (channel != null) | |||
| channel.Recipient.Update(data, UpdateSource.WebSocket); | |||
| } | |||
| } | |||
| break; | |||
| @@ -1095,22 +1125,37 @@ namespace Discord | |||
| { | |||
| try | |||
| { | |||
| var state = ConnectionState; | |||
| while (state == ConnectionState.Connecting || state == ConnectionState.Connected) | |||
| while (!cancelToken.IsCancellationRequested) | |||
| { | |||
| await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); | |||
| if (_heartbeatTime != 0) //Server never responded to our last heartbeat | |||
| { | |||
| await _gatewayLogger.WarningAsync("Server missed last heartbeat").ConfigureAwait(false); | |||
| await StartReconnectAsync().ConfigureAwait(false); | |||
| return; | |||
| if (ConnectionState == ConnectionState.Connected && (_guildDownloadTask?.IsCompleted ?? false)) | |||
| { | |||
| await _gatewayLogger.WarningAsync("Server missed last heartbeat").ConfigureAwait(false); | |||
| await StartReconnectAsync().ConfigureAwait(false); | |||
| return; | |||
| } | |||
| } | |||
| _heartbeatTime = Environment.TickCount; | |||
| else | |||
| _heartbeatTime = Environment.TickCount; | |||
| await ApiClient.SendHeartbeatAsync(_lastSeq).ConfigureAwait(false); | |||
| } | |||
| } | |||
| catch (OperationCanceledException) { } | |||
| } | |||
| private async Task WaitForGuildsAsync(CancellationToken cancelToken) | |||
| { | |||
| while ((_unavailableGuilds > 0) || (Environment.TickCount - _lastGuildAvailableTime > 2000)) | |||
| await Task.Delay(500, cancelToken).ConfigureAwait(false); | |||
| } | |||
| public async Task WaitForGuildsAsync() | |||
| { | |||
| var downloadTask = _guildDownloadTask; | |||
| if (downloadTask != null) | |||
| await _guildDownloadTask.ConfigureAwait(false); | |||
| } | |||
| } | |||
| } | |||
| @@ -14,11 +14,11 @@ namespace Discord | |||
| internal class DMChannel : SnowflakeEntity, IDMChannel | |||
| { | |||
| public override DiscordClient Discord { get; } | |||
| public User Recipient { get; private set; } | |||
| public IUser Recipient { get; private set; } | |||
| public virtual IReadOnlyCollection<IMessage> CachedMessages => ImmutableArray.Create<IMessage>(); | |||
| public DMChannel(DiscordClient discord, User recipient, Model model) | |||
| public DMChannel(DiscordClient discord, IUser recipient, Model model) | |||
| : base(model.Id) | |||
| { | |||
| Discord = discord; | |||
| @@ -30,7 +30,9 @@ namespace Discord | |||
| { | |||
| if (source == UpdateSource.Rest && IsAttached) return; | |||
| Recipient.Update(model.Recipient.Value, UpdateSource.Rest); | |||
| //TODO: Is this cast okay? | |||
| if (Recipient is User) | |||
| (Recipient as User).Update(model.Recipient.Value, source); | |||
| } | |||
| public async Task UpdateAsync() | |||
| @@ -119,8 +121,7 @@ namespace Discord | |||
| public override string ToString() => '@' + Recipient.ToString(); | |||
| private string DebuggerDisplay => $"@{Recipient} ({Id}, DM)"; | |||
| IUser IDMChannel.Recipient => Recipient; | |||
| IMessage IMessageChannel.GetCachedMessage(ulong id) => null; | |||
| } | |||
| } | |||
| @@ -1,7 +1,5 @@ | |||
| using Discord.API.Rest; | |||
| using Discord.Extensions; | |||
| using System; | |||
| using System.Collections.Concurrent; | |||
| using System.Collections.Generic; | |||
| using System.Collections.Immutable; | |||
| using System.Diagnostics; | |||
| @@ -14,7 +12,7 @@ namespace Discord | |||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
| internal abstract class GuildChannel : SnowflakeEntity, IGuildChannel | |||
| { | |||
| private ConcurrentDictionary<ulong, Overwrite> _overwrites; | |||
| private List<Overwrite> _overwrites; //TODO: Is maintaining a list here too expensive? Is this threadsafe? | |||
| public string Name { get; private set; } | |||
| public int Position { get; private set; } | |||
| @@ -38,9 +36,9 @@ namespace Discord | |||
| Position = model.Position.Value; | |||
| var overwrites = model.PermissionOverwrites.Value; | |||
| var newOverwrites = new ConcurrentDictionary<ulong, Overwrite>(); | |||
| var newOverwrites = new List<Overwrite>(overwrites.Length); | |||
| for (int i = 0; i < overwrites.Length; i++) | |||
| newOverwrites[overwrites[i].TargetId] = new Overwrite(overwrites[i]); | |||
| newOverwrites.Add(new Overwrite(overwrites[i])); | |||
| _overwrites = newOverwrites; | |||
| } | |||
| @@ -89,16 +87,20 @@ namespace Discord | |||
| public OverwritePermissions? GetPermissionOverwrite(IUser user) | |||
| { | |||
| Overwrite value; | |||
| if (_overwrites.TryGetValue(user.Id, out value)) | |||
| return value.Permissions; | |||
| for (int i = 0; i < _overwrites.Count; i++) | |||
| { | |||
| if (_overwrites[i].TargetId == user.Id) | |||
| return _overwrites[i].Permissions; | |||
| } | |||
| return null; | |||
| } | |||
| public OverwritePermissions? GetPermissionOverwrite(IRole role) | |||
| { | |||
| Overwrite value; | |||
| if (_overwrites.TryGetValue(role.Id, out value)) | |||
| return value.Permissions; | |||
| for (int i = 0; i < _overwrites.Count; i++) | |||
| { | |||
| if (_overwrites[i].TargetId == role.Id) | |||
| return _overwrites[i].Permissions; | |||
| } | |||
| return null; | |||
| } | |||
| @@ -106,34 +108,46 @@ namespace Discord | |||
| { | |||
| var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue }; | |||
| await Discord.ApiClient.ModifyChannelPermissionsAsync(Id, user.Id, args).ConfigureAwait(false); | |||
| _overwrites[user.Id] = new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = user.Id, TargetType = PermissionTarget.User }); | |||
| _overwrites.Add(new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = user.Id, TargetType = PermissionTarget.User })); | |||
| } | |||
| public async Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions perms) | |||
| { | |||
| var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue }; | |||
| await Discord.ApiClient.ModifyChannelPermissionsAsync(Id, role.Id, args).ConfigureAwait(false); | |||
| _overwrites[role.Id] = new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = role.Id, TargetType = PermissionTarget.Role }); | |||
| _overwrites.Add(new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = role.Id, TargetType = PermissionTarget.Role })); | |||
| } | |||
| public async Task RemovePermissionOverwriteAsync(IUser user) | |||
| { | |||
| await Discord.ApiClient.DeleteChannelPermissionAsync(Id, user.Id).ConfigureAwait(false); | |||
| Overwrite value; | |||
| _overwrites.TryRemove(user.Id, out value); | |||
| for (int i = 0; i < _overwrites.Count; i++) | |||
| { | |||
| if (_overwrites[i].TargetId == user.Id) | |||
| { | |||
| _overwrites.RemoveAt(i); | |||
| return; | |||
| } | |||
| } | |||
| } | |||
| public async Task RemovePermissionOverwriteAsync(IRole role) | |||
| { | |||
| await Discord.ApiClient.DeleteChannelPermissionAsync(Id, role.Id).ConfigureAwait(false); | |||
| Overwrite value; | |||
| _overwrites.TryRemove(role.Id, out value); | |||
| for (int i = 0; i < _overwrites.Count; i++) | |||
| { | |||
| if (_overwrites[i].TargetId == role.Id) | |||
| { | |||
| _overwrites.RemoveAt(i); | |||
| return; | |||
| } | |||
| } | |||
| } | |||
| public override string ToString() => Name; | |||
| private string DebuggerDisplay => $"{Name} ({Id})"; | |||
| IGuild IGuildChannel.Guild => Guild; | |||
| IReadOnlyCollection<Overwrite> IGuildChannel.PermissionOverwrites => _overwrites.ToReadOnlyCollection(); | |||
| IReadOnlyCollection<Overwrite> IGuildChannel.PermissionOverwrites => _overwrites.AsReadOnly(); | |||
| async Task<IUser> IChannel.GetUserAsync(ulong id) => await GetUserAsync(id).ConfigureAwait(false); | |||
| async Task<IReadOnlyCollection<IUser>> IChannel.GetUsersAsync() => await GetUsersAsync().ConfigureAwait(false); | |||
| @@ -33,8 +33,9 @@ namespace Discord | |||
| public bool IsBot => User.IsBot; | |||
| public string Mention => User.Mention; | |||
| public string Username => User.Username; | |||
| public virtual UserStatus Status => User.Status; | |||
| public virtual Game Game => User.Game; | |||
| public virtual UserStatus Status => UserStatus.Unknown; | |||
| public virtual Game Game => null; | |||
| public DiscordClient Discord => Guild.Discord; | |||
| public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); | |||
| @@ -1,7 +1,5 @@ | |||
| using Discord.API.Rest; | |||
| using System; | |||
| using System; | |||
| using System.Diagnostics; | |||
| using System.Threading.Tasks; | |||
| using Model = Discord.API.User; | |||
| namespace Discord | |||
| @@ -9,16 +9,19 @@ namespace Discord | |||
| { | |||
| internal class CachedDMChannel : DMChannel, IDMChannel, ICachedChannel, ICachedMessageChannel | |||
| { | |||
| private readonly MessageCache _messages; | |||
| private readonly MessageManager _messages; | |||
| public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; | |||
| public new CachedPublicUser Recipient => base.Recipient as CachedPublicUser; | |||
| public new CachedDMUser Recipient => base.Recipient as CachedDMUser; | |||
| public IReadOnlyCollection<ICachedUser> Members => ImmutableArray.Create<ICachedUser>(Discord.CurrentUser, Recipient); | |||
| public CachedDMChannel(DiscordSocketClient discord, CachedPublicUser recipient, Model model) | |||
| public CachedDMChannel(DiscordSocketClient discord, CachedDMUser recipient, Model model) | |||
| : base(discord, recipient, model) | |||
| { | |||
| _messages = new MessageCache(Discord, this); | |||
| if (Discord.MessageCacheSize > 0) | |||
| _messages = new MessageCache(Discord, this); | |||
| else | |||
| _messages = new MessageManager(Discord, this); | |||
| } | |||
| public override Task<IUser> GetUserAsync(ulong id) => Task.FromResult<IUser>(GetUser(id)); | |||
| @@ -0,0 +1,38 @@ | |||
| using System; | |||
| using PresenceModel = Discord.API.Presence; | |||
| namespace Discord | |||
| { | |||
| internal class CachedDMUser : ICachedUser | |||
| { | |||
| public CachedGlobalUser User { get; } | |||
| public Game Game { get; private set; } | |||
| public UserStatus Status { get; private set; } | |||
| public DiscordSocketClient Discord => User.Discord; | |||
| public ulong Id => User.Id; | |||
| public string AvatarUrl => User.AvatarUrl; | |||
| public DateTimeOffset CreatedAt => User.CreatedAt; | |||
| public string Discriminator => User.Discriminator; | |||
| public bool IsAttached => User.IsAttached; | |||
| public bool IsBot => User.IsBot; | |||
| public string Mention => User.Mention; | |||
| public string Username => User.Username; | |||
| public CachedDMUser(CachedGlobalUser user) | |||
| { | |||
| User = user; | |||
| } | |||
| public void Update(PresenceModel model, UpdateSource source) | |||
| { | |||
| Status = model.Status; | |||
| Game = model.Game != null ? new Game(model.Game) : null; | |||
| } | |||
| public CachedDMUser Clone() => MemberwiseClone() as CachedDMUser; | |||
| ICachedUser ICachedUser.Clone() => Clone(); | |||
| } | |||
| } | |||
| @@ -0,0 +1,39 @@ | |||
| using System; | |||
| using Model = Discord.API.User; | |||
| namespace Discord | |||
| { | |||
| internal class CachedGlobalUser : User, ICachedUser | |||
| { | |||
| private ushort _references; | |||
| public new DiscordSocketClient Discord { get { throw new NotSupportedException(); } } | |||
| public override UserStatus Status => UserStatus.Unknown;// _status; | |||
| public override Game Game => null; //_game; | |||
| public CachedGlobalUser(Model model) | |||
| : base(model) | |||
| { | |||
| } | |||
| public void AddRef() | |||
| { | |||
| checked | |||
| { | |||
| lock (this) | |||
| _references++; | |||
| } | |||
| } | |||
| public void RemoveRef(DiscordSocketClient discord) | |||
| { | |||
| lock (this) | |||
| { | |||
| if (--_references == 0) | |||
| discord.RemoveUser(Id); | |||
| } | |||
| } | |||
| public CachedGlobalUser Clone() => MemberwiseClone() as CachedGlobalUser; | |||
| ICachedUser ICachedUser.Clone() => Clone(); | |||
| } | |||
| } | |||
| @@ -10,7 +10,7 @@ namespace Discord | |||
| public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; | |||
| public new CachedGuild Guild => base.Guild as CachedGuild; | |||
| public new CachedPublicUser User => base.User as CachedPublicUser; | |||
| public new CachedGlobalUser User => base.User as CachedGlobalUser; | |||
| public override Game Game => _game; | |||
| public override UserStatus Status => _status; | |||
| @@ -21,11 +21,11 @@ namespace Discord | |||
| public bool IsSuppressed => VoiceState?.IsSuppressed ?? false; | |||
| public CachedVoiceChannel VoiceChannel => VoiceState?.VoiceChannel; | |||
| public CachedGuildUser(CachedGuild guild, CachedPublicUser user, Model model) | |||
| public CachedGuildUser(CachedGuild guild, CachedGlobalUser user, Model model) | |||
| : base(guild, user, model) | |||
| { | |||
| } | |||
| public CachedGuildUser(CachedGuild guild, CachedPublicUser user, PresenceModel model) | |||
| public CachedGuildUser(CachedGuild guild, CachedGlobalUser user, PresenceModel model) | |||
| : base(guild, user, model) | |||
| { | |||
| } | |||
| @@ -1,75 +0,0 @@ | |||
| using ChannelModel = Discord.API.Channel; | |||
| using Model = Discord.API.User; | |||
| using PresenceModel = Discord.API.Presence; | |||
| namespace Discord | |||
| { | |||
| internal class CachedPublicUser : User, ICachedUser | |||
| { | |||
| //TODO: Fix removed game/status (add CachedDMUser?) | |||
| private int _references; | |||
| //private Game? _game; | |||
| //private UserStatus _status; | |||
| public CachedDMChannel DMChannel { get; private set; } | |||
| public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; | |||
| public override UserStatus Status => UserStatus.Unknown;// _status; | |||
| public override Game Game => null; //_game; | |||
| public CachedPublicUser(Model model) | |||
| : base(model) | |||
| { | |||
| } | |||
| public CachedDMChannel AddDMChannel(DiscordSocketClient discord, ChannelModel model) | |||
| { | |||
| lock (this) | |||
| { | |||
| var channel = new CachedDMChannel(discord, this, model); | |||
| DMChannel = channel; | |||
| return channel; | |||
| } | |||
| } | |||
| public CachedDMChannel RemoveDMChannel(ulong id) | |||
| { | |||
| lock (this) | |||
| { | |||
| var channel = DMChannel; | |||
| if (channel.Id == id) | |||
| { | |||
| DMChannel = null; | |||
| return channel; | |||
| } | |||
| return null; | |||
| } | |||
| } | |||
| public void Update(PresenceModel model, UpdateSource source) | |||
| { | |||
| if (source == UpdateSource.Rest) return; | |||
| //var game = model.Game != null ? new Game(model.Game) : (Game)null; | |||
| //_status = model.Status; | |||
| //_game = game; | |||
| } | |||
| public void AddRef() | |||
| { | |||
| lock (this) | |||
| _references++; | |||
| } | |||
| public void RemoveRef(DiscordSocketClient discord) | |||
| { | |||
| lock (this) | |||
| { | |||
| if (--_references == 0 && DMChannel == null) | |||
| discord.RemoveUser(Id); | |||
| } | |||
| } | |||
| public CachedPublicUser Clone() => MemberwiseClone() as CachedPublicUser; | |||
| ICachedUser ICachedUser.Clone() => Clone(); | |||
| } | |||
| } | |||
| @@ -9,7 +9,7 @@ namespace Discord | |||
| { | |||
| internal class CachedTextChannel : TextChannel, ICachedGuildChannel, ICachedMessageChannel | |||
| { | |||
| private readonly MessageCache _messages; | |||
| private readonly MessageManager _messages; | |||
| public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; | |||
| public new CachedGuild Guild => base.Guild as CachedGuild; | |||
| @@ -20,7 +20,10 @@ namespace Discord | |||
| public CachedTextChannel(CachedGuild guild, Model model) | |||
| : base(guild, model) | |||
| { | |||
| _messages = new MessageCache(Discord, this); | |||
| if (Discord.MessageCacheSize > 0) | |||
| _messages = new MessageCache(Discord, this); | |||
| else | |||
| _messages = new MessageManager(Discord, this); | |||
| } | |||
| public override Task<IGuildUser> GetUserAsync(ulong id) => Task.FromResult<IGuildUser>(GetUser(id)); | |||
| @@ -1,5 +1,4 @@ | |||
| using Discord.API.Rest; | |||
| using Discord.Extensions; | |||
| using Discord.Extensions; | |||
| using System; | |||
| using System.Collections.Concurrent; | |||
| using System.Collections.Generic; | |||
| @@ -9,26 +8,23 @@ using System.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| internal class MessageCache | |||
| internal class MessageCache : MessageManager | |||
| { | |||
| private readonly DiscordSocketClient _discord; | |||
| private readonly ICachedMessageChannel _channel; | |||
| private readonly ConcurrentDictionary<ulong, CachedMessage> _messages; | |||
| private readonly ConcurrentQueue<ulong> _orderedMessages; | |||
| private readonly int _size; | |||
| public IReadOnlyCollection<CachedMessage> Messages => _messages.ToReadOnlyCollection(); | |||
| public override IReadOnlyCollection<CachedMessage> Messages => _messages.ToReadOnlyCollection(); | |||
| public MessageCache(DiscordSocketClient discord, ICachedMessageChannel channel) | |||
| : base(discord, channel) | |||
| { | |||
| _discord = discord; | |||
| _channel = channel; | |||
| _size = discord.MessageCacheSize; | |||
| _messages = new ConcurrentDictionary<ulong, CachedMessage>(1, (int)(_size * 1.05)); | |||
| _orderedMessages = new ConcurrentQueue<ulong>(); | |||
| } | |||
| public void Add(CachedMessage message) | |||
| public override void Add(CachedMessage message) | |||
| { | |||
| if (_messages.TryAdd(message.Id, message)) | |||
| { | |||
| @@ -41,21 +37,21 @@ namespace Discord | |||
| } | |||
| } | |||
| public CachedMessage Remove(ulong id) | |||
| public override CachedMessage Remove(ulong id) | |||
| { | |||
| CachedMessage msg; | |||
| _messages.TryRemove(id, out msg); | |||
| return msg; | |||
| } | |||
| public CachedMessage Get(ulong id) | |||
| public override CachedMessage Get(ulong id) | |||
| { | |||
| CachedMessage result; | |||
| if (_messages.TryGetValue(id, out result)) | |||
| return result; | |||
| return null; | |||
| } | |||
| public IImmutableList<CachedMessage> GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) | |||
| public override IImmutableList<CachedMessage> GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) | |||
| { | |||
| if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); | |||
| if (limit == 0) return ImmutableArray<CachedMessage>.Empty; | |||
| @@ -81,57 +77,12 @@ namespace Discord | |||
| .ToImmutableArray(); | |||
| } | |||
| public async Task<CachedMessage> DownloadAsync(ulong id) | |||
| public override async Task<CachedMessage> DownloadAsync(ulong id) | |||
| { | |||
| var msg = Get(id); | |||
| if (msg != null) | |||
| return msg; | |||
| var model = await _discord.ApiClient.GetChannelMessageAsync(_channel.Id, id).ConfigureAwait(false); | |||
| if (model != null) | |||
| return new CachedMessage(_channel, new User(model.Author.Value), model); | |||
| return null; | |||
| } | |||
| public async Task<IReadOnlyCollection<CachedMessage>> DownloadAsync(ulong? fromId, Direction dir, int limit) | |||
| { | |||
| //TODO: Test heavily, especially the ordering of messages | |||
| if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); | |||
| if (limit == 0) return ImmutableArray<CachedMessage>.Empty; | |||
| var cachedMessages = GetMany(fromId, dir, limit); | |||
| if (cachedMessages.Count == limit) | |||
| return cachedMessages; | |||
| else if (cachedMessages.Count > limit) | |||
| return cachedMessages.Skip(cachedMessages.Count - limit).ToImmutableArray(); | |||
| else | |||
| { | |||
| Optional<ulong> relativeId; | |||
| if (cachedMessages.Count == 0) | |||
| relativeId = fromId ?? new Optional<ulong>(); | |||
| else | |||
| relativeId = dir == Direction.Before ? cachedMessages[0].Id : cachedMessages[cachedMessages.Count - 1].Id; | |||
| var args = new GetChannelMessagesParams | |||
| { | |||
| Limit = limit - cachedMessages.Count, | |||
| RelativeDirection = dir, | |||
| RelativeMessageId = relativeId | |||
| }; | |||
| var downloadedMessages = await _discord.ApiClient.GetChannelMessagesAsync(_channel.Id, args).ConfigureAwait(false); | |||
| var guild = (_channel as ICachedGuildChannel).Guild; | |||
| return cachedMessages.Concat(downloadedMessages.Select(x => | |||
| { | |||
| IUser user = _channel.GetUser(x.Author.Value.Id, true); | |||
| if (user == null) | |||
| { | |||
| var newUser = new User(x.Author.Value); | |||
| if (guild != null) | |||
| user = new GuildUser(guild, newUser); | |||
| else | |||
| user = newUser; | |||
| } | |||
| return new CachedMessage(_channel, user, x); | |||
| })).ToImmutableArray(); | |||
| } | |||
| return await base.DownloadAsync(id).ConfigureAwait(false); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,81 @@ | |||
| using Discord.API.Rest; | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Collections.Immutable; | |||
| using System.Linq; | |||
| using System.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| internal class MessageManager | |||
| { | |||
| private readonly DiscordSocketClient _discord; | |||
| private readonly ICachedMessageChannel _channel; | |||
| public virtual IReadOnlyCollection<CachedMessage> Messages | |||
| => ImmutableArray.Create<CachedMessage>(); | |||
| public MessageManager(DiscordSocketClient discord, ICachedMessageChannel channel) | |||
| { | |||
| _discord = discord; | |||
| _channel = channel; | |||
| } | |||
| public virtual void Add(CachedMessage message) { } | |||
| public virtual CachedMessage Remove(ulong id) => null; | |||
| public virtual CachedMessage Get(ulong id) => null; | |||
| public virtual IImmutableList<CachedMessage> GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) | |||
| => ImmutableArray.Create<CachedMessage>(); | |||
| public virtual async Task<CachedMessage> DownloadAsync(ulong id) | |||
| { | |||
| var model = await _discord.ApiClient.GetChannelMessageAsync(_channel.Id, id).ConfigureAwait(false); | |||
| if (model != null) | |||
| return new CachedMessage(_channel, new User(model.Author.Value), model); | |||
| return null; | |||
| } | |||
| public async Task<IReadOnlyCollection<CachedMessage>> DownloadAsync(ulong? fromId, Direction dir, int limit) | |||
| { | |||
| //TODO: Test heavily, especially the ordering of messages | |||
| if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); | |||
| if (limit == 0) return ImmutableArray<CachedMessage>.Empty; | |||
| var cachedMessages = GetMany(fromId, dir, limit); | |||
| if (cachedMessages.Count == limit) | |||
| return cachedMessages; | |||
| else if (cachedMessages.Count > limit) | |||
| return cachedMessages.Skip(cachedMessages.Count - limit).ToImmutableArray(); | |||
| else | |||
| { | |||
| Optional<ulong> relativeId; | |||
| if (cachedMessages.Count == 0) | |||
| relativeId = fromId ?? new Optional<ulong>(); | |||
| else | |||
| relativeId = dir == Direction.Before ? cachedMessages[0].Id : cachedMessages[cachedMessages.Count - 1].Id; | |||
| var args = new GetChannelMessagesParams | |||
| { | |||
| Limit = limit - cachedMessages.Count, | |||
| RelativeDirection = dir, | |||
| RelativeMessageId = relativeId | |||
| }; | |||
| var downloadedMessages = await _discord.ApiClient.GetChannelMessagesAsync(_channel.Id, args).ConfigureAwait(false); | |||
| var guild = (_channel as ICachedGuildChannel).Guild; | |||
| return cachedMessages.Concat(downloadedMessages.Select(x => | |||
| { | |||
| IUser user = _channel.GetUser(x.Author.Value.Id, true); | |||
| if (user == null) | |||
| { | |||
| var newUser = new User(x.Author.Value); | |||
| if (guild != null) | |||
| user = new GuildUser(guild, newUser); | |||
| else | |||
| user = newUser; | |||
| } | |||
| return new CachedMessage(_channel, user, x); | |||
| })).ToImmutableArray(); | |||
| } | |||
| } | |||
| } | |||
| } | |||