| @@ -1,4 +1,4 @@ | |||||
| using System.Collections.Immutable; | |||||
| using System.Collections.Generic; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| @@ -14,10 +14,10 @@ namespace Discord | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the set of clients where this user is currently active. | /// Gets the set of clients where this user is currently active. | ||||
| /// </summary> | /// </summary> | ||||
| IImmutableSet<ClientType> ActiveClients { get; } | |||||
| IReadOnlyCollection<ClientType> ActiveClients { get; } | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the list of activities that this user currently has available. | /// Gets the list of activities that this user currently has available. | ||||
| /// </summary> | /// </summary> | ||||
| IImmutableList<IActivity> Activities { get; } | |||||
| IReadOnlyCollection<IActivity> Activities { get; } | |||||
| } | } | ||||
| } | } | ||||
| @@ -5,6 +5,7 @@ using System.Globalization; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Model = Discord.API.User; | using Model = Discord.API.User; | ||||
| using EventUserModel = Discord.API.GuildScheduledEventUser; | using EventUserModel = Discord.API.GuildScheduledEventUser; | ||||
| using System.Collections.Generic; | |||||
| namespace Discord.Rest | namespace Discord.Rest | ||||
| { | { | ||||
| @@ -41,9 +42,9 @@ namespace Discord.Rest | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public virtual UserStatus Status => UserStatus.Offline; | public virtual UserStatus Status => UserStatus.Offline; | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public virtual IImmutableSet<ClientType> ActiveClients => ImmutableHashSet<ClientType>.Empty; | |||||
| public virtual IReadOnlyCollection<ClientType> ActiveClients => ImmutableHashSet<ClientType>.Empty; | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public virtual IImmutableList<IActivity> Activities => ImmutableList<IActivity>.Empty; | |||||
| public virtual IReadOnlyCollection<IActivity> Activities => ImmutableList<IActivity>.Empty; | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public virtual bool IsWebhook => false; | public virtual bool IsWebhook => false; | ||||
| @@ -502,6 +502,18 @@ namespace Discord.WebSocket | |||||
| internal readonly AsyncEvent<Func<SocketGroupUser, Task>> _recipientRemovedEvent = new AsyncEvent<Func<SocketGroupUser, Task>>(); | internal readonly AsyncEvent<Func<SocketGroupUser, Task>> _recipientRemovedEvent = new AsyncEvent<Func<SocketGroupUser, Task>>(); | ||||
| #endregion | #endregion | ||||
| #region Presence | |||||
| /// <summary> Fired when a users presence is updated. </summary> | |||||
| public event Func<SocketUser, SocketPresence, SocketPresence, Task> PresenceUpdated | |||||
| { | |||||
| add { _presenceUpdated.Add(value); } | |||||
| remove { _presenceUpdated.Remove(value); } | |||||
| } | |||||
| internal readonly AsyncEvent<Func<SocketUser, SocketPresence, SocketPresence, Task>> _presenceUpdated = new AsyncEvent<Func<SocketUser, SocketPresence, SocketPresence, Task>>(); | |||||
| #endregion | |||||
| #region Invites | #region Invites | ||||
| /// <summary> | /// <summary> | ||||
| /// Fired when an invite is created. | /// Fired when an invite is created. | ||||
| @@ -1858,6 +1858,8 @@ namespace Discord.WebSocket | |||||
| var data = (payload as JToken).ToObject<API.Presence>(_serializer); | var data = (payload as JToken).ToObject<API.Presence>(_serializer); | ||||
| SocketUser user = null; | |||||
| if (data.GuildId.IsSpecified) | if (data.GuildId.IsSpecified) | ||||
| { | { | ||||
| var guild = State.GetGuild(data.GuildId.Value); | var guild = State.GetGuild(data.GuildId.Value); | ||||
| @@ -1872,7 +1874,7 @@ namespace Discord.WebSocket | |||||
| return; | return; | ||||
| } | } | ||||
| var user = guild.GetUser(data.User.Id); | |||||
| user = guild.GetUser(data.User.Id); | |||||
| if (user == null) | if (user == null) | ||||
| { | { | ||||
| if (data.Status == UserStatus.Offline) | if (data.Status == UserStatus.Offline) | ||||
| @@ -1890,26 +1892,21 @@ namespace Discord.WebSocket | |||||
| await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false); | await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false); | ||||
| } | } | ||||
| } | } | ||||
| var before = user.Clone(); | |||||
| user.Update(State, data, true); | |||||
| var cacheableBefore = new Cacheable<SocketGuildUser, ulong>(before, user.Id, true, () => Task.FromResult(user)); | |||||
| await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); | |||||
| } | } | ||||
| else | else | ||||
| { | { | ||||
| var globalUser = State.GetUser(data.User.Id); | |||||
| if (globalUser == null) | |||||
| user = State.GetUser(data.User.Id); | |||||
| if (user == null) | |||||
| { | { | ||||
| await UnknownGlobalUserAsync(type, data.User.Id).ConfigureAwait(false); | await UnknownGlobalUserAsync(type, data.User.Id).ConfigureAwait(false); | ||||
| return; | return; | ||||
| } | } | ||||
| var before = globalUser.Clone(); | |||||
| globalUser.Update(State, data.User); | |||||
| globalUser.Update(State, data); | |||||
| await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), before, globalUser).ConfigureAwait(false); | |||||
| } | } | ||||
| var before = user.Presence.Clone(); | |||||
| user.Update(State, data.User); | |||||
| user.Update(data); | |||||
| await TimedInvokeAsync(_presenceUpdated, nameof(PresenceUpdated), user, before, user.Presence).ConfigureAwait(false); | |||||
| } | } | ||||
| break; | break; | ||||
| case "TYPING_START": | case "TYPING_START": | ||||
| @@ -1,7 +1,6 @@ | |||||
| using System.Diagnostics; | using System.Diagnostics; | ||||
| using System.Linq; | using System.Linq; | ||||
| using Model = Discord.API.User; | using Model = Discord.API.User; | ||||
| using PresenceModel = Discord.API.Presence; | |||||
| namespace Discord.WebSocket | namespace Discord.WebSocket | ||||
| { | { | ||||
| @@ -48,11 +47,6 @@ namespace Discord.WebSocket | |||||
| } | } | ||||
| } | } | ||||
| internal void Update(ClientState state, PresenceModel model) | |||||
| { | |||||
| Presence = SocketPresence.Create(model); | |||||
| } | |||||
| private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Global)"; | private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Global)"; | ||||
| internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser; | internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser; | ||||
| } | } | ||||
| @@ -164,8 +164,7 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| if (updatePresence) | if (updatePresence) | ||||
| { | { | ||||
| Presence = SocketPresence.Create(model); | |||||
| GlobalUser.Update(state, model); | |||||
| Update(model); | |||||
| } | } | ||||
| if (model.Nick.IsSpecified) | if (model.Nick.IsSpecified) | ||||
| Nickname = model.Nick.Value; | Nickname = model.Nick.Value; | ||||
| @@ -174,6 +173,13 @@ namespace Discord.WebSocket | |||||
| if (model.PremiumSince.IsSpecified) | if (model.PremiumSince.IsSpecified) | ||||
| _premiumSinceTicks = model.PremiumSince.Value?.UtcTicks; | _premiumSinceTicks = model.PremiumSince.Value?.UtcTicks; | ||||
| } | } | ||||
| internal override void Update(PresenceModel model) | |||||
| { | |||||
| Presence.Update(model); | |||||
| GlobalUser.Update(model); | |||||
| } | |||||
| private void UpdateRoles(ulong[] roleIds) | private void UpdateRoles(ulong[] roleIds) | ||||
| { | { | ||||
| var roles = ImmutableArray.CreateBuilder<ulong>(roleIds.Length + 1); | var roles = ImmutableArray.CreateBuilder<ulong>(roleIds.Length + 1); | ||||
| @@ -11,26 +11,37 @@ namespace Discord.WebSocket | |||||
| /// Represents the WebSocket user's presence status. This may include their online status and their activity. | /// Represents the WebSocket user's presence status. This may include their online status and their activity. | ||||
| /// </summary> | /// </summary> | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| public struct SocketPresence : IPresence | |||||
| public class SocketPresence : IPresence | |||||
| { | { | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public UserStatus Status { get; } | |||||
| public UserStatus Status { get; private set; } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public IImmutableSet<ClientType> ActiveClients { get; } | |||||
| public IReadOnlyCollection<ClientType> ActiveClients { get; private set; } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public IImmutableList<IActivity> Activities { get; } | |||||
| public IReadOnlyCollection<IActivity> Activities { get; private set; } | |||||
| internal SocketPresence() { } | |||||
| internal SocketPresence(UserStatus status, IImmutableSet<ClientType> activeClients, IImmutableList<IActivity> activities) | internal SocketPresence(UserStatus status, IImmutableSet<ClientType> activeClients, IImmutableList<IActivity> activities) | ||||
| { | { | ||||
| Status = status; | Status = status; | ||||
| ActiveClients = activeClients ?? ImmutableHashSet<ClientType>.Empty; | ActiveClients = activeClients ?? ImmutableHashSet<ClientType>.Empty; | ||||
| Activities = activities ?? ImmutableList<IActivity>.Empty; | Activities = activities ?? ImmutableList<IActivity>.Empty; | ||||
| } | } | ||||
| internal static SocketPresence Create(Model model) | internal static SocketPresence Create(Model model) | ||||
| { | { | ||||
| var clients = ConvertClientTypesDict(model.ClientStatus.GetValueOrDefault()); | |||||
| var activities = ConvertActivitiesList(model.Activities); | |||||
| return new SocketPresence(model.Status, clients, activities); | |||||
| var entity = new SocketPresence(); | |||||
| entity.Update(model); | |||||
| return entity; | |||||
| } | |||||
| internal void Update(Model model) | |||||
| { | |||||
| Status = model.Status; | |||||
| ActiveClients = ConvertClientTypesDict(model.ClientStatus.GetValueOrDefault()) ?? ImmutableArray<ClientType>.Empty; | |||||
| Activities = ConvertActivitiesList(model.Activities) ?? ImmutableArray<IActivity>.Empty; | |||||
| } | } | ||||
| /// <summary> | /// <summary> | ||||
| /// Creates a new <see cref="IReadOnlyCollection{T}"/> containing all of the client types | /// Creates a new <see cref="IReadOnlyCollection{T}"/> containing all of the client types | ||||
| /// where a user is active from the data supplied in the Presence update frame. | /// where a user is active from the data supplied in the Presence update frame. | ||||
| @@ -42,7 +53,7 @@ namespace Discord.WebSocket | |||||
| /// <returns> | /// <returns> | ||||
| /// A collection of all <see cref="ClientType"/>s that this user is active. | /// A collection of all <see cref="ClientType"/>s that this user is active. | ||||
| /// </returns> | /// </returns> | ||||
| private static IImmutableSet<ClientType> ConvertClientTypesDict(IDictionary<string, string> clientTypesDict) | |||||
| private static IReadOnlyCollection<ClientType> ConvertClientTypesDict(IDictionary<string, string> clientTypesDict) | |||||
| { | { | ||||
| if (clientTypesDict == null || clientTypesDict.Count == 0) | if (clientTypesDict == null || clientTypesDict.Count == 0) | ||||
| return ImmutableHashSet<ClientType>.Empty; | return ImmutableHashSet<ClientType>.Empty; | ||||
| @@ -84,6 +95,6 @@ namespace Discord.WebSocket | |||||
| public override string ToString() => Status.ToString(); | public override string ToString() => Status.ToString(); | ||||
| private string DebuggerDisplay => $"{Status}{(Activities?.FirstOrDefault()?.Name ?? "")}"; | private string DebuggerDisplay => $"{Status}{(Activities?.FirstOrDefault()?.Name ?? "")}"; | ||||
| internal SocketPresence Clone() => this; | |||||
| internal SocketPresence Clone() => MemberwiseClone() as SocketPresence; | |||||
| } | } | ||||
| } | } | ||||
| @@ -7,6 +7,7 @@ using System.Linq; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Discord.Rest; | using Discord.Rest; | ||||
| using Model = Discord.API.User; | using Model = Discord.API.User; | ||||
| using PresenceModel = Discord.API.Presence; | |||||
| namespace Discord.WebSocket | namespace Discord.WebSocket | ||||
| { | { | ||||
| @@ -40,9 +41,9 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public UserStatus Status => Presence.Status; | public UserStatus Status => Presence.Status; | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public IImmutableSet<ClientType> ActiveClients => Presence.ActiveClients ?? ImmutableHashSet<ClientType>.Empty; | |||||
| public IReadOnlyCollection<ClientType> ActiveClients => Presence.ActiveClients ?? ImmutableHashSet<ClientType>.Empty; | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public IImmutableList<IActivity> Activities => Presence.Activities ?? ImmutableList<IActivity>.Empty; | |||||
| public IReadOnlyCollection<IActivity> Activities => Presence.Activities ?? ImmutableList<IActivity>.Empty; | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets mutual guilds shared with this user. | /// Gets mutual guilds shared with this user. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -91,6 +92,11 @@ namespace Discord.WebSocket | |||||
| return hasChanges; | return hasChanges; | ||||
| } | } | ||||
| internal virtual void Update(PresenceModel model) | |||||
| { | |||||
| Presence.Update(model); | |||||
| } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public async Task<IDMChannel> CreateDMChannelAsync(RequestOptions options = null) | public async Task<IDMChannel> CreateDMChannelAsync(RequestOptions options = null) | ||||
| => await UserHelper.CreateDMChannelAsync(this, Discord, options).ConfigureAwait(false); | => await UserHelper.CreateDMChannelAsync(this, Discord, options).ConfigureAwait(false); | ||||