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