| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
67c5094462 | more work on cache provider | 3 years ago |
|
|
e14e54061e | Merge branch 'dev' into v4/state-cache-providers | 3 years ago |
|
|
dc2dafa3ac | custom model factory | 3 years ago |
|
|
157063c7ed | default model maps | 3 years ago |
|
|
59c334ac60 | refactor models and remove cache run mode | 3 years ago |
|
|
9826cf699f
|
Update src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs
Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> |
3 years ago |
|
|
a4e1f54d6e
|
Update src/Discord.Net.WebSocket/DiscordSocketConfig.cs
Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> |
3 years ago |
|
|
d89d13d703 | Final POC for users | 3 years ago |
|
|
17306d5139 | updates | 3 years ago |
|
|
adcb58e473 | refactor default state provider | 3 years ago |
|
|
627f88795b | Change up model flows | 3 years ago |
|
|
3e36fbb854 |
initial implementation
Cachable models so far - Users - Presence |
3 years ago |
| @@ -0,0 +1,116 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| internal static class CacheableEntityExtensions | |||||
| { | |||||
| public static IActivityModel ToModel<TModel>(this RichGame richGame) where TModel : IActivityModel, new() | |||||
| { | |||||
| return new TModel() | |||||
| { | |||||
| ApplicationId = richGame.ApplicationId, | |||||
| SmallImage = richGame.SmallAsset?.ImageId, | |||||
| SmallText = richGame.SmallAsset?.Text, | |||||
| LargeImage = richGame.LargeAsset?.ImageId, | |||||
| LargeText = richGame.LargeAsset?.Text, | |||||
| Details = richGame.Details, | |||||
| Flags = richGame.Flags, | |||||
| Name = richGame.Name, | |||||
| Type = richGame.Type, | |||||
| JoinSecret = richGame.Secrets?.Join, | |||||
| SpectateSecret = richGame.Secrets?.Spectate, | |||||
| MatchSecret = richGame.Secrets?.Match, | |||||
| State = richGame.State, | |||||
| PartyId = richGame.Party?.Id, | |||||
| PartySize = richGame.Party?.Members != null && richGame.Party?.Capacity != null | |||||
| ? new long[] { richGame.Party.Members, richGame.Party.Capacity } | |||||
| : null, | |||||
| TimestampEnd = richGame.Timestamps?.End, | |||||
| TimestampStart = richGame.Timestamps?.Start | |||||
| }; | |||||
| } | |||||
| public static IActivityModel ToModel<TModel>(this SpotifyGame spotify) where TModel : IActivityModel, new() | |||||
| { | |||||
| return new TModel() | |||||
| { | |||||
| Name = spotify.Name, | |||||
| SessionId = spotify.SessionId, | |||||
| SyncId = spotify.TrackId, | |||||
| LargeText = spotify.AlbumTitle, | |||||
| Details = spotify.TrackTitle, | |||||
| State = string.Join(";", spotify.Artists), | |||||
| TimestampEnd = spotify.EndsAt, | |||||
| TimestampStart = spotify.StartedAt, | |||||
| LargeImage = spotify.AlbumArt, | |||||
| Type = ActivityType.Listening, | |||||
| Flags = spotify.Flags, | |||||
| }; | |||||
| } | |||||
| public static IActivityModel ToModel<TModel, TEmoteModel>(this CustomStatusGame custom) | |||||
| where TModel : IActivityModel, new() | |||||
| where TEmoteModel : IEmojiModel, new() | |||||
| { | |||||
| return new TModel | |||||
| { | |||||
| Id = "custom", | |||||
| Type = ActivityType.CustomStatus, | |||||
| Name = custom.Name, | |||||
| State = custom.State, | |||||
| Emoji = custom.Emote.ToModel<TEmoteModel>(), | |||||
| CreatedAt = custom.CreatedAt | |||||
| }; | |||||
| } | |||||
| public static IActivityModel ToModel<TModel>(this StreamingGame stream) where TModel : IActivityModel, new() | |||||
| { | |||||
| return new TModel | |||||
| { | |||||
| Name = stream.Name, | |||||
| Url = stream.Url, | |||||
| Flags = stream.Flags, | |||||
| Details = stream.Details | |||||
| }; | |||||
| } | |||||
| public static IEmojiModel ToModel(this IEmote emote, IEmojiModel model) | |||||
| { | |||||
| if (emote == null) | |||||
| return null; | |||||
| model.Name = emote.Name; | |||||
| if (emote is GuildEmote guildEmote) | |||||
| { | |||||
| model.Id = guildEmote.Id; | |||||
| model.IsAnimated = guildEmote.Animated; | |||||
| model.IsAvailable = guildEmote.IsAvailable; | |||||
| model.IsManaged = guildEmote.IsManaged; | |||||
| model.CreatorId = guildEmote.CreatorId; | |||||
| model.RequireColons = guildEmote.RequireColons; | |||||
| model.Roles = guildEmote.RoleIds.ToArray(); | |||||
| } | |||||
| if (emote is Emote e) | |||||
| { | |||||
| model.IsAnimated = e.Animated; | |||||
| model.Id = e.Id; | |||||
| } | |||||
| return model; | |||||
| } | |||||
| public static IEmojiModel ToModel<TModel>(this IEmote emote) where TModel : IEmojiModel, new() | |||||
| { | |||||
| if (emote == null) | |||||
| return null; | |||||
| return emote.ToModel(new TModel()); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,20 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| internal interface ICached<TType> : ICached, IDisposable | |||||
| { | |||||
| void Update(TType model); | |||||
| TType ToModel(); | |||||
| } | |||||
| public interface ICached | |||||
| { | |||||
| bool IsFreed { get; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,16 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| public interface IPartialApplicationModel : IEntityModel<ulong> | |||||
| { | |||||
| string Name { get; set; } | |||||
| string Icon { get; set; } | |||||
| string Description { get; set; } | |||||
| string CoverImage { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,21 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| public interface IEmojiModel | |||||
| { | |||||
| ulong? Id { get; set; } | |||||
| string Name { get; set; } | |||||
| ulong[] Roles { get; set; } | |||||
| bool RequireColons { get; set; } | |||||
| bool IsManaged { get; set; } | |||||
| bool IsAnimated { get; set; } | |||||
| bool IsAvailable { get; set; } | |||||
| ulong? CreatorId { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,13 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| public interface IEntityModel<TId> where TId : IEquatable<TId> | |||||
| { | |||||
| TId Id { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,35 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| public interface IMessageComponentModel | |||||
| { | |||||
| ComponentType Type { get; set; } | |||||
| string CustomId { get; set; } | |||||
| bool? Disabled { get; set; } | |||||
| ButtonStyle? Style { get; set; } | |||||
| string Label { get; set; } | |||||
| // emoji | |||||
| ulong? EmojiId { get; set; } | |||||
| string EmojiName { get; set; } | |||||
| bool? EmojiAnimated { get; set; } | |||||
| string Url { get; set; } | |||||
| IMessageComponentOptionModel[] Options { get; set; } | |||||
| string Placeholder { get; set; } | |||||
| int? MinValues { get; set; } | |||||
| int? MaxValues { get; set; } | |||||
| IMessageComponentModel[] Components { get; set; } | |||||
| int? MinLength { get; set; } | |||||
| int? MaxLength { get; set; } | |||||
| bool? Required { get; set; } | |||||
| string Value { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,22 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| public interface IMessageComponentOptionModel | |||||
| { | |||||
| string Label { get; set; } | |||||
| string Value { get; set; } | |||||
| string Description { get; set; } | |||||
| // emoji | |||||
| ulong? EmojiId { get; set; } | |||||
| string EmojiName { get; set; } | |||||
| bool? EmojiAnimated { get; set; } | |||||
| bool? Default { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,21 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| public interface IAttachmentModel : IEntityModel<ulong> | |||||
| { | |||||
| string FileName { get; set; } | |||||
| string Description { get; set; } | |||||
| string ContentType { get; set; } | |||||
| int Size { get; set; } | |||||
| string Url { get; set; } | |||||
| string ProxyUrl { get; set; } | |||||
| int? Height { get; set; } | |||||
| int? Width { get; set; } | |||||
| bool Ephemeral { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,44 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| public interface IEmbedModel | |||||
| { | |||||
| string Title { get; set; } | |||||
| EmbedType Type { get; set; } | |||||
| string Description { get; set; } | |||||
| string Url { get; set; } | |||||
| long? Timestamp { get; set; } | |||||
| uint? Color { get; set; } | |||||
| string FooterText { get; set; } | |||||
| string FooterIconUrl { get; set; } | |||||
| string FooterProxyUrl { get; set; } | |||||
| string ProviderName { get; set; } | |||||
| string ProviderUrl { get; set; } | |||||
| string AuthorName { get; set; } | |||||
| string AuthorUrl { get; set; } | |||||
| string AuthorIconUrl { get; set; } | |||||
| string AuthorProxyIconUrl { get; set; } | |||||
| IEmbedMediaModel Image { get; set; } | |||||
| IEmbedMediaModel Thumbnail { get; set; } | |||||
| IEmbedMediaModel Video { get; set; } | |||||
| IEmbedFieldModel[] Fields { get; set; } | |||||
| } | |||||
| public interface IEmbedMediaModel | |||||
| { | |||||
| string Url { get; set; } | |||||
| string ProxyUrl { get; set; } | |||||
| int? Height { get; set; } | |||||
| int? Width { get; set; } | |||||
| } | |||||
| public interface IEmbedFieldModel | |||||
| { | |||||
| string Name { get; set; } | |||||
| string Value { get; set; } | |||||
| bool Inline { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,14 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| public interface IMessageActivityModel | |||||
| { | |||||
| MessageActivityType? Type { get; set; } | |||||
| string PartyId { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,48 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| public interface IMessageModel : IEntityModel<ulong> | |||||
| { | |||||
| MessageType Type { get; set; } | |||||
| ulong ChannelId { get; set; } | |||||
| ulong? GuildId { get; set; } | |||||
| ulong AuthorId { get; set; } | |||||
| bool IsWebhookMessage { get; set; } | |||||
| string Content { get; set; } | |||||
| long Timestamp { get; set; } | |||||
| long? EditedTimestamp { get; set; } | |||||
| bool IsTextToSpeech { get; set; } | |||||
| bool MentionEveryone { get; set; } | |||||
| ulong[] UserMentionIds { get; set; } | |||||
| ulong[] RoleMentionIds { get; set; } | |||||
| IAttachmentModel[] Attachments { get; set; } | |||||
| IEmbedModel[] Embeds { get; set; } | |||||
| IReactionMetadataModel[] Reactions { get; set; } | |||||
| bool Pinned { get; set; } | |||||
| IMessageActivityModel Activity { get; set; } | |||||
| IPartialApplicationModel Application { get; set; } | |||||
| ulong? ApplicationId { get; set; } | |||||
| // message reference | |||||
| ulong? ReferenceMessageId { get; set; } | |||||
| ulong? ReferenceMessageChannelId { get; set; } | |||||
| ulong? ReferenceMessageGuildId { get; set; } | |||||
| MessageFlags Flags { get; set; } | |||||
| // interaction | |||||
| ulong? InteractionId { get; set; } | |||||
| string InteractionName { get; set; } | |||||
| InteractionType? InteractionType { get; set; } | |||||
| ulong? InteractionUserId { get; set; } | |||||
| IMessageComponentModel[] Components { get; set; } | |||||
| IStickerItemModel[] Stickers { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,14 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| public interface IReactionMetadataModel | |||||
| { | |||||
| IEmojiModel Emoji { get; set; } | |||||
| ulong[] Users { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,15 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| public interface IStickerItemModel | |||||
| { | |||||
| ulong Id { get; set; } | |||||
| string Name { get; set; } | |||||
| StickerFormatType Format { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,48 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| public interface IActivityModel | |||||
| { | |||||
| string Id { get; set; } | |||||
| string Url { get; set; } | |||||
| string Name { get; set; } | |||||
| ActivityType Type { get; set; } | |||||
| string Details { get; set; } | |||||
| string State { get; set; } | |||||
| ActivityProperties Flags { get; set; } | |||||
| DateTimeOffset CreatedAt { get; set; } | |||||
| IEmojiModel Emoji { get; set; } | |||||
| ulong? ApplicationId { get; set; } | |||||
| string SyncId { get; set; } | |||||
| string SessionId { get; set; } | |||||
| #region Assets | |||||
| string LargeImage { get; set; } | |||||
| string LargeText { get; set; } | |||||
| string SmallImage { get; set; } | |||||
| string SmallText { get; set; } | |||||
| #endregion | |||||
| #region Party | |||||
| string PartyId { get; set; } | |||||
| long[] PartySize { get; set; } | |||||
| #endregion | |||||
| #region Secrets | |||||
| string JoinSecret { get; set; } | |||||
| string SpectateSecret { get; set; } | |||||
| string MatchSecret { get; set; } | |||||
| #endregion | |||||
| #region Timestamps | |||||
| DateTimeOffset? TimestampStart { get; set; } | |||||
| DateTimeOffset? TimestampEnd { get; set; } | |||||
| #endregion | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,17 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| public interface IPresenceModel : IEntityModel<ulong> | |||||
| { | |||||
| ulong UserId { get; set; } | |||||
| ulong? GuildId { get; set; } | |||||
| UserStatus Status { get; set; } | |||||
| ClientType[] ActiveClients { get; set; } | |||||
| IActivityModel[] Activities { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,19 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| public interface ICurrentUserModel : IUserModel | |||||
| { | |||||
| bool? IsVerified { get; set; } | |||||
| string Email { get; set; } | |||||
| bool? IsMfaEnabled { get; set; } | |||||
| UserProperties Flags { get; set; } | |||||
| PremiumType PremiumType { get; set; } | |||||
| string Locale { get; set; } | |||||
| UserProperties PublicFlags { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,22 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| public interface IMemberModel : IEntityModel<ulong> | |||||
| { | |||||
| //IUserModel User { get; set; } | |||||
| string Nickname { get; set; } | |||||
| string GuildAvatar { get; set; } | |||||
| ulong[] Roles { get; set; } | |||||
| DateTimeOffset? JoinedAt { get; set; } | |||||
| DateTimeOffset? PremiumSince { get; set; } | |||||
| bool IsDeaf { get; set; } | |||||
| bool IsMute { get; set; } | |||||
| bool? IsPending { get; set; } | |||||
| DateTimeOffset? CommunicationsDisabledUntil { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,14 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| public interface IThreadMemberModel : IEntityModel<ulong> | |||||
| { | |||||
| ulong? ThreadId { get; set; } | |||||
| DateTimeOffset JoinedAt { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,16 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| public interface IUserModel : IEntityModel<ulong> | |||||
| { | |||||
| string Username { get; set; } | |||||
| string Discriminator { get; set; } | |||||
| bool? IsBot { get; set; } | |||||
| string Avatar { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -107,6 +107,8 @@ namespace Discord | |||||
| /// </returns> | /// </returns> | ||||
| public string TrackUrl { get; internal set; } | public string TrackUrl { get; internal set; } | ||||
| internal string AlbumArt { get; set; } | |||||
| internal SpotifyGame() { } | internal SpotifyGame() { } | ||||
| /// <summary> | /// <summary> | ||||
| @@ -24,6 +24,13 @@ namespace Discord | |||||
| /// </returns> | /// </returns> | ||||
| public bool RequireColons { get; } | public bool RequireColons { get; } | ||||
| /// <summary> | /// <summary> | ||||
| /// Gets whether or not the emote is available. | |||||
| /// </summary> | |||||
| /// <remarks> | |||||
| /// An emote can be unavailable if the guild has lost its boost status. | |||||
| /// </remarks> | |||||
| public bool IsAvailable { get; } | |||||
| /// <summary> | |||||
| /// Gets the roles that are allowed to use this emoji. | /// Gets the roles that are allowed to use this emoji. | ||||
| /// </summary> | /// </summary> | ||||
| /// <returns> | /// <returns> | ||||
| @@ -39,12 +46,13 @@ namespace Discord | |||||
| /// </returns> | /// </returns> | ||||
| public ulong? CreatorId { get; } | public ulong? CreatorId { get; } | ||||
| internal GuildEmote(ulong id, string name, bool animated, bool isManaged, bool requireColons, IReadOnlyList<ulong> roleIds, ulong? userId) : base(id, name, animated) | |||||
| internal GuildEmote(ulong id, string name, bool animated, bool isManaged, bool isAvailable, bool requireColons, IReadOnlyList<ulong> roleIds, ulong? userId) : base(id, name, animated) | |||||
| { | { | ||||
| IsManaged = isManaged; | IsManaged = isManaged; | ||||
| RequireColons = requireColons; | RequireColons = requireColons; | ||||
| RoleIds = roleIds; | RoleIds = roleIds; | ||||
| CreatorId = userId; | CreatorId = userId; | ||||
| IsAvailable = isAvailable; | |||||
| } | } | ||||
| private string DebuggerDisplay => $"{Name} ({Id})"; | private string DebuggerDisplay => $"{Name} ({Id})"; | ||||
| @@ -46,6 +46,11 @@ namespace Discord | |||||
| /// </returns> | /// </returns> | ||||
| bool MentionedEveryone { get; } | bool MentionedEveryone { get; } | ||||
| /// <summary> | /// <summary> | ||||
| /// If the message is a <see cref="MessageType.ApplicationCommand"/> or application-owned webhook, | |||||
| /// this is the id of the application. | |||||
| /// </summary> | |||||
| ulong? ApplicationId { get; } | |||||
| /// <summary> | |||||
| /// Gets the content for this message. | /// Gets the content for this message. | ||||
| /// </summary> | /// </summary> | ||||
| /// <returns> | /// <returns> | ||||
| @@ -10,7 +10,7 @@ namespace Discord | |||||
| /// Represents a partial <see cref="IDiscordInteraction"/> within a message. | /// Represents a partial <see cref="IDiscordInteraction"/> within a message. | ||||
| /// </summary> | /// </summary> | ||||
| /// <typeparam name="TUser">The type of the user.</typeparam> | /// <typeparam name="TUser">The type of the user.</typeparam> | ||||
| public class MessageInteraction<TUser> : IMessageInteraction where TUser : IUser | |||||
| public class MessageInteraction<TUser> : IMessageInteraction where TUser : class, IUser | |||||
| { | { | ||||
| /// <summary> | /// <summary> | ||||
| /// Gets the snowflake id of the interaction. | /// Gets the snowflake id of the interaction. | ||||
| @@ -30,14 +30,36 @@ namespace Discord | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the <typeparamref name="TUser"/> who invoked the interaction. | /// Gets the <typeparamref name="TUser"/> who invoked the interaction. | ||||
| /// </summary> | /// </summary> | ||||
| public TUser User { get; } | |||||
| /// <remarks> | |||||
| /// When this property is a SocketUser, the get accessor will attempt to preform a | |||||
| /// synchronous cache lookup. | |||||
| /// </remarks> | |||||
| public TUser User | |||||
| => _user ?? (_userLookup != null ? _userLookup(UserId) : null); | |||||
| /// <summary> | |||||
| /// Gets the id of the user who invoked the interaction. | |||||
| /// </summary> | |||||
| public ulong UserId { get; } | |||||
| private readonly TUser _user; | |||||
| private readonly Func<ulong, TUser> _userLookup; | |||||
| internal MessageInteraction(ulong id, InteractionType type, string name, TUser user) | internal MessageInteraction(ulong id, InteractionType type, string name, TUser user) | ||||
| { | { | ||||
| Id = id; | Id = id; | ||||
| Type = type; | Type = type; | ||||
| Name = name; | Name = name; | ||||
| User = user; | |||||
| _user = user; | |||||
| UserId = user.Id; | |||||
| } | |||||
| internal MessageInteraction(ulong id, InteractionType type, string name, ulong userId, Func<ulong, TUser> lookup) | |||||
| { | |||||
| Id = id; | |||||
| Type = type; | |||||
| Name = name; | |||||
| UserId = userId; | |||||
| _userLookup = lookup; | |||||
| } | } | ||||
| IUser IMessageInteraction.User => User; | IUser IMessageInteraction.User => User; | ||||
| @@ -56,5 +56,9 @@ namespace Discord | |||||
| public static T? ToNullable<T>(this Optional<T> val) | public static T? ToNullable<T>(this Optional<T> val) | ||||
| where T : struct | where T : struct | ||||
| => val.IsSpecified ? val.Value : null; | => val.IsSpecified ? val.Value : null; | ||||
| public static Optional<T> ToOptional<T>(this T? value) | |||||
| where T : struct | |||||
| => value.HasValue ? new Optional<T>(value.Value) : new(); | |||||
| } | } | ||||
| } | } | ||||
| @@ -3,7 +3,7 @@ using System.Linq; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| internal class ActionRowComponent : IMessageComponent | |||||
| internal class ActionRowComponent : IMessageComponent, IMessageComponentModel | |||||
| { | { | ||||
| [JsonProperty("type")] | [JsonProperty("type")] | ||||
| public ComponentType Type { get; set; } | public ComponentType Type { get; set; } | ||||
| @@ -29,5 +29,27 @@ namespace Discord.API | |||||
| [JsonIgnore] | [JsonIgnore] | ||||
| string IMessageComponent.CustomId => null; | string IMessageComponent.CustomId => null; | ||||
| ComponentType IMessageComponentModel.Type { get => Type; set => throw new System.NotSupportedException(); } | |||||
| IMessageComponentModel[] IMessageComponentModel.Components { get => Components.Select(x => x as IMessageComponentModel).ToArray(); set => throw new System.NotSupportedException(); } // cursed hack here | |||||
| #region unused | |||||
| string IMessageComponentModel.CustomId { get => null; set => throw new System.NotSupportedException(); } | |||||
| bool? IMessageComponentModel.Disabled { get => null; set => throw new System.NotSupportedException(); } | |||||
| ButtonStyle? IMessageComponentModel.Style { get => null; set => throw new System.NotSupportedException(); } | |||||
| string IMessageComponentModel.Label { get => null; set => throw new System.NotSupportedException(); } | |||||
| ulong? IMessageComponentModel.EmojiId { get => null; set => throw new System.NotSupportedException(); } | |||||
| string IMessageComponentModel.EmojiName { get => null; set => throw new System.NotSupportedException(); } | |||||
| bool? IMessageComponentModel.EmojiAnimated { get => null; set => throw new System.NotSupportedException(); } | |||||
| string IMessageComponentModel.Url { get => null; set => throw new System.NotSupportedException(); } | |||||
| IMessageComponentOptionModel[] IMessageComponentModel.Options { get => null; set => throw new System.NotSupportedException(); } | |||||
| string IMessageComponentModel.Placeholder { get => null; set => throw new System.NotSupportedException(); } | |||||
| int? IMessageComponentModel.MinValues { get => null; set => throw new System.NotSupportedException(); } | |||||
| int? IMessageComponentModel.MaxValues { get => null; set => throw new System.NotSupportedException(); } | |||||
| int? IMessageComponentModel.MinLength { get => null; set => throw new System.NotSupportedException(); } | |||||
| int? IMessageComponentModel.MaxLength { get => null; set => throw new System.NotSupportedException(); } | |||||
| bool? IMessageComponentModel.Required { get => null; set => throw new System.NotSupportedException(); } | |||||
| string IMessageComponentModel.Value { get => null; set => throw new System.NotSupportedException(); } | |||||
| #endregion | |||||
| } | } | ||||
| } | } | ||||
| @@ -2,7 +2,7 @@ using Newtonsoft.Json; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| internal class Attachment | |||||
| internal class Attachment : IAttachmentModel | |||||
| { | { | ||||
| [JsonProperty("id")] | [JsonProperty("id")] | ||||
| public ulong Id { get; set; } | public ulong Id { get; set; } | ||||
| @@ -24,5 +24,16 @@ namespace Discord.API | |||||
| public Optional<int> Width { get; set; } | public Optional<int> Width { get; set; } | ||||
| [JsonProperty("ephemeral")] | [JsonProperty("ephemeral")] | ||||
| public Optional<bool> Ephemeral { get; set; } | public Optional<bool> Ephemeral { get; set; } | ||||
| string IAttachmentModel.FileName { get => Filename; set => throw new System.NotSupportedException(); } | |||||
| string IAttachmentModel.Description { get => Description.GetValueOrDefault(); set => throw new System.NotSupportedException(); } | |||||
| string IAttachmentModel.ContentType { get => ContentType.GetValueOrDefault(); set => throw new System.NotSupportedException(); } | |||||
| int IAttachmentModel.Size { get => Size; set => throw new System.NotSupportedException(); } | |||||
| string IAttachmentModel.Url { get => Url; set => throw new System.NotSupportedException(); } | |||||
| string IAttachmentModel.ProxyUrl { get => ProxyUrl; set => throw new System.NotSupportedException(); } | |||||
| int? IAttachmentModel.Height { get => Height.ToNullable(); set => throw new System.NotSupportedException(); } | |||||
| int? IAttachmentModel.Width { get => Width.ToNullable(); set => throw new System.NotSupportedException(); } | |||||
| bool IAttachmentModel.Ephemeral { get => Ephemeral.GetValueOrDefault(); set => throw new System.NotSupportedException(); } | |||||
| ulong IEntityModel<ulong>.Id { get => Id; set => throw new System.NotSupportedException(); } | |||||
| } | } | ||||
| } | } | ||||
| @@ -2,7 +2,7 @@ using Newtonsoft.Json; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| internal class ButtonComponent : IMessageComponent | |||||
| internal class ButtonComponent : IMessageComponent, IMessageComponentModel | |||||
| { | { | ||||
| [JsonProperty("type")] | [JsonProperty("type")] | ||||
| public ComponentType Type { get; set; } | public ComponentType Type { get; set; } | ||||
| @@ -59,5 +59,27 @@ namespace Discord.API | |||||
| [JsonIgnore] | [JsonIgnore] | ||||
| string IMessageComponent.CustomId => CustomId.GetValueOrDefault(); | string IMessageComponent.CustomId => CustomId.GetValueOrDefault(); | ||||
| ComponentType IMessageComponentModel.Type { get => Type; set => throw new System.NotSupportedException(); } | |||||
| string IMessageComponentModel.CustomId { get => CustomId.GetValueOrDefault(); set => throw new System.NotSupportedException(); } | |||||
| bool? IMessageComponentModel.Disabled { get => Disabled.ToNullable(); set => throw new System.NotSupportedException(); } | |||||
| ButtonStyle? IMessageComponentModel.Style { get => Style; set => throw new System.NotSupportedException(); } | |||||
| string IMessageComponentModel.Label { get => Label.GetValueOrDefault(); set => throw new System.NotSupportedException(); } | |||||
| ulong? IMessageComponentModel.EmojiId { get => Emote.GetValueOrDefault()?.Id; set => throw new System.NotSupportedException(); } | |||||
| string IMessageComponentModel.EmojiName { get => Emote.GetValueOrDefault()?.Name; set => throw new System.NotSupportedException(); } | |||||
| bool? IMessageComponentModel.EmojiAnimated { get => Emote.GetValueOrDefault()?.Animated; set => throw new System.NotSupportedException(); } | |||||
| string IMessageComponentModel.Url { get => Url.GetValueOrDefault(); set => throw new System.NotSupportedException(); } | |||||
| #region unused | |||||
| IMessageComponentOptionModel[] IMessageComponentModel.Options { get => null; set => throw new System.NotSupportedException(); } | |||||
| string IMessageComponentModel.Placeholder { get => null; set => throw new System.NotSupportedException(); } | |||||
| int? IMessageComponentModel.MinValues { get => null; set => throw new System.NotSupportedException(); } | |||||
| int? IMessageComponentModel.MaxValues { get => null; set => throw new System.NotSupportedException(); } | |||||
| IMessageComponentModel[] IMessageComponentModel.Components { get => null; set => throw new System.NotSupportedException(); } | |||||
| int? IMessageComponentModel.MinLength { get => null; set => throw new System.NotSupportedException(); } | |||||
| int? IMessageComponentModel.MaxLength { get => null; set => throw new System.NotSupportedException(); } | |||||
| bool? IMessageComponentModel.Required { get => null; set => throw new System.NotSupportedException(); } | |||||
| string IMessageComponentModel.Value { get => null; set => throw new System.NotSupportedException(); } | |||||
| #endregion | |||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,70 @@ | |||||
| using Newtonsoft.Json; | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.API | |||||
| { | |||||
| internal class CurrentUser : User, ICurrentUserModel | |||||
| { | |||||
| [JsonProperty("verified")] | |||||
| public Optional<bool> Verified { get; set; } | |||||
| [JsonProperty("email")] | |||||
| public Optional<string> Email { get; set; } | |||||
| [JsonProperty("mfa_enabled")] | |||||
| public Optional<bool> MfaEnabled { get; set; } | |||||
| [JsonProperty("flags")] | |||||
| public Optional<UserProperties> Flags { get; set; } | |||||
| [JsonProperty("premium_type")] | |||||
| public Optional<PremiumType> PremiumType { get; set; } | |||||
| [JsonProperty("locale")] | |||||
| public Optional<string> Locale { get; set; } | |||||
| [JsonProperty("public_flags")] | |||||
| public Optional<UserProperties> PublicFlags { get; set; } | |||||
| // ICurrentUserModel | |||||
| bool? ICurrentUserModel.IsVerified | |||||
| { | |||||
| get => Verified.ToNullable(); | |||||
| set => throw new NotSupportedException(); | |||||
| } | |||||
| string ICurrentUserModel.Email | |||||
| { | |||||
| get => Email.GetValueOrDefault(); | |||||
| set => throw new NotSupportedException(); | |||||
| } | |||||
| bool? ICurrentUserModel.IsMfaEnabled | |||||
| { | |||||
| get => MfaEnabled.ToNullable(); | |||||
| set => throw new NotSupportedException(); | |||||
| } | |||||
| UserProperties ICurrentUserModel.Flags | |||||
| { | |||||
| get => Flags.GetValueOrDefault(); | |||||
| set => throw new NotSupportedException(); | |||||
| } | |||||
| PremiumType ICurrentUserModel.PremiumType | |||||
| { | |||||
| get => PremiumType.GetValueOrDefault(); | |||||
| set => throw new NotSupportedException(); | |||||
| } | |||||
| string ICurrentUserModel.Locale | |||||
| { | |||||
| get => Locale.GetValueOrDefault(); | |||||
| set => throw new NotSupportedException(); | |||||
| } | |||||
| UserProperties ICurrentUserModel.PublicFlags | |||||
| { | |||||
| get => PublicFlags.GetValueOrDefault(); | |||||
| set => throw new NotSupportedException(); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -4,7 +4,7 @@ using Discord.Net.Converters; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| internal class Embed | |||||
| internal class Embed : IEmbedModel | |||||
| { | { | ||||
| [JsonProperty("title")] | [JsonProperty("title")] | ||||
| public string Title { get; set; } | public string Title { get; set; } | ||||
| @@ -32,5 +32,15 @@ namespace Discord.API | |||||
| public Optional<EmbedProvider> Provider { get; set; } | public Optional<EmbedProvider> Provider { get; set; } | ||||
| [JsonProperty("fields")] | [JsonProperty("fields")] | ||||
| public Optional<EmbedField[]> Fields { get; set; } | public Optional<EmbedField[]> Fields { get; set; } | ||||
| EmbedType IEmbedModel.Type { get => Type; set => throw new NotSupportedException(); } | |||||
| DateTimeOffset? IEmbedModel.Timestamp { get => Timestamp; set => throw new NotSupportedException(); } | |||||
| IEmbedFooterModel IEmbedModel.Footer { get => Footer.GetValueOrDefault(); set => throw new NotSupportedException(); } | |||||
| IEmbedMediaModel IEmbedModel.Image { get => Image.GetValueOrDefault(); set => throw new NotSupportedException(); } | |||||
| IEmbedMediaModel IEmbedModel.Thumbnail { get => Thumbnail.GetValueOrDefault(); set => throw new NotSupportedException(); } | |||||
| IEmbedMediaModel IEmbedModel.Video { get => Video.GetValueOrDefault(); set => throw new NotSupportedException(); } | |||||
| IEmbedProviderModel IEmbedModel.Provider { get => Provider.GetValueOrDefault(); set => throw new NotSupportedException(); } | |||||
| IEmbedAuthorModel IEmbedModel.Author { get => Author.GetValueOrDefault(); set => throw new NotSupportedException(); } | |||||
| IEmbedFieldModel[] IEmbedModel.Fields { get => Fields.GetValueOrDefault(); set => throw new NotSupportedException(); } | |||||
| } | } | ||||
| } | } | ||||
| @@ -2,7 +2,7 @@ using Newtonsoft.Json; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| internal class EmbedAuthor | |||||
| internal class EmbedAuthor : IEmbedAuthorModel | |||||
| { | { | ||||
| [JsonProperty("name")] | [JsonProperty("name")] | ||||
| public string Name { get; set; } | public string Name { get; set; } | ||||
| @@ -1,8 +1,8 @@ | |||||
| using Newtonsoft.Json; | |||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| internal class EmbedField | |||||
| internal class EmbedField : IEmbedFieldModel | |||||
| { | { | ||||
| [JsonProperty("name")] | [JsonProperty("name")] | ||||
| public string Name { get; set; } | public string Name { get; set; } | ||||
| @@ -2,7 +2,7 @@ using Newtonsoft.Json; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| internal class EmbedFooter | |||||
| internal class EmbedFooter : IEmbedFooterModel | |||||
| { | { | ||||
| [JsonProperty("text")] | [JsonProperty("text")] | ||||
| public string Text { get; set; } | public string Text { get; set; } | ||||
| @@ -2,7 +2,7 @@ using Newtonsoft.Json; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| internal class EmbedImage | |||||
| internal class EmbedImage : IEmbedMediaModel | |||||
| { | { | ||||
| [JsonProperty("url")] | [JsonProperty("url")] | ||||
| public string Url { get; set; } | public string Url { get; set; } | ||||
| @@ -12,5 +12,8 @@ namespace Discord.API | |||||
| public Optional<int> Height { get; set; } | public Optional<int> Height { get; set; } | ||||
| [JsonProperty("width")] | [JsonProperty("width")] | ||||
| public Optional<int> Width { get; set; } | public Optional<int> Width { get; set; } | ||||
| int? IEmbedMediaModel.Height { get => Height.ToNullable(); set => throw new System.NotSupportedException(); } | |||||
| int? IEmbedMediaModel.Width { get => Width.ToNullable(); set => throw new System.NotSupportedException(); } | |||||
| } | } | ||||
| } | } | ||||
| @@ -2,7 +2,7 @@ using Newtonsoft.Json; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| internal class EmbedProvider | |||||
| internal class EmbedProvider : IEmbedProviderModel | |||||
| { | { | ||||
| [JsonProperty("name")] | [JsonProperty("name")] | ||||
| public string Name { get; set; } | public string Name { get; set; } | ||||
| @@ -2,7 +2,7 @@ using Newtonsoft.Json; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| internal class EmbedThumbnail | |||||
| internal class EmbedThumbnail : IEmbedMediaModel | |||||
| { | { | ||||
| [JsonProperty("url")] | [JsonProperty("url")] | ||||
| public string Url { get; set; } | public string Url { get; set; } | ||||
| @@ -12,5 +12,8 @@ namespace Discord.API | |||||
| public Optional<int> Height { get; set; } | public Optional<int> Height { get; set; } | ||||
| [JsonProperty("width")] | [JsonProperty("width")] | ||||
| public Optional<int> Width { get; set; } | public Optional<int> Width { get; set; } | ||||
| int? IEmbedMediaModel.Height { get => Height.ToNullable(); set => throw new System.NotSupportedException(); } | |||||
| int? IEmbedMediaModel.Width { get => Width.ToNullable(); set => throw new System.NotSupportedException(); } | |||||
| } | } | ||||
| } | } | ||||
| @@ -2,13 +2,18 @@ using Newtonsoft.Json; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| internal class EmbedVideo | |||||
| internal class EmbedVideo : IEmbedMediaModel | |||||
| { | { | ||||
| [JsonProperty("url")] | [JsonProperty("url")] | ||||
| public string Url { get; set; } | public string Url { get; set; } | ||||
| [JsonProperty("proxy_url")] | |||||
| public string ProxyUrl { get; set; } | |||||
| [JsonProperty("height")] | [JsonProperty("height")] | ||||
| public Optional<int> Height { get; set; } | public Optional<int> Height { get; set; } | ||||
| [JsonProperty("width")] | [JsonProperty("width")] | ||||
| public Optional<int> Width { get; set; } | public Optional<int> Width { get; set; } | ||||
| int? IEmbedMediaModel.Height { get => Height.ToNullable(); set => throw new System.NotSupportedException(); } | |||||
| int? IEmbedMediaModel.Width { get => Width.ToNullable(); set => throw new System.NotSupportedException(); } | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,8 +1,9 @@ | |||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| using System; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| internal class Emoji | |||||
| internal class Emoji : IEmojiModel | |||||
| { | { | ||||
| [JsonProperty("id")] | [JsonProperty("id")] | ||||
| public ulong? Id { get; set; } | public ulong? Id { get; set; } | ||||
| @@ -16,7 +17,57 @@ namespace Discord.API | |||||
| public bool RequireColons { get; set; } | public bool RequireColons { get; set; } | ||||
| [JsonProperty("managed")] | [JsonProperty("managed")] | ||||
| public bool Managed { get; set; } | public bool Managed { get; set; } | ||||
| [JsonProperty("available")] | |||||
| public Optional<bool> Available { get; set; } | |||||
| [JsonProperty("user")] | [JsonProperty("user")] | ||||
| public Optional<User> User { get; set; } | public Optional<User> User { get; set; } | ||||
| ulong? IEmojiModel.Id | |||||
| { | |||||
| get => Id; | |||||
| set => throw new NotSupportedException(); | |||||
| } | |||||
| string IEmojiModel.Name | |||||
| { | |||||
| get => Name; | |||||
| set => throw new NotSupportedException(); | |||||
| } | |||||
| ulong[] IEmojiModel.Roles | |||||
| { | |||||
| get => Roles; | |||||
| set => throw new NotSupportedException(); | |||||
| } | |||||
| bool IEmojiModel.RequireColons | |||||
| { | |||||
| get => RequireColons; | |||||
| set => throw new NotSupportedException(); | |||||
| } | |||||
| bool IEmojiModel.IsManaged | |||||
| { | |||||
| get => Managed; | |||||
| set => throw new NotSupportedException(); | |||||
| } | |||||
| bool IEmojiModel.IsAnimated | |||||
| { | |||||
| get => Animated.GetValueOrDefault(); | |||||
| set => throw new NotSupportedException(); | |||||
| } | |||||
| bool IEmojiModel.IsAvailable | |||||
| { | |||||
| get => Available.GetValueOrDefault(); | |||||
| set => throw new NotSupportedException(); | |||||
| } | |||||
| ulong? IEmojiModel.CreatorId | |||||
| { | |||||
| get => User.GetValueOrDefault()?.Id; | |||||
| set => throw new NotSupportedException(); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,10 +1,11 @@ | |||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| using Newtonsoft.Json.Serialization; | using Newtonsoft.Json.Serialization; | ||||
| using System; | |||||
| using System.Runtime.Serialization; | using System.Runtime.Serialization; | ||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| internal class Game | |||||
| internal class Game : IActivityModel | |||||
| { | { | ||||
| [JsonProperty("name")] | [JsonProperty("name")] | ||||
| public string Name { get; set; } | public string Name { get; set; } | ||||
| @@ -32,7 +33,7 @@ namespace Discord.API | |||||
| public Optional<string> SyncId { get; set; } | public Optional<string> SyncId { get; set; } | ||||
| [JsonProperty("session_id")] | [JsonProperty("session_id")] | ||||
| public Optional<string> SessionId { get; set; } | public Optional<string> SessionId { get; set; } | ||||
| [JsonProperty("Flags")] | |||||
| [JsonProperty("flags")] | |||||
| public Optional<ActivityProperties> Flags { get; set; } | public Optional<ActivityProperties> Flags { get; set; } | ||||
| [JsonProperty("id")] | [JsonProperty("id")] | ||||
| public Optional<string> Id { get; set; } | public Optional<string> Id { get; set; } | ||||
| @@ -40,6 +41,100 @@ namespace Discord.API | |||||
| public Optional<Emoji> Emoji { get; set; } | public Optional<Emoji> Emoji { get; set; } | ||||
| [JsonProperty("created_at")] | [JsonProperty("created_at")] | ||||
| public Optional<long> CreatedAt { get; set; } | public Optional<long> CreatedAt { get; set; } | ||||
| string IActivityModel.Id { | |||||
| get => Id.GetValueOrDefault(); set => throw new NotSupportedException(); | |||||
| } | |||||
| string IActivityModel.Url { | |||||
| get => StreamUrl.GetValueOrDefault(); set => throw new NotSupportedException(); | |||||
| } | |||||
| string IActivityModel.State { | |||||
| get => State.GetValueOrDefault(); set => throw new NotSupportedException(); | |||||
| } | |||||
| IEmojiModel IActivityModel.Emoji { | |||||
| get => Emoji.GetValueOrDefault(); set => throw new NotSupportedException(); | |||||
| } | |||||
| string IActivityModel.Name { | |||||
| get => Name; set => throw new NotSupportedException(); | |||||
| } | |||||
| ActivityType IActivityModel.Type { | |||||
| get => Type.GetValueOrDefault().GetValueOrDefault(); set => throw new NotSupportedException(); | |||||
| } | |||||
| ActivityProperties IActivityModel.Flags { | |||||
| get => Flags.GetValueOrDefault(); set => throw new NotSupportedException(); | |||||
| } | |||||
| string IActivityModel.Details { | |||||
| get => Details.GetValueOrDefault(); set => throw new NotSupportedException(); | |||||
| } | |||||
| DateTimeOffset IActivityModel.CreatedAt { | |||||
| get => DateTimeOffset.FromUnixTimeMilliseconds(CreatedAt.GetValueOrDefault()); set => throw new NotSupportedException(); | |||||
| } | |||||
| ulong? IActivityModel.ApplicationId { | |||||
| get => ApplicationId.ToNullable(); set => throw new NotSupportedException(); | |||||
| } | |||||
| string IActivityModel.SyncId { | |||||
| get => SyncId.GetValueOrDefault(); set => throw new NotSupportedException(); | |||||
| } | |||||
| string IActivityModel.SessionId { | |||||
| get => SessionId.GetValueOrDefault(); set => throw new NotSupportedException(); | |||||
| } | |||||
| string IActivityModel.LargeImage { | |||||
| get => Assets.GetValueOrDefault()?.LargeImage.GetValueOrDefault(); set => throw new NotSupportedException(); | |||||
| } | |||||
| string IActivityModel.LargeText { | |||||
| get => Assets.GetValueOrDefault()?.LargeText.GetValueOrDefault(); set => throw new NotSupportedException(); | |||||
| } | |||||
| string IActivityModel.SmallImage { | |||||
| get => Assets.GetValueOrDefault()?.SmallImage.GetValueOrDefault(); set => throw new NotSupportedException(); | |||||
| } | |||||
| string IActivityModel.SmallText { | |||||
| get => Assets.GetValueOrDefault()?.SmallText.GetValueOrDefault(); set => throw new NotSupportedException(); | |||||
| } | |||||
| string IActivityModel.PartyId { | |||||
| get => Party.GetValueOrDefault()?.Id; set => throw new NotSupportedException(); | |||||
| } | |||||
| long[] IActivityModel.PartySize { | |||||
| get => Party.GetValueOrDefault()?.Size; set => throw new NotSupportedException(); | |||||
| } | |||||
| string IActivityModel.JoinSecret { | |||||
| get => Secrets.GetValueOrDefault()?.Join; set => throw new NotSupportedException(); | |||||
| } | |||||
| string IActivityModel.SpectateSecret { | |||||
| get => Secrets.GetValueOrDefault()?.Spectate; set => throw new NotSupportedException(); | |||||
| } | |||||
| string IActivityModel.MatchSecret { | |||||
| get => Secrets.GetValueOrDefault()?.Match; set => throw new NotSupportedException(); | |||||
| } | |||||
| DateTimeOffset? IActivityModel.TimestampStart { | |||||
| get => Timestamps.GetValueOrDefault()?.Start.ToNullable(); set => throw new NotSupportedException(); | |||||
| } | |||||
| DateTimeOffset? IActivityModel.TimestampEnd { | |||||
| get => Timestamps.GetValueOrDefault()?.End.ToNullable(); set => throw new NotSupportedException(); | |||||
| } | |||||
| //[JsonProperty("buttons")] | //[JsonProperty("buttons")] | ||||
| //public Optional<RichPresenceButton[]> Buttons { get; set; } | //public Optional<RichPresenceButton[]> Buttons { get; set; } | ||||
| @@ -3,7 +3,7 @@ using System; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| internal class GuildMember | |||||
| internal class GuildMember : IMemberModel | |||||
| { | { | ||||
| [JsonProperty("user")] | [JsonProperty("user")] | ||||
| public User User { get; set; } | public User User { get; set; } | ||||
| @@ -25,5 +25,46 @@ namespace Discord.API | |||||
| public Optional<DateTimeOffset?> PremiumSince { get; set; } | public Optional<DateTimeOffset?> PremiumSince { get; set; } | ||||
| [JsonProperty("communication_disabled_until")] | [JsonProperty("communication_disabled_until")] | ||||
| public Optional<DateTimeOffset?> TimedOutUntil { get; set; } | public Optional<DateTimeOffset?> TimedOutUntil { get; set; } | ||||
| // IMemberModel | |||||
| string IMemberModel.Nickname { | |||||
| get => Nick.GetValueOrDefault(); set => throw new NotSupportedException(); | |||||
| } | |||||
| string IMemberModel.GuildAvatar { | |||||
| get => Avatar.GetValueOrDefault(); set => throw new NotSupportedException(); | |||||
| } | |||||
| ulong[] IMemberModel.Roles { | |||||
| get => Roles.GetValueOrDefault(Array.Empty<ulong>()); set => throw new NotSupportedException(); | |||||
| } | |||||
| DateTimeOffset? IMemberModel.JoinedAt { | |||||
| get => JoinedAt.ToNullable(); set => throw new NotSupportedException(); | |||||
| } | |||||
| DateTimeOffset? IMemberModel.PremiumSince { | |||||
| get => PremiumSince.GetValueOrDefault(); set => throw new NotSupportedException(); | |||||
| } | |||||
| bool IMemberModel.IsDeaf { | |||||
| get => Deaf.GetValueOrDefault(false); set => throw new NotSupportedException(); | |||||
| } | |||||
| bool IMemberModel.IsMute { | |||||
| get => Mute.GetValueOrDefault(false); set => throw new NotSupportedException(); | |||||
| } | |||||
| bool? IMemberModel.IsPending { | |||||
| get => Pending.ToNullable(); set => throw new NotSupportedException(); | |||||
| } | |||||
| DateTimeOffset? IMemberModel.CommunicationsDisabledUntil { | |||||
| get => TimedOutUntil.GetValueOrDefault(); set => throw new NotSupportedException(); | |||||
| } | |||||
| ulong IEntityModel<ulong>.Id { | |||||
| get => User.Id; set => throw new NotSupportedException(); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,9 +1,10 @@ | |||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| using System; | using System; | ||||
| using System.Linq; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| internal class Message | |||||
| internal class Message : IMessageModel | |||||
| { | { | ||||
| [JsonProperty("id")] | [JsonProperty("id")] | ||||
| public ulong Id { get; set; } | public ulong Id { get; set; } | ||||
| @@ -49,6 +50,8 @@ namespace Discord.API | |||||
| // sent with Rich Presence-related chat embeds | // sent with Rich Presence-related chat embeds | ||||
| [JsonProperty("application")] | [JsonProperty("application")] | ||||
| public Optional<MessageApplication> Application { get; set; } | public Optional<MessageApplication> Application { get; set; } | ||||
| [JsonProperty("application_id")] | |||||
| public Optional<ulong> ApplicationId { get; set; } | |||||
| [JsonProperty("message_reference")] | [JsonProperty("message_reference")] | ||||
| public Optional<MessageReference> Reference { get; set; } | public Optional<MessageReference> Reference { get; set; } | ||||
| [JsonProperty("flags")] | [JsonProperty("flags")] | ||||
| @@ -62,5 +65,35 @@ namespace Discord.API | |||||
| public Optional<MessageInteraction> Interaction { get; set; } | public Optional<MessageInteraction> Interaction { get; set; } | ||||
| [JsonProperty("sticker_items")] | [JsonProperty("sticker_items")] | ||||
| public Optional<StickerItem[]> StickerItems { get; set; } | public Optional<StickerItem[]> StickerItems { get; set; } | ||||
| MessageType IMessageModel.Type { get => Type; set => throw new NotSupportedException(); } | |||||
| ulong IMessageModel.ChannelId { get => ChannelId; set => throw new NotSupportedException(); } | |||||
| ulong? IMessageModel.GuildId { get => GuildId.ToNullable(); set => throw new NotSupportedException(); } | |||||
| ulong IMessageModel.AuthorId { get => Author.IsSpecified ? Author.Value.Id : Member.IsSpecified ? Member.Value.User.Id : WebhookId.GetValueOrDefault(); set => throw new NotSupportedException(); } | |||||
| bool IMessageModel.IsWebhookMessage { get => WebhookId.IsSpecified; set => throw new NotSupportedException(); } | |||||
| string IMessageModel.Content { get => Content.GetValueOrDefault(); set => throw new NotSupportedException(); } | |||||
| DateTimeOffset IMessageModel.Timestamp { get => Timestamp.Value; set => throw new NotSupportedException(); } // might break? | |||||
| DateTimeOffset? IMessageModel.EditedTimestamp { get => Timestamp.ToNullable(); set => throw new NotSupportedException(); } | |||||
| bool IMessageModel.IsTextToSpeech { get => IsTextToSpeech.GetValueOrDefault(); set => throw new NotSupportedException(); } | |||||
| bool IMessageModel.MentionEveryone { get => MentionEveryone.GetValueOrDefault(); set => throw new NotSupportedException(); } | |||||
| ulong[] IMessageModel.UserMentionIds { get => UserMentions.IsSpecified ? UserMentions.Value.Select(x => x.Id).ToArray() : Array.Empty<ulong>(); set => throw new NotSupportedException(); } | |||||
| IAttachmentModel[] IMessageModel.Attachments { get => Attachments.GetValueOrDefault(Array.Empty<Attachment>()); set => throw new NotSupportedException(); } | |||||
| IEmbedModel[] IMessageModel.Embeds { get => Embeds.GetValueOrDefault(Array.Empty<Embed>()); set => throw new NotSupportedException(); } | |||||
| IReactionMetadataModel[] IMessageModel.Reactions { get => Reactions.GetValueOrDefault(Array.Empty<Reaction>()); set => throw new NotSupportedException(); } | |||||
| bool IMessageModel.Pinned { get => Pinned.GetValueOrDefault(); set => throw new NotSupportedException(); } | |||||
| IMessageActivityModel IMessageModel.Activity { get => Activity.GetValueOrDefault(); set => throw new NotSupportedException(); } | |||||
| IPartialApplicationModel IMessageModel.Application { get => Application.GetValueOrDefault(); set => throw new NotSupportedException(); } | |||||
| ulong? IMessageModel.ApplicationId { get => ApplicationId.ToNullable(); set => throw new NotSupportedException(); } | |||||
| ulong? IMessageModel.ReferenceMessageId { get => ReferencedMessage.GetValueOrDefault()?.Id; set => throw new NotSupportedException(); } | |||||
| ulong? IMessageModel.ReferenceMessageChannelId { get => ReferencedMessage.GetValueOrDefault()?.ChannelId; set => throw new NotSupportedException(); } | |||||
| MessageFlags IMessageModel.Flags { get => Flags.GetValueOrDefault(); set => throw new NotSupportedException(); } | |||||
| ulong? IMessageModel.InteractionId { get => Interaction.GetValueOrDefault()?.Id; set => throw new NotSupportedException(); } | |||||
| string IMessageModel.InteractionName { get => Interaction.GetValueOrDefault()?.Name; set => throw new NotSupportedException(); } | |||||
| InteractionType? IMessageModel.InteractionType { get => Interaction.GetValueOrDefault()?.Type; set => throw new NotSupportedException(); } | |||||
| ulong? IMessageModel.InteractionUserId { get => Interaction.GetValueOrDefault()?.User.Id; set => throw new NotSupportedException(); } | |||||
| IMessageComponentModel[] IMessageModel.Components { get => Components.GetValueOrDefault(Array.Empty<ActionRowComponent>()); set => throw new NotSupportedException(); } | |||||
| IStickerItemModel[] IMessageModel.Stickers { get => StickerItems.GetValueOrDefault(Array.Empty<StickerItem>()); set => throw new NotSupportedException(); } | |||||
| ulong IEntityModel<ulong>.Id { get => Id; set => throw new NotSupportedException(); } | |||||
| } | } | ||||
| } | } | ||||
| @@ -7,11 +7,14 @@ using System.Threading.Tasks; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| public class MessageActivity | |||||
| public class MessageActivity : IMessageActivityModel | |||||
| { | { | ||||
| [JsonProperty("type")] | [JsonProperty("type")] | ||||
| public Optional<MessageActivityType> Type { get; set; } | public Optional<MessageActivityType> Type { get; set; } | ||||
| [JsonProperty("party_id")] | [JsonProperty("party_id")] | ||||
| public Optional<string> PartyId { get; set; } | public Optional<string> PartyId { get; set; } | ||||
| MessageActivityType? IMessageActivityModel.Type { get => Type.ToNullable(); set => throw new NotSupportedException(); } | |||||
| string IMessageActivityModel.PartyId { get => PartyId.GetValueOrDefault(); set => throw new NotSupportedException(); } | |||||
| } | } | ||||
| } | } | ||||
| @@ -7,7 +7,7 @@ using System.Threading.Tasks; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| public class MessageApplication | |||||
| internal class MessageApplication : IPartialApplicationModel | |||||
| { | { | ||||
| /// <summary> | /// <summary> | ||||
| /// Gets the snowflake ID of the application. | /// Gets the snowflake ID of the application. | ||||
| @@ -34,5 +34,10 @@ namespace Discord.API | |||||
| /// </summary> | /// </summary> | ||||
| [JsonProperty("name")] | [JsonProperty("name")] | ||||
| public string Name { get; set; } | public string Name { get; set; } | ||||
| string IPartialApplicationModel.CoverImage { get => CoverImage; set => throw new NotSupportedException(); } | |||||
| string IPartialApplicationModel.Icon { get => Icon; set => throw new NotSupportedException(); } | |||||
| string IPartialApplicationModel.Name { get => Name; set => throw new NotSupportedException(); } | |||||
| ulong IEntityModel<ulong>.Id { get => Id; set => throw new NotSupportedException(); } | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,10 +1,11 @@ | |||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| using System; | using System; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Linq; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| internal class Presence | |||||
| internal class Presence : IPresenceModel | |||||
| { | { | ||||
| [JsonProperty("user")] | [JsonProperty("user")] | ||||
| public User User { get; set; } | public User User { get; set; } | ||||
| @@ -28,5 +29,28 @@ namespace Discord.API | |||||
| public List<Game> Activities { get; set; } | public List<Game> Activities { get; set; } | ||||
| [JsonProperty("premium_since")] | [JsonProperty("premium_since")] | ||||
| public Optional<DateTimeOffset?> PremiumSince { get; set; } | public Optional<DateTimeOffset?> PremiumSince { get; set; } | ||||
| ulong IPresenceModel.UserId { | |||||
| get => User.Id; set => throw new NotSupportedException(); | |||||
| } | |||||
| ulong? IPresenceModel.GuildId { | |||||
| get => GuildId.ToNullable(); set => throw new NotSupportedException(); | |||||
| } | |||||
| UserStatus IPresenceModel.Status { | |||||
| get => Status; set => throw new NotSupportedException(); | |||||
| } | |||||
| ClientType[] IPresenceModel.ActiveClients { | |||||
| get => ClientStatus.IsSpecified ? ClientStatus.Value.Select(x => (ClientType)Enum.Parse(typeof(ClientType), x.Key, true)).ToArray() : Array.Empty<ClientType>(); set => throw new NotSupportedException(); | |||||
| } | |||||
| IActivityModel[] IPresenceModel.Activities { | |||||
| get => Activities.ToArray(); set => throw new NotSupportedException(); | |||||
| } | |||||
| ulong IEntityModel<ulong>.Id { | |||||
| get => User.Id; set => throw new NotSupportedException(); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,8 +1,8 @@ | |||||
| using Newtonsoft.Json; | |||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| internal class Reaction | |||||
| internal class Reaction : IReactionMetadataModel | |||||
| { | { | ||||
| [JsonProperty("count")] | [JsonProperty("count")] | ||||
| public int Count { get; set; } | public int Count { get; set; } | ||||
| @@ -10,5 +10,9 @@ namespace Discord.API | |||||
| public bool Me { get; set; } | public bool Me { get; set; } | ||||
| [JsonProperty("emoji")] | [JsonProperty("emoji")] | ||||
| public Emoji Emoji { get; set; } | public Emoji Emoji { get; set; } | ||||
| int IReactionMetadataModel.Count { get => Count; set => throw new System.NotSupportedException(); } | |||||
| bool IReactionMetadataModel.Me { get => Me; set => throw new System.NotSupportedException(); } | |||||
| IEmojiModel IReactionMetadataModel.Emoji { get => Emoji; set => throw new System.NotSupportedException(); } | |||||
| } | } | ||||
| } | } | ||||
| @@ -3,7 +3,7 @@ using System.Linq; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| internal class SelectMenuComponent : IMessageComponent | |||||
| internal class SelectMenuComponent : IMessageComponent, IMessageComponentModel | |||||
| { | { | ||||
| [JsonProperty("type")] | [JsonProperty("type")] | ||||
| public ComponentType Type { get; set; } | public ComponentType Type { get; set; } | ||||
| @@ -28,6 +28,7 @@ namespace Discord.API | |||||
| [JsonProperty("values")] | [JsonProperty("values")] | ||||
| public Optional<string[]> Values { get; set; } | public Optional<string[]> Values { get; set; } | ||||
| public SelectMenuComponent() { } | public SelectMenuComponent() { } | ||||
| public SelectMenuComponent(Discord.SelectMenuComponent component) | public SelectMenuComponent(Discord.SelectMenuComponent component) | ||||
| @@ -40,5 +41,27 @@ namespace Discord.API | |||||
| MaxValues = component.MaxValues; | MaxValues = component.MaxValues; | ||||
| Disabled = component.IsDisabled; | Disabled = component.IsDisabled; | ||||
| } | } | ||||
| ComponentType IMessageComponentModel.Type { get => Type; set => throw new System.NotSupportedException(); } | |||||
| string IMessageComponentModel.CustomId { get => CustomId; set => throw new System.NotSupportedException(); } | |||||
| bool? IMessageComponentModel.Disabled { get => Disabled; set => throw new System.NotSupportedException(); } | |||||
| IMessageComponentOptionModel[] IMessageComponentModel.Options { get => Options; set => throw new System.NotSupportedException(); } | |||||
| string IMessageComponentModel.Placeholder { get => Placeholder.GetValueOrDefault(); set => throw new System.NotSupportedException(); } | |||||
| int? IMessageComponentModel.MinValues { get => MinValues; set => throw new System.NotSupportedException(); } | |||||
| int? IMessageComponentModel.MaxValues { get => MaxValues; set => throw new System.NotSupportedException(); } | |||||
| #region unused | |||||
| ButtonStyle? IMessageComponentModel.Style { get => null; set => throw new System.NotSupportedException(); } | |||||
| string IMessageComponentModel.Label { get => null; set => throw new System.NotSupportedException(); } | |||||
| ulong? IMessageComponentModel.EmojiId { get => null; set => throw new System.NotSupportedException(); } | |||||
| string IMessageComponentModel.EmojiName { get => null; set => throw new System.NotSupportedException(); } | |||||
| bool? IMessageComponentModel.EmojiAnimated { get => null; set => throw new System.NotSupportedException(); } | |||||
| string IMessageComponentModel.Url { get => null; set => throw new System.NotSupportedException(); } | |||||
| IMessageComponentModel[] IMessageComponentModel.Components { get => null; set => throw new System.NotSupportedException(); } | |||||
| int? IMessageComponentModel.MinLength { get => null; set => throw new System.NotSupportedException(); } | |||||
| int? IMessageComponentModel.MaxLength { get => null; set => throw new System.NotSupportedException(); } | |||||
| bool? IMessageComponentModel.Required { get => null; set => throw new System.NotSupportedException(); } | |||||
| string IMessageComponentModel.Value { get => null; set => throw new System.NotSupportedException(); } | |||||
| #endregion | |||||
| } | } | ||||
| } | } | ||||
| @@ -2,7 +2,7 @@ using Newtonsoft.Json; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| internal class SelectMenuOption | |||||
| internal class SelectMenuOption : IMessageComponentOptionModel | |||||
| { | { | ||||
| [JsonProperty("label")] | [JsonProperty("label")] | ||||
| public string Label { get; set; } | public string Label { get; set; } | ||||
| @@ -49,5 +49,13 @@ namespace Discord.API | |||||
| Default = option.IsDefault ?? Optional<bool>.Unspecified; | Default = option.IsDefault ?? Optional<bool>.Unspecified; | ||||
| } | } | ||||
| string IMessageComponentOptionModel.Label { get => Label; set => throw new System.NotSupportedException(); } | |||||
| string IMessageComponentOptionModel.Value { get => Value; set => throw new System.NotSupportedException(); } | |||||
| string IMessageComponentOptionModel.Description { get => Description.GetValueOrDefault(); set => throw new System.NotSupportedException(); } | |||||
| ulong? IMessageComponentOptionModel.EmojiId { get => Emoji.GetValueOrDefault()?.Id; set => throw new System.NotSupportedException(); } | |||||
| string IMessageComponentOptionModel.EmojiName { get => Emoji.GetValueOrDefault()?.Name; set => throw new System.NotSupportedException(); } | |||||
| bool? IMessageComponentOptionModel.EmojiAnimated { get => Emoji.GetValueOrDefault()?.Animated; set => throw new System.NotSupportedException(); } | |||||
| bool? IMessageComponentOptionModel.Default { get => Default.ToNullable(); set => throw new System.NotSupportedException(); } | |||||
| } | } | ||||
| } | } | ||||
| @@ -2,7 +2,7 @@ using Newtonsoft.Json; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| internal class StickerItem | |||||
| internal class StickerItem : IStickerItemModel | |||||
| { | { | ||||
| [JsonProperty("id")] | [JsonProperty("id")] | ||||
| public ulong Id { get; set; } | public ulong Id { get; set; } | ||||
| @@ -12,5 +12,10 @@ namespace Discord.API | |||||
| [JsonProperty("format_type")] | [JsonProperty("format_type")] | ||||
| public StickerFormatType FormatType { get; set; } | public StickerFormatType FormatType { get; set; } | ||||
| ulong IStickerItemModel.Id { get => Id; set => throw new System.NotSupportedException(); } | |||||
| string IStickerItemModel.Name { get => Name; set => throw new System.NotSupportedException(); } | |||||
| StickerFormatType IStickerItemModel.Format { get => FormatType; set => throw new System.NotSupportedException(); } | |||||
| } | } | ||||
| } | } | ||||
| @@ -2,7 +2,7 @@ using Newtonsoft.Json; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| internal class TextInputComponent : IMessageComponent | |||||
| internal class TextInputComponent : IMessageComponent, IMessageComponentModel | |||||
| { | { | ||||
| [JsonProperty("type")] | [JsonProperty("type")] | ||||
| public ComponentType Type { get; set; } | public ComponentType Type { get; set; } | ||||
| @@ -45,5 +45,29 @@ namespace Discord.API | |||||
| Required = component.Required ?? Optional<bool>.Unspecified; | Required = component.Required ?? Optional<bool>.Unspecified; | ||||
| Value = component.Value ?? Optional<string>.Unspecified; | Value = component.Value ?? Optional<string>.Unspecified; | ||||
| } | } | ||||
| ComponentType IMessageComponentModel.Type { get => Type; set => throw new System.NotSupportedException(); } | |||||
| string IMessageComponentModel.CustomId { get => CustomId; set => throw new System.NotSupportedException(); } | |||||
| int? IMessageComponentModel.MinLength { get => MinLength.ToNullable(); set => throw new System.NotSupportedException(); } | |||||
| int? IMessageComponentModel.MaxLength { get => MaxLength.ToNullable(); set => throw new System.NotSupportedException(); } | |||||
| bool? IMessageComponentModel.Required { get => Required.ToNullable(); set => throw new System.NotSupportedException(); } | |||||
| string IMessageComponentModel.Value { get => Value.GetValueOrDefault(); set => throw new System.NotSupportedException(); } | |||||
| string IMessageComponentModel.Label { get => Label; set => throw new System.NotSupportedException(); } | |||||
| string IMessageComponentModel.Placeholder { get => Placeholder.GetValueOrDefault(); set => throw new System.NotSupportedException(); } | |||||
| #region unused | |||||
| bool? IMessageComponentModel.Disabled { get => null; set => throw new System.NotSupportedException(); } | |||||
| ButtonStyle? IMessageComponentModel.Style { get => null; set => throw new System.NotSupportedException(); } | |||||
| ulong? IMessageComponentModel.EmojiId { get => null; set => throw new System.NotSupportedException(); } | |||||
| string IMessageComponentModel.EmojiName { get => null; set => throw new System.NotSupportedException(); } | |||||
| bool? IMessageComponentModel.EmojiAnimated { get => null; set => throw new System.NotSupportedException(); } | |||||
| string IMessageComponentModel.Url { get => null; set => throw new System.NotSupportedException(); } | |||||
| IMessageComponentOptionModel[] IMessageComponentModel.Options { get => null; set => throw new System.NotSupportedException(); } | |||||
| int? IMessageComponentModel.MinValues { get => null; set => throw new System.NotSupportedException(); } | |||||
| int? IMessageComponentModel.MaxValues { get => null; set => throw new System.NotSupportedException(); } | |||||
| IMessageComponentModel[] IMessageComponentModel.Components { get => null; set => throw new System.NotSupportedException(); } | |||||
| #endregion | |||||
| } | } | ||||
| } | } | ||||
| @@ -3,10 +3,10 @@ using System; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| internal class ThreadMember | |||||
| internal class ThreadMember : IThreadMemberModel | |||||
| { | { | ||||
| [JsonProperty("id")] | [JsonProperty("id")] | ||||
| public Optional<ulong> Id { get; set; } | |||||
| public Optional<ulong> ThreadId { get; set; } | |||||
| [JsonProperty("user_id")] | [JsonProperty("user_id")] | ||||
| public Optional<ulong> UserId { get; set; } | public Optional<ulong> UserId { get; set; } | ||||
| @@ -14,7 +14,8 @@ namespace Discord.API | |||||
| [JsonProperty("join_timestamp")] | [JsonProperty("join_timestamp")] | ||||
| public DateTimeOffset JoinTimestamp { get; set; } | public DateTimeOffset JoinTimestamp { get; set; } | ||||
| [JsonProperty("flags")] | |||||
| public int Flags { get; set; } // No enum type (yet?) | |||||
| ulong? IThreadMemberModel.ThreadId { get => ThreadId.ToNullable(); set => throw new NotSupportedException(); } | |||||
| DateTimeOffset IThreadMemberModel.JoinedAt { get => JoinTimestamp; set => throw new NotSupportedException(); } | |||||
| ulong IEntityModel<ulong>.Id { get => UserId.GetValueOrDefault(0); set => throw new NotSupportedException(); } | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,8 +1,9 @@ | |||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| using System; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| internal class User | |||||
| internal class User : IUserModel | |||||
| { | { | ||||
| [JsonProperty("id")] | [JsonProperty("id")] | ||||
| public ulong Id { get; set; } | public ulong Id { get; set; } | ||||
| @@ -19,20 +20,33 @@ namespace Discord.API | |||||
| [JsonProperty("accent_color")] | [JsonProperty("accent_color")] | ||||
| public Optional<uint?> AccentColor { get; set; } | public Optional<uint?> AccentColor { get; set; } | ||||
| //CurrentUser | |||||
| [JsonProperty("verified")] | |||||
| public Optional<bool> Verified { get; set; } | |||||
| [JsonProperty("email")] | |||||
| public Optional<string> Email { get; set; } | |||||
| [JsonProperty("mfa_enabled")] | |||||
| public Optional<bool> MfaEnabled { get; set; } | |||||
| [JsonProperty("flags")] | |||||
| public Optional<UserProperties> Flags { get; set; } | |||||
| [JsonProperty("premium_type")] | |||||
| public Optional<PremiumType> PremiumType { get; set; } | |||||
| [JsonProperty("locale")] | |||||
| public Optional<string> Locale { get; set; } | |||||
| [JsonProperty("public_flags")] | |||||
| public Optional<UserProperties> PublicFlags { get; set; } | |||||
| // IUserModel | |||||
| string IUserModel.Username | |||||
| { | |||||
| get => Username.GetValueOrDefault(); | |||||
| set => throw new NotSupportedException(); | |||||
| } | |||||
| string IUserModel.Discriminator { | |||||
| get => Discriminator.GetValueOrDefault(); set => throw new NotSupportedException(); | |||||
| } | |||||
| bool? IUserModel.IsBot | |||||
| { | |||||
| get => Bot.ToNullable(); | |||||
| set => throw new NotSupportedException(); | |||||
| } | |||||
| string IUserModel.Avatar | |||||
| { | |||||
| get => Avatar.GetValueOrDefault(); set => throw new NotSupportedException(); | |||||
| } | |||||
| ulong IEntityModel<ulong>.Id | |||||
| { | |||||
| get => Id; | |||||
| set => throw new NotSupportedException(); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -46,6 +46,16 @@ namespace Discord.Rest | |||||
| .Select(x => RestGroupChannel.Create(client, x)).ToImmutableArray(); | .Select(x => RestGroupChannel.Create(client, x)).ToImmutableArray(); | ||||
| } | } | ||||
| public static async Task<RestMessage> GetMessageAsync(BaseDiscordClient client, ulong channelId, ulong messageId, RequestOptions options) | |||||
| { | |||||
| var channel = await GetChannelAsync(client, channelId, options).ConfigureAwait(false); | |||||
| if (channel is not IRestMessageChannel msgChannel) | |||||
| return null; | |||||
| return await msgChannel.GetMessageAsync(messageId, options).ConfigureAwait(false); | |||||
| } | |||||
| public static async Task<IReadOnlyCollection<RestConnection>> GetConnectionsAsync(BaseDiscordClient client, RequestOptions options) | public static async Task<IReadOnlyCollection<RestConnection>> GetConnectionsAsync(BaseDiscordClient client, RequestOptions options) | ||||
| { | { | ||||
| var models = await client.ApiClient.GetMyConnectionsAsync(options).ConfigureAwait(false); | var models = await client.ApiClient.GetMyConnectionsAsync(options).ConfigureAwait(false); | ||||
| @@ -151,6 +161,16 @@ namespace Discord.Rest | |||||
| return null; | return null; | ||||
| } | } | ||||
| public static async Task<IReadOnlyCollection<RestGuildUser>> GetGuildUsersAsync(BaseDiscordClient client, | |||||
| ulong guildId, RequestOptions options) | |||||
| { | |||||
| var guild = await GetGuildAsync(client, guildId, false, options).ConfigureAwait(false); | |||||
| if (guild == null) | |||||
| return null; | |||||
| return (await GuildHelper.GetUsersAsync(guild, client, null, null, options).FlattenAsync()).ToImmutableArray(); | |||||
| } | |||||
| public static async Task<RestWebhook> GetWebhookAsync(BaseDiscordClient client, ulong id, RequestOptions options) | public static async Task<RestWebhook> GetWebhookAsync(BaseDiscordClient client, ulong id, RequestOptions options) | ||||
| { | { | ||||
| var model = await client.ApiClient.GetWebhookAsync(id).ConfigureAwait(false); | var model = await client.ApiClient.GetWebhookAsync(id).ConfigureAwait(false); | ||||
| @@ -2063,10 +2063,10 @@ namespace Discord.API | |||||
| #endregion | #endregion | ||||
| #region Current User/DMs | #region Current User/DMs | ||||
| public async Task<User> GetMyUserAsync(RequestOptions options = null) | |||||
| public async Task<CurrentUser> GetMyUserAsync(RequestOptions options = null) | |||||
| { | { | ||||
| options = RequestOptions.CreateOrClone(options); | options = RequestOptions.CreateOrClone(options); | ||||
| return await SendAsync<User>("GET", () => "users/@me", new BucketIds(), options: options).ConfigureAwait(false); | |||||
| return await SendAsync<CurrentUser>("GET", () => "users/@me", new BucketIds(), options: options).ConfigureAwait(false); | |||||
| } | } | ||||
| public async Task<IReadOnlyCollection<Connection>> GetMyConnectionsAsync(RequestOptions options = null) | public async Task<IReadOnlyCollection<Connection>> GetMyConnectionsAsync(RequestOptions options = null) | ||||
| { | { | ||||
| @@ -158,6 +158,9 @@ namespace Discord.Rest | |||||
| public Task<IReadOnlyCollection<RestGroupChannel>> GetGroupChannelsAsync(RequestOptions options = null) | public Task<IReadOnlyCollection<RestGroupChannel>> GetGroupChannelsAsync(RequestOptions options = null) | ||||
| => ClientHelper.GetGroupChannelsAsync(this, options); | => ClientHelper.GetGroupChannelsAsync(this, options); | ||||
| public Task<RestMessage> GetMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) | |||||
| => ClientHelper.GetMessageAsync(this, channelId, messageId, options); | |||||
| public Task<IReadOnlyCollection<RestConnection>> GetConnectionsAsync(RequestOptions options = null) | public Task<IReadOnlyCollection<RestConnection>> GetConnectionsAsync(RequestOptions options = null) | ||||
| => ClientHelper.GetConnectionsAsync(this, options); | => ClientHelper.GetConnectionsAsync(this, options); | ||||
| @@ -185,6 +188,8 @@ namespace Discord.Rest | |||||
| => ClientHelper.GetUserAsync(this, id, options); | => ClientHelper.GetUserAsync(this, id, options); | ||||
| public Task<RestGuildUser> GetGuildUserAsync(ulong guildId, ulong id, RequestOptions options = null) | public Task<RestGuildUser> GetGuildUserAsync(ulong guildId, ulong id, RequestOptions options = null) | ||||
| => ClientHelper.GetGuildUserAsync(this, guildId, id, options); | => ClientHelper.GetGuildUserAsync(this, guildId, id, options); | ||||
| public Task<IReadOnlyCollection<RestGuildUser>> GetGuildUsersAsync(ulong guildId, RequestOptions options = null) | |||||
| => ClientHelper.GetGuildUsersAsync(this, guildId, options); | |||||
| public Task<IReadOnlyCollection<RestVoiceRegion>> GetVoiceRegionsAsync(RequestOptions options = null) | public Task<IReadOnlyCollection<RestVoiceRegion>> GetVoiceRegionsAsync(RequestOptions options = null) | ||||
| => ClientHelper.GetVoiceRegionsAsync(this, options); | => ClientHelper.GetVoiceRegionsAsync(this, options); | ||||
| @@ -1,5 +1,5 @@ | |||||
| using System.Diagnostics; | using System.Diagnostics; | ||||
| using Model = Discord.API.Attachment; | |||||
| using Model = Discord.IAttachmentModel; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| @@ -44,11 +44,11 @@ namespace Discord | |||||
| } | } | ||||
| internal static Attachment Create(Model model) | internal static Attachment Create(Model model) | ||||
| { | { | ||||
| return new Attachment(model.Id, model.Filename, model.Url, model.ProxyUrl, model.Size, | |||||
| model.Height.IsSpecified ? model.Height.Value : (int?)null, | |||||
| model.Width.IsSpecified ? model.Width.Value : (int?)null, | |||||
| model.Ephemeral.ToNullable(), model.Description.GetValueOrDefault(), | |||||
| model.ContentType.GetValueOrDefault()); | |||||
| return new Attachment(model.Id, model.FileName, model.Url, model.ProxyUrl, model.Size, | |||||
| model.Height, | |||||
| model.Width, | |||||
| model.Ephemeral, model.Description, | |||||
| model.ContentType); | |||||
| } | } | ||||
| /// <summary> | /// <summary> | ||||
| @@ -221,7 +221,7 @@ namespace Discord.Rest | |||||
| await client.ApiClient.RemovePinAsync(msg.Channel.Id, msg.Id, options).ConfigureAwait(false); | await client.ApiClient.RemovePinAsync(msg.Channel.Id, msg.Id, options).ConfigureAwait(false); | ||||
| } | } | ||||
| public static ImmutableArray<ITag> ParseTags(string text, IMessageChannel channel, IGuild guild, IReadOnlyCollection<IUser> userMentions) | |||||
| public static ImmutableArray<ITag> ParseTags(string text, IMessageChannel channel, IGuild guild, ulong[] userMentions) | |||||
| { | { | ||||
| var tags = ImmutableArray.CreateBuilder<ITag>(); | var tags = ImmutableArray.CreateBuilder<ITag>(); | ||||
| int index = 0; | int index = 0; | ||||
| @@ -278,11 +278,9 @@ namespace Discord.Rest | |||||
| IUser mentionedUser = null; | IUser mentionedUser = null; | ||||
| foreach (var mention in userMentions) | foreach (var mention in userMentions) | ||||
| { | { | ||||
| if (mention.Id == id) | |||||
| if (mention == id) | |||||
| { | { | ||||
| mentionedUser = channel?.GetUserAsync(id, CacheMode.CacheOnly).GetAwaiter().GetResult(); | mentionedUser = channel?.GetUserAsync(id, CacheMode.CacheOnly).GetAwaiter().GetResult(); | ||||
| if (mentionedUser == null) | |||||
| mentionedUser = mention; | |||||
| break; | break; | ||||
| } | } | ||||
| } | } | ||||
| @@ -372,11 +370,11 @@ namespace Discord.Rest | |||||
| .ToImmutableArray(); | .ToImmutableArray(); | ||||
| } | } | ||||
| public static MessageSource GetSource(Model msg) | |||||
| public static MessageSource GetSource(IMessageModel msg) | |||||
| { | { | ||||
| if (msg.Type != MessageType.Default && msg.Type != MessageType.Reply) | if (msg.Type != MessageType.Default && msg.Type != MessageType.Reply) | ||||
| return MessageSource.System; | return MessageSource.System; | ||||
| else if (msg.WebhookId.IsSpecified) | |||||
| else if (msg.IsWebhookMessage) | |||||
| return MessageSource.Webhook; | return MessageSource.Webhook; | ||||
| else if (msg.Author.GetValueOrDefault()?.Bot.GetValueOrDefault(false) == true) | else if (msg.Author.GetValueOrDefault()?.Bot.GetValueOrDefault(false) == true) | ||||
| return MessageSource.Bot; | return MessageSource.Bot; | ||||
| @@ -94,6 +94,7 @@ namespace Discord.Rest | |||||
| internal void Update(Model model) | internal void Update(Model model) | ||||
| { | { | ||||
| base.Update(model.User); | base.Update(model.User); | ||||
| if (model.JoinedAt.IsSpecified) | if (model.JoinedAt.IsSpecified) | ||||
| _joinedAtTicks = model.JoinedAt.Value.UtcTicks; | _joinedAtTicks = model.JoinedAt.Value.UtcTicks; | ||||
| if (model.Nick.IsSpecified) | if (model.Nick.IsSpecified) | ||||
| @@ -1,7 +1,8 @@ | |||||
| using System; | using System; | ||||
| using System.Diagnostics; | using System.Diagnostics; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Model = Discord.API.User; | |||||
| using UserModel = Discord.API.User; | |||||
| using Model = Discord.API.CurrentUser; | |||||
| namespace Discord.Rest | namespace Discord.Rest | ||||
| { | { | ||||
| @@ -28,29 +29,26 @@ namespace Discord.Rest | |||||
| : base(discord, id) | : base(discord, id) | ||||
| { | { | ||||
| } | } | ||||
| internal new static RestSelfUser Create(BaseDiscordClient discord, Model model) | |||||
| internal new static RestSelfUser Create(BaseDiscordClient discord, UserModel model) | |||||
| { | { | ||||
| var entity = new RestSelfUser(discord, model.Id); | var entity = new RestSelfUser(discord, model.Id); | ||||
| entity.Update(model); | entity.Update(model); | ||||
| return entity; | return entity; | ||||
| } | } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| internal override void Update(Model model) | |||||
| internal override void Update(UserModel model) | |||||
| { | { | ||||
| base.Update(model); | base.Update(model); | ||||
| if (model.Email.IsSpecified) | |||||
| Email = model.Email.Value; | |||||
| if (model.Verified.IsSpecified) | |||||
| IsVerified = model.Verified.Value; | |||||
| if (model.MfaEnabled.IsSpecified) | |||||
| IsMfaEnabled = model.MfaEnabled.Value; | |||||
| if (model.Flags.IsSpecified) | |||||
| Flags = (UserProperties)model.Flags.Value; | |||||
| if (model.PremiumType.IsSpecified) | |||||
| PremiumType = model.PremiumType.Value; | |||||
| if (model.Locale.IsSpecified) | |||||
| Locale = model.Locale.Value; | |||||
| if (model is not Model currentUserModel) | |||||
| throw new ArgumentException("Got unexpected model type when updating RestSelfUser"); | |||||
| Email = currentUserModel.Email.GetValueOrDefault(); | |||||
| IsVerified = currentUserModel.Verified.GetValueOrDefault(false); | |||||
| IsMfaEnabled = currentUserModel.MfaEnabled.GetValueOrDefault(false); | |||||
| Flags = currentUserModel.Flags.GetValueOrDefault(); | |||||
| PremiumType = currentUserModel.PremiumType.GetValueOrDefault(); | |||||
| Locale = currentUserModel.Locale.GetValueOrDefault(); | |||||
| } | } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -78,20 +78,16 @@ namespace Discord.Rest | |||||
| internal virtual void Update(Model model) | internal virtual void Update(Model model) | ||||
| { | { | ||||
| if (model.Avatar.IsSpecified) | |||||
| AvatarId = model.Avatar.Value; | |||||
| if (model.Banner.IsSpecified) | |||||
| BannerId = model.Banner.Value; | |||||
| if (model.AccentColor.IsSpecified) | |||||
| AccentColor = model.AccentColor.Value; | |||||
| if (model.Discriminator.IsSpecified) | |||||
| AvatarId = model.Avatar.GetValueOrDefault(); | |||||
| if(model.Discriminator.IsSpecified) | |||||
| DiscriminatorValue = ushort.Parse(model.Discriminator.Value, NumberStyles.None, CultureInfo.InvariantCulture); | DiscriminatorValue = ushort.Parse(model.Discriminator.Value, NumberStyles.None, CultureInfo.InvariantCulture); | ||||
| if (model.Bot.IsSpecified) | |||||
| IsBot = model.Bot.Value; | |||||
| if (model.Username.IsSpecified) | |||||
| Username = model.Username.Value; | |||||
| if (model.PublicFlags.IsSpecified) | |||||
| PublicFlags = model.PublicFlags.Value; | |||||
| IsBot = model.Bot.GetValueOrDefault(false); | |||||
| Username = model.Username.GetValueOrDefault(); | |||||
| if(model is ICurrentUserModel currentUserModel) | |||||
| { | |||||
| PublicFlags = currentUserModel.PublicFlags; | |||||
| } | |||||
| } | } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -1,3 +1,4 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
| using System.Linq; | using System.Linq; | ||||
| @@ -6,6 +7,23 @@ namespace Discord.Rest | |||||
| { | { | ||||
| internal static class EntityExtensions | internal static class EntityExtensions | ||||
| { | { | ||||
| public static IEmote ToIEmote(this IEmojiModel model) | |||||
| { | |||||
| if (model.Id.HasValue) | |||||
| return model.ToEntity(); | |||||
| return new Emoji(model.Name); | |||||
| } | |||||
| public static GuildEmote ToEntity(this IEmojiModel model) | |||||
| => new GuildEmote(model.Id.Value, | |||||
| model.Name, | |||||
| model.IsAnimated, | |||||
| model.IsManaged, | |||||
| model.IsAvailable, | |||||
| model.RequireColons, | |||||
| ImmutableArray.Create(model.Roles), | |||||
| model.CreatorId); | |||||
| public static IEmote ToIEmote(this API.Emoji model) | public static IEmote ToIEmote(this API.Emoji model) | ||||
| { | { | ||||
| if (model.Id.HasValue) | if (model.Id.HasValue) | ||||
| @@ -18,21 +36,30 @@ namespace Discord.Rest | |||||
| model.Name, | model.Name, | ||||
| model.Animated.GetValueOrDefault(), | model.Animated.GetValueOrDefault(), | ||||
| model.Managed, | model.Managed, | ||||
| model.Available.GetValueOrDefault(), | |||||
| model.RequireColons, | model.RequireColons, | ||||
| ImmutableArray.Create(model.Roles), | ImmutableArray.Create(model.Roles), | ||||
| model.User.IsSpecified ? model.User.Value.Id : (ulong?)null); | model.User.IsSpecified ? model.User.Value.Id : (ulong?)null); | ||||
| public static Embed ToEntity(this API.Embed model) | |||||
| public static Embed ToEntity(this IEmbedModel model) | |||||
| { | { | ||||
| return new Embed(model.Type, model.Title, model.Description, model.Url, model.Timestamp, | |||||
| return new Embed(model.Type, model.Title, model.Description, model.Url, | |||||
| model.Timestamp.HasValue ? new DateTimeOffset(model.Timestamp.Value, TimeSpan.Zero) : null, | |||||
| model.Color.HasValue ? new Color(model.Color.Value) : (Color?)null, | model.Color.HasValue ? new Color(model.Color.Value) : (Color?)null, | ||||
| model.Image.IsSpecified ? model.Image.Value.ToEntity() : (EmbedImage?)null, | |||||
| model.Video.IsSpecified ? model.Video.Value.ToEntity() : (EmbedVideo?)null, | |||||
| model.Author.IsSpecified ? model.Author.Value.ToEntity() : (EmbedAuthor?)null, | |||||
| model.Footer.IsSpecified ? model.Footer.Value.ToEntity() : (EmbedFooter?)null, | |||||
| model.Provider.IsSpecified ? model.Provider.Value.ToEntity() : (EmbedProvider?)null, | |||||
| model.Thumbnail.IsSpecified ? model.Thumbnail.Value.ToEntity() : (EmbedThumbnail?)null, | |||||
| model.Fields.IsSpecified ? model.Fields.Value.Select(x => x.ToEntity()).ToImmutableArray() : ImmutableArray.Create<EmbedField>()); | |||||
| model.Image != null | |||||
| ? new EmbedImage(model.Image.Url, model.Image.ProxyUrl, model.Image.Height, model.Image.Width) : (EmbedImage?)null, | |||||
| model.Video != null | |||||
| ? new EmbedVideo(model.Video.Url, model.Video.Height, model.Video.Width) : (EmbedVideo?)null, | |||||
| model.AuthorIconUrl != null || model.AuthorName != null || model.AuthorProxyIconUrl != null || model.AuthorUrl != null | |||||
| ? new EmbedAuthor(model.AuthorName, model.AuthorUrl, model.AuthorIconUrl, model.AuthorProxyIconUrl) : (EmbedAuthor?)null, | |||||
| model.FooterIconUrl != null || model.FooterProxyUrl != null || model.FooterText != null | |||||
| ? new EmbedFooter(model.FooterText, model.FooterIconUrl, model.FooterProxyUrl) : (EmbedFooter?)null, | |||||
| model.ProviderUrl != null || model.ProviderName != null | |||||
| ? new EmbedProvider(model.ProviderName, model.ProviderUrl) : (EmbedProvider?)null, | |||||
| model.Thumbnail != null | |||||
| ? new EmbedThumbnail(model.Thumbnail.Url, model.Thumbnail.ProxyUrl, model.Thumbnail.Height, model.Thumbnail.Width) : (EmbedThumbnail?)null, | |||||
| model.Fields != null | |||||
| ? model.Fields.Select(x => x.ToEntity()).ToImmutableArray() : ImmutableArray.Create<EmbedField>()); | |||||
| } | } | ||||
| public static RoleTags ToEntity(this API.RoleTags model) | public static RoleTags ToEntity(this API.RoleTags model) | ||||
| { | { | ||||
| @@ -98,15 +125,11 @@ namespace Discord.Rest | |||||
| if (mentionTypes.HasFlag(AllowedMentionTypes.Users)) | if (mentionTypes.HasFlag(AllowedMentionTypes.Users)) | ||||
| yield return "users"; | yield return "users"; | ||||
| } | } | ||||
| public static EmbedAuthor ToEntity(this API.EmbedAuthor model) | |||||
| { | |||||
| return new EmbedAuthor(model.Name, model.Url, model.IconUrl, model.ProxyIconUrl); | |||||
| } | |||||
| public static API.EmbedAuthor ToModel(this EmbedAuthor entity) | public static API.EmbedAuthor ToModel(this EmbedAuthor entity) | ||||
| { | { | ||||
| return new API.EmbedAuthor { Name = entity.Name, Url = entity.Url, IconUrl = entity.IconUrl }; | return new API.EmbedAuthor { Name = entity.Name, Url = entity.Url, IconUrl = entity.IconUrl }; | ||||
| } | } | ||||
| public static EmbedField ToEntity(this API.EmbedField model) | |||||
| public static EmbedField ToEntity(this IEmbedFieldModel model) | |||||
| { | { | ||||
| return new EmbedField(model.Name, model.Value, model.Inline); | return new EmbedField(model.Name, model.Value, model.Inline); | ||||
| } | } | ||||
| @@ -114,48 +137,22 @@ namespace Discord.Rest | |||||
| { | { | ||||
| return new API.EmbedField { Name = entity.Name, Value = entity.Value, Inline = entity.Inline }; | return new API.EmbedField { Name = entity.Name, Value = entity.Value, Inline = entity.Inline }; | ||||
| } | } | ||||
| public static EmbedFooter ToEntity(this API.EmbedFooter model) | |||||
| { | |||||
| return new EmbedFooter(model.Text, model.IconUrl, model.ProxyIconUrl); | |||||
| } | |||||
| public static API.EmbedFooter ToModel(this EmbedFooter entity) | public static API.EmbedFooter ToModel(this EmbedFooter entity) | ||||
| { | { | ||||
| return new API.EmbedFooter { Text = entity.Text, IconUrl = entity.IconUrl }; | return new API.EmbedFooter { Text = entity.Text, IconUrl = entity.IconUrl }; | ||||
| } | } | ||||
| public static EmbedImage ToEntity(this API.EmbedImage model) | |||||
| { | |||||
| return new EmbedImage(model.Url, model.ProxyUrl, | |||||
| model.Height.IsSpecified ? model.Height.Value : (int?)null, | |||||
| model.Width.IsSpecified ? model.Width.Value : (int?)null); | |||||
| } | |||||
| public static API.EmbedImage ToModel(this EmbedImage entity) | public static API.EmbedImage ToModel(this EmbedImage entity) | ||||
| { | { | ||||
| return new API.EmbedImage { Url = entity.Url }; | return new API.EmbedImage { Url = entity.Url }; | ||||
| } | } | ||||
| public static EmbedProvider ToEntity(this API.EmbedProvider model) | |||||
| { | |||||
| return new EmbedProvider(model.Name, model.Url); | |||||
| } | |||||
| public static API.EmbedProvider ToModel(this EmbedProvider entity) | public static API.EmbedProvider ToModel(this EmbedProvider entity) | ||||
| { | { | ||||
| return new API.EmbedProvider { Name = entity.Name, Url = entity.Url }; | return new API.EmbedProvider { Name = entity.Name, Url = entity.Url }; | ||||
| } | } | ||||
| public static EmbedThumbnail ToEntity(this API.EmbedThumbnail model) | |||||
| { | |||||
| return new EmbedThumbnail(model.Url, model.ProxyUrl, | |||||
| model.Height.IsSpecified ? model.Height.Value : (int?)null, | |||||
| model.Width.IsSpecified ? model.Width.Value : (int?)null); | |||||
| } | |||||
| public static API.EmbedThumbnail ToModel(this EmbedThumbnail entity) | public static API.EmbedThumbnail ToModel(this EmbedThumbnail entity) | ||||
| { | { | ||||
| return new API.EmbedThumbnail { Url = entity.Url }; | return new API.EmbedThumbnail { Url = entity.Url }; | ||||
| } | } | ||||
| public static EmbedVideo ToEntity(this API.EmbedVideo model) | |||||
| { | |||||
| return new EmbedVideo(model.Url, | |||||
| model.Height.IsSpecified ? model.Height.Value : (int?)null, | |||||
| model.Width.IsSpecified ? model.Width.Value : (int?)null); | |||||
| } | |||||
| public static API.EmbedVideo ToModel(this EmbedVideo entity) | public static API.EmbedVideo ToModel(this EmbedVideo entity) | ||||
| { | { | ||||
| return new API.EmbedVideo { Url = entity.Url }; | return new API.EmbedVideo { Url = entity.Url }; | ||||
| @@ -170,48 +167,5 @@ namespace Discord.Rest | |||||
| { | { | ||||
| return new Overwrite(model.TargetId, model.TargetType, new OverwritePermissions(model.Allow, model.Deny)); | return new Overwrite(model.TargetId, model.TargetType, new OverwritePermissions(model.Allow, model.Deny)); | ||||
| } | } | ||||
| public static API.Message ToMessage(this API.InteractionResponse model, IDiscordInteraction interaction) | |||||
| { | |||||
| if (model.Data.IsSpecified) | |||||
| { | |||||
| var data = model.Data.Value; | |||||
| var messageModel = new API.Message | |||||
| { | |||||
| IsTextToSpeech = data.TTS, | |||||
| Content = (data.Content.IsSpecified && data.Content.Value == null) ? Optional<string>.Unspecified : data.Content, | |||||
| Embeds = data.Embeds, | |||||
| AllowedMentions = data.AllowedMentions, | |||||
| Components = data.Components, | |||||
| Flags = data.Flags, | |||||
| }; | |||||
| if(interaction is IApplicationCommandInteraction command) | |||||
| { | |||||
| messageModel.Interaction = new API.MessageInteraction | |||||
| { | |||||
| Id = command.Id, | |||||
| Name = command.Data.Name, | |||||
| Type = InteractionType.ApplicationCommand, | |||||
| User = new API.User | |||||
| { | |||||
| Username = command.User.Username, | |||||
| Avatar = command.User.AvatarId, | |||||
| Bot = command.User.IsBot, | |||||
| Discriminator = command.User.Discriminator, | |||||
| PublicFlags = command.User.PublicFlags.HasValue ? command.User.PublicFlags.Value : Optional<UserProperties>.Unspecified, | |||||
| Id = command.User.Id, | |||||
| } | |||||
| }; | |||||
| } | |||||
| return messageModel; | |||||
| } | |||||
| return new API.Message | |||||
| { | |||||
| Id = interaction.Id, | |||||
| }; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -17,7 +17,7 @@ namespace Discord.API.Gateway | |||||
| [JsonProperty("v")] | [JsonProperty("v")] | ||||
| public int Version { get; set; } | public int Version { get; set; } | ||||
| [JsonProperty("user")] | [JsonProperty("user")] | ||||
| public User User { get; set; } | |||||
| public CurrentUser User { get; set; } | |||||
| [JsonProperty("session_id")] | [JsonProperty("session_id")] | ||||
| public string SessionId { get; set; } | public string SessionId { get; set; } | ||||
| [JsonProperty("read_state")] | [JsonProperty("read_state")] | ||||
| @@ -0,0 +1,105 @@ | |||||
| using System; | |||||
| using System.Collections.Concurrent; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.WebSocket | |||||
| { | |||||
| public class DefaultConcurrentCacheProvider : ICacheProvider | |||||
| { | |||||
| private readonly ConcurrentDictionary<Type, object> _storeCache = new(); | |||||
| private readonly ConcurrentDictionary<object, object> _subStoreCache = new(); | |||||
| private class DefaultEntityStore<TModel, TId> : IEntityStore<TModel, TId> | |||||
| where TModel : IEntityModel<TId> | |||||
| where TId : IEquatable<TId> | |||||
| { | |||||
| private ConcurrentDictionary<TId, TModel> _cache; | |||||
| public DefaultEntityStore(ConcurrentDictionary<TId, TModel> cache) | |||||
| { | |||||
| _cache = cache; | |||||
| } | |||||
| public TModel Get(TId id) | |||||
| { | |||||
| if (_cache.TryGetValue(id, out var model)) | |||||
| return model; | |||||
| return default; | |||||
| } | |||||
| public IEnumerable<TModel> GetAll() | |||||
| { | |||||
| return _cache.Select(x => x.Value); | |||||
| } | |||||
| public void AddOrUpdate(TModel model) | |||||
| { | |||||
| _cache.AddOrUpdate(model.Id, model, (_, __) => model); | |||||
| } | |||||
| public void AddOrUpdateBatch(IEnumerable<TModel> models) | |||||
| { | |||||
| foreach (var model in models) | |||||
| _cache.AddOrUpdate(model.Id, model, (_, __) => model); | |||||
| } | |||||
| public void Remove(TId id) | |||||
| { | |||||
| _cache.TryRemove(id, out _); | |||||
| } | |||||
| public void PurgeAll() | |||||
| { | |||||
| _cache.Clear(); | |||||
| } | |||||
| ValueTask<TModel> IEntityStore<TModel, TId>.GetAsync(TId id) => new ValueTask<TModel>(Get(id)); | |||||
| IAsyncEnumerable<TModel> IEntityStore<TModel, TId>.GetAllAsync() | |||||
| { | |||||
| var enumerator = GetAll().GetEnumerator(); | |||||
| return AsyncEnumerable.Create((cancellationToken) | |||||
| => AsyncEnumerator.Create( | |||||
| () => new ValueTask<bool>(enumerator.MoveNext()), | |||||
| () => enumerator.Current, | |||||
| () => new ValueTask()) | |||||
| ); | |||||
| } | |||||
| ValueTask IEntityStore<TModel, TId>.AddOrUpdateAsync(TModel model) | |||||
| { | |||||
| AddOrUpdate(model); | |||||
| return default; | |||||
| } | |||||
| ValueTask IEntityStore<TModel, TId>.AddOrUpdateBatchAsync(IEnumerable<TModel> models) | |||||
| { | |||||
| AddOrUpdateBatch(models); | |||||
| return default; | |||||
| } | |||||
| ValueTask IEntityStore<TModel, TId>.RemoveAsync(TId id) | |||||
| { | |||||
| Remove(id); | |||||
| return default; | |||||
| } | |||||
| ValueTask IEntityStore<TModel, TId>.PurgeAllAsync() | |||||
| { | |||||
| PurgeAll(); | |||||
| return default; | |||||
| } | |||||
| } | |||||
| public Type GetModel<TInterface>() => null; | |||||
| public virtual ValueTask<IEntityStore<TModel, TId>> GetStoreAsync<TModel, TId>() | |||||
| where TModel : IEntityModel<TId> | |||||
| where TId : IEquatable<TId> | |||||
| { | |||||
| var store = _storeCache.GetOrAdd(typeof(TModel), (_) => new DefaultEntityStore<TModel, TId>(new ConcurrentDictionary<TId, TModel>())); | |||||
| return new ValueTask<IEntityStore<TModel, TId>>((IEntityStore<TModel, TId>)store); | |||||
| } | |||||
| public virtual ValueTask<IEntityStore<TModel, TId>> GetSubStoreAsync<TModel, TId>(TId parentId) | |||||
| where TModel : IEntityModel<TId> | |||||
| where TId : IEquatable<TId> | |||||
| { | |||||
| var store = _subStoreCache.GetOrAdd(parentId, (_) => new DefaultEntityStore<TModel, TId>(new ConcurrentDictionary<TId, TModel>())); | |||||
| return new ValueTask<IEntityStore<TModel, TId>>((IEntityStore<TModel, TId>)store); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,39 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.WebSocket | |||||
| { | |||||
| public interface ICacheProvider | |||||
| { | |||||
| Type GetModel<TModelInterface>(); | |||||
| ValueTask<IEntityStore<TModel, TId>> GetStoreAsync<TModel, TId>() | |||||
| where TModel : IEntityModel<TId> | |||||
| where TId : IEquatable<TId>; | |||||
| ValueTask<IEntityStore<TModel, TId>> GetSubStoreAsync<TModel, TId>(TId parentId) | |||||
| where TModel : IEntityModel<TId> | |||||
| where TId : IEquatable<TId>; | |||||
| } | |||||
| public interface IEntityStore<TModel, TId> | |||||
| where TModel : IEntityModel<TId> | |||||
| where TId : IEquatable<TId> | |||||
| { | |||||
| ValueTask<TModel> GetAsync(TId id); | |||||
| TModel Get(TId id); | |||||
| IAsyncEnumerable<TModel> GetAllAsync(); | |||||
| IEnumerable<TModel> GetAll(); | |||||
| ValueTask AddOrUpdateAsync(TModel model); | |||||
| void AddOrUpdate(TModel model); | |||||
| ValueTask AddOrUpdateBatchAsync(IEnumerable<TModel> models); | |||||
| void AddOrUpdateBatch(IEnumerable<TModel> models); | |||||
| ValueTask RemoveAsync(TId id); | |||||
| void Remove(TId id); | |||||
| ValueTask PurgeAllAsync(); | |||||
| void PurgeAll(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,76 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.WebSocket | |||||
| { | |||||
| /// <summary> | |||||
| /// Represents a lazily-loaded cached value that can be loaded synchronously or asynchronously. | |||||
| /// </summary> | |||||
| /// <typeparam name="TEntity">The type of the entity.</typeparam> | |||||
| /// <typeparam name="TId">The primary id type of the entity.</typeparam> | |||||
| public class LazyCached<TEntity, TId> | |||||
| where TEntity : class, ICached | |||||
| where TId : IEquatable<TId> | |||||
| { | |||||
| /// <summary> | |||||
| /// Gets or loads the cached value synchronously. | |||||
| /// </summary> | |||||
| public TEntity Value | |||||
| => GetOrLoad(); | |||||
| /// <summary> | |||||
| /// Gets whether or not the <see cref="Value"/> has been loaded and is still alive. | |||||
| /// </summary> | |||||
| public bool IsValueCreated | |||||
| => _loadedValue != null && _loadedValue.IsFreed; | |||||
| private TEntity _loadedValue; | |||||
| private readonly ILookupReferenceStore<TEntity, TId> _store; | |||||
| private readonly TId _id; | |||||
| private readonly object _lock = new(); | |||||
| internal LazyCached(TEntity value) | |||||
| { | |||||
| _loadedValue = value; | |||||
| } | |||||
| internal LazyCached(TId id, ILookupReferenceStore<TEntity, TId> store) | |||||
| { | |||||
| _store = store; | |||||
| _id = id; | |||||
| } | |||||
| private TEntity GetOrLoad() | |||||
| { | |||||
| lock (_lock) | |||||
| { | |||||
| if(!IsValueCreated) | |||||
| _loadedValue = _store.Get(_id); | |||||
| return _loadedValue; | |||||
| } | |||||
| } | |||||
| /// <summary> | |||||
| /// Gets or loads the value from the cache asynchronously. | |||||
| /// </summary> | |||||
| /// <returns>The loaded or fetched entity.</returns> | |||||
| public async ValueTask<TEntity> GetAsync() | |||||
| { | |||||
| if (!IsValueCreated) | |||||
| _loadedValue = await _store.GetAsync(_id).ConfigureAwait(false); | |||||
| return _loadedValue; | |||||
| } | |||||
| } | |||||
| public class LazyCached<TEntity> : LazyCached<TEntity, ulong> | |||||
| where TEntity : class, ICached | |||||
| { | |||||
| internal LazyCached(ulong id, ILookupReferenceStore<TEntity, ulong> store) | |||||
| : base(id, store) { } | |||||
| internal LazyCached(TEntity entity) | |||||
| : base(entity) { } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,479 @@ | |||||
| using Discord.Rest; | |||||
| using System; | |||||
| using System.Collections.Concurrent; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Runtime.CompilerServices; | |||||
| using System.Text; | |||||
| using System.Threading; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.WebSocket | |||||
| { | |||||
| internal class CacheReference<TType> where TType : class | |||||
| { | |||||
| public WeakReference<TType> Reference { get; } | |||||
| public bool CanRelease | |||||
| => !Reference.TryGetTarget(out _) || _referenceCount <= 0; | |||||
| private int _referenceCount; | |||||
| public CacheReference(TType value) | |||||
| { | |||||
| Reference = new(value); | |||||
| _referenceCount = 1; | |||||
| } | |||||
| public bool TryObtainReference(out TType reference) | |||||
| { | |||||
| if (Reference.TryGetTarget(out reference)) | |||||
| { | |||||
| Interlocked.Increment(ref _referenceCount); | |||||
| return true; | |||||
| } | |||||
| return false; | |||||
| } | |||||
| public void ReleaseReference() | |||||
| { | |||||
| Interlocked.Decrement(ref _referenceCount); | |||||
| } | |||||
| } | |||||
| internal interface ILookupReferenceStore<TEntity, TId> | |||||
| { | |||||
| TEntity Get(TId id); | |||||
| ValueTask<TEntity> GetAsync(TId id); | |||||
| } | |||||
| internal class ReferenceStore<TEntity, TModel, TId, TSharedEntity> : ILookupReferenceStore<TEntity, TId> | |||||
| where TEntity : class, ICached<TModel>, TSharedEntity | |||||
| where TModel : class, IEntityModel<TId> | |||||
| where TId : IEquatable<TId> | |||||
| where TSharedEntity : class | |||||
| { | |||||
| private readonly ICacheProvider _cacheProvider; | |||||
| private readonly ConcurrentDictionary<TId, CacheReference<TEntity>> _references = new(); | |||||
| private IEntityStore<TModel, TId> _store; | |||||
| private Func<TModel, TEntity> _entityBuilder; | |||||
| private Func<TModel> _modelFactory; | |||||
| private Func<TId, RequestOptions, Task<TSharedEntity>> _restLookup; | |||||
| private readonly object _lock = new(); | |||||
| public ReferenceStore(ICacheProvider cacheProvider, | |||||
| Func<TModel, TEntity> entityBuilder, | |||||
| Func<TId, RequestOptions, Task<TSharedEntity>> restLookup, | |||||
| Func<TModel> userDefinedModelFactory) | |||||
| { | |||||
| _cacheProvider = cacheProvider; | |||||
| _entityBuilder = entityBuilder; | |||||
| _restLookup = restLookup; | |||||
| _modelFactory = userDefinedModelFactory; | |||||
| } | |||||
| private TModel GetUserDefinedModel(TModel t) | |||||
| => t.ToSpecifiedModel(_modelFactory()); | |||||
| internal bool RemoveReference(TId id) | |||||
| { | |||||
| if(_references.TryGetValue(id, out var rf)) | |||||
| { | |||||
| rf.ReleaseReference(); | |||||
| if (rf.CanRelease) | |||||
| return _references.TryRemove(id, out _); | |||||
| } | |||||
| return false; | |||||
| } | |||||
| internal void ClearDeadReferences() | |||||
| { | |||||
| lock (_lock) | |||||
| { | |||||
| var references = _references.Where(x => x.Value.CanRelease).ToArray(); | |||||
| foreach (var reference in references) | |||||
| _references.TryRemove(reference.Key, out _); | |||||
| } | |||||
| } | |||||
| public async ValueTask InitializeAsync() | |||||
| { | |||||
| _store ??= await _cacheProvider.GetStoreAsync<TModel, TId>().ConfigureAwait(false); | |||||
| } | |||||
| public async ValueTask InitializeAsync(TId parentId) | |||||
| { | |||||
| _store ??= await _cacheProvider.GetSubStoreAsync<TModel, TId>(parentId).ConfigureAwait(false); | |||||
| } | |||||
| private bool TryGetReference(TId id, out TEntity entity) | |||||
| { | |||||
| entity = null; | |||||
| return _references.TryGetValue(id, out var reference) && reference.TryObtainReference(out entity); | |||||
| } | |||||
| public TEntity Get(TId id) | |||||
| { | |||||
| if(TryGetReference(id, out var entity)) | |||||
| { | |||||
| return entity; | |||||
| } | |||||
| var model = _store.Get(id); | |||||
| if (model != null) | |||||
| { | |||||
| entity = _entityBuilder(model); | |||||
| _references.TryAdd(id, new CacheReference<TEntity>(entity)); | |||||
| return entity; | |||||
| } | |||||
| return null; | |||||
| } | |||||
| public async ValueTask<TSharedEntity> GetAsync(TId id, CacheMode mode, RequestOptions options = null) | |||||
| { | |||||
| if (TryGetReference(id, out var entity)) | |||||
| { | |||||
| return entity; | |||||
| } | |||||
| var model = await _store.GetAsync(id).ConfigureAwait(false); | |||||
| if (model != null) | |||||
| { | |||||
| entity = _entityBuilder(model); | |||||
| _references.TryAdd(id, new CacheReference<TEntity>(entity)); | |||||
| return entity; | |||||
| } | |||||
| if(mode == CacheMode.AllowDownload) | |||||
| { | |||||
| return await _restLookup(id, options).ConfigureAwait(false); | |||||
| } | |||||
| return null; | |||||
| } | |||||
| public IEnumerable<TEntity> GetAll() | |||||
| { | |||||
| var models = _store.GetAll(); | |||||
| return models.Select(x => | |||||
| { | |||||
| var entity = _entityBuilder(x); | |||||
| _references.TryAdd(x.Id, new CacheReference<TEntity>(entity)); | |||||
| return entity; | |||||
| }); | |||||
| } | |||||
| public async IAsyncEnumerable<TEntity> GetAllAsync() | |||||
| { | |||||
| await foreach(var model in _store.GetAllAsync()) | |||||
| { | |||||
| var entity = _entityBuilder(model); | |||||
| _references.TryAdd(model.Id, new CacheReference<TEntity>(entity)); | |||||
| yield return entity; | |||||
| } | |||||
| } | |||||
| public TEntity GetOrAdd(TId id, Func<TId, TModel> valueFactory) | |||||
| { | |||||
| var entity = Get(id); | |||||
| if (entity != null) | |||||
| return entity; | |||||
| var model = valueFactory(id); | |||||
| AddOrUpdate(model); | |||||
| return _entityBuilder(model); | |||||
| } | |||||
| public async ValueTask<TEntity> GetOrAddAsync(TId id, Func<TId, TModel> valueFactory) | |||||
| { | |||||
| var entity = await GetAsync(id, CacheMode.CacheOnly).ConfigureAwait(false); | |||||
| if (entity != null) | |||||
| return (TEntity)entity; | |||||
| var model = valueFactory(id); | |||||
| await AddOrUpdateAsync(model).ConfigureAwait(false); | |||||
| return _entityBuilder(model); | |||||
| } | |||||
| public void AddOrUpdate(TModel model) | |||||
| { | |||||
| var userDefinedModel = GetUserDefinedModel(model); | |||||
| _store.AddOrUpdate(userDefinedModel); | |||||
| if (TryGetReference(model.Id, out var reference)) | |||||
| reference.Update(userDefinedModel); | |||||
| } | |||||
| public ValueTask AddOrUpdateAsync(TModel model) | |||||
| { | |||||
| var userDefinedModel = GetUserDefinedModel(model); | |||||
| if (TryGetReference(userDefinedModel.Id, out var reference)) | |||||
| reference.Update(userDefinedModel); | |||||
| return _store.AddOrUpdateAsync(userDefinedModel); | |||||
| } | |||||
| public void BulkAddOrUpdate(IEnumerable<TModel> models) | |||||
| { | |||||
| models = models.Select(x => GetUserDefinedModel(x)); | |||||
| _store.AddOrUpdateBatch(models); | |||||
| foreach (var model in models) | |||||
| { | |||||
| if (_references.TryGetValue(model.Id, out var rf) && rf.Reference.TryGetTarget(out var entity)) | |||||
| entity.Update(model); | |||||
| } | |||||
| } | |||||
| public async ValueTask BulkAddOrUpdateAsync(IEnumerable<TModel> models) | |||||
| { | |||||
| models = models.Select(x => GetUserDefinedModel(x)); | |||||
| await _store.AddOrUpdateBatchAsync(models).ConfigureAwait(false); | |||||
| foreach (var model in models) | |||||
| { | |||||
| if (_references.TryGetValue(model.Id, out var rf) && rf.Reference.TryGetTarget(out var entity)) | |||||
| entity.Update(model); | |||||
| } | |||||
| } | |||||
| public void Remove(TId id) | |||||
| { | |||||
| _store.Remove(id); | |||||
| _references.TryRemove(id, out _); | |||||
| } | |||||
| public ValueTask RemoveAsync(TId id) | |||||
| { | |||||
| _references.TryRemove(id, out _); | |||||
| return _store.RemoveAsync(id); | |||||
| } | |||||
| public void Purge() | |||||
| { | |||||
| _store.PurgeAll(); | |||||
| _references.Clear(); | |||||
| } | |||||
| public ValueTask PurgeAsync() | |||||
| { | |||||
| _references.Clear(); | |||||
| return _store.PurgeAllAsync(); | |||||
| } | |||||
| public IEnumerable<TEntity> GetEnumerable(IEnumerable<TId> ids) | |||||
| { | |||||
| foreach (var id in ids) | |||||
| { | |||||
| yield return Get(id); | |||||
| } | |||||
| } | |||||
| public async IAsyncEnumerable<TEntity> GetEnumerableAsync(IEnumerable<TId> ids) | |||||
| { | |||||
| foreach (var id in ids) | |||||
| { | |||||
| yield return (TEntity)await GetAsync(id, CacheMode.CacheOnly); | |||||
| } | |||||
| } | |||||
| TEntity ILookupReferenceStore<TEntity, TId>.Get(TId id) => Get(id); | |||||
| async ValueTask<TEntity> ILookupReferenceStore<TEntity, TId>.GetAsync(TId id) => (TEntity)await GetAsync(id, CacheMode.CacheOnly).ConfigureAwait(false); | |||||
| } | |||||
| internal partial class ClientStateManager | |||||
| { | |||||
| public ReferenceStore<SocketUser, IUserModel, ulong, IUser> UserStore; | |||||
| public ReferenceStore<SocketPresence, IPresenceModel, ulong, IPresence> PresenceStore; | |||||
| private ConcurrentDictionary<ulong, ReferenceStore<SocketGuildUser, IMemberModel, ulong, IGuildUser>> _memberStores; | |||||
| private ConcurrentDictionary<ulong, ReferenceStore<SocketThreadUser, IThreadMemberModel, ulong, IThreadUser>> _threadMemberStores; | |||||
| private ConcurrentDictionary<ulong, ReferenceStore<SocketMessage, IMessageModel, ulong, IMessage>> _messageStores; | |||||
| private SemaphoreSlim _memberStoreLock; | |||||
| private SemaphoreSlim _messageStoreLock; | |||||
| private SemaphoreSlim _threadMemberLock; | |||||
| #region Models | |||||
| private readonly Dictionary<Type, Func<object>> _defaultModelFactory = new() | |||||
| { | |||||
| { typeof(IUserModel), () => new SocketUser.CacheModel() }, | |||||
| { typeof(IMemberModel), () => new SocketGuildUser.CacheModel() }, | |||||
| { typeof(ICurrentUserModel), () => new SocketSelfUser.CacheModel() }, | |||||
| { typeof(IThreadMemberModel), () => new SocketThreadUser.CacheModel() }, | |||||
| { typeof(IPresenceModel), () => new SocketPresence.CacheModel() }, | |||||
| { typeof(IActivityModel), () => new SocketPresence.ActivityCacheModel() }, | |||||
| { typeof(IMessageModel), () => new SocketMessage.CacheModel() }, | |||||
| { typeof(IMessageActivityModel), () => new SocketMessage.CacheModel.MessageActivityModel() }, | |||||
| { typeof(IMessageComponentModel), () => new SocketMessage.CacheModel.MessageComponentModel() }, | |||||
| { typeof(IMessageComponentOptionModel), () => new SocketMessage.CacheModel.MessageComponentModel.MessageComponentOptionModel() }, | |||||
| { typeof(IPartialApplicationModel), () => new SocketMessage.CacheModel.PartialApplicationModel() }, | |||||
| { typeof(IStickerItemModel), () => new SocketMessage.CacheModel.StickerItemModel() }, | |||||
| { typeof(IReactionMetadataModel), () => new SocketMessage.CacheModel.ReactionModel() }, | |||||
| { typeof(IEmbedModel), () => new SocketMessage.CacheModel.EmbedModel() }, | |||||
| { typeof(IEmbedFieldModel), () => new SocketMessage.CacheModel.EmbedModel.EmbedFieldModel() }, | |||||
| { typeof(IEmbedMediaModel), () => new SocketMessage.CacheModel.EmbedModel.EmbedMediaModel()} | |||||
| }; | |||||
| public TModel GetModel<TModel, TFallback>() | |||||
| where TFallback : class, TModel, new() | |||||
| where TModel : class | |||||
| { | |||||
| return GetModel<TModel>() ?? new TFallback(); | |||||
| } | |||||
| public TModel GetModel<TModel>() | |||||
| where TModel : class | |||||
| { | |||||
| var type = _cacheProvider.GetModel<TModel>(); | |||||
| if (type != null) | |||||
| { | |||||
| if (!type.GetInterfaces().Contains(typeof(TModel))) | |||||
| throw new InvalidOperationException($"Cannot use {type.Name} as a model for {typeof(TModel).Name}"); | |||||
| return (TModel)Activator.CreateInstance(type); | |||||
| } | |||||
| else | |||||
| return _defaultModelFactory.TryGetValue(typeof(TModel), out var m) ? (TModel)m() : null; | |||||
| } | |||||
| #endregion | |||||
| #region References & Initialization | |||||
| public void ClearDeadReferences() | |||||
| { | |||||
| UserStore.ClearDeadReferences(); | |||||
| PresenceStore.ClearDeadReferences(); | |||||
| } | |||||
| public async ValueTask InitializeAsync() | |||||
| { | |||||
| await UserStore.InitializeAsync(); | |||||
| await PresenceStore.InitializeAsync(); | |||||
| } | |||||
| private void CreateStores() | |||||
| { | |||||
| UserStore = new ReferenceStore<SocketUser, IUserModel, ulong, IUser>( | |||||
| _cacheProvider, | |||||
| m => SocketGlobalUser.Create(_client, m), | |||||
| async (id, options) => await _client.Rest.GetUserAsync(id, options).ConfigureAwait(false), | |||||
| GetModel<IUserModel>); | |||||
| PresenceStore = new ReferenceStore<SocketPresence, IPresenceModel, ulong, IPresence>( | |||||
| _cacheProvider, | |||||
| m => SocketPresence.Create(_client, m), | |||||
| (id, options) => Task.FromResult<IPresence>(null), | |||||
| GetModel<IPresenceModel>); | |||||
| _memberStores = new(); | |||||
| _threadMemberStores = new(); | |||||
| _threadMemberLock = new(1, 1); | |||||
| _memberStoreLock = new(1, 1); | |||||
| } | |||||
| #endregion | |||||
| #region Members | |||||
| public ReferenceStore<SocketGuildUser, IMemberModel, ulong, IGuildUser> GetMemberStore(ulong guildId) | |||||
| => TryGetMemberStore(guildId, out var store) ? store : null; | |||||
| public bool TryGetMemberStore(ulong guildId, out ReferenceStore<SocketGuildUser, IMemberModel, ulong, IGuildUser> store) | |||||
| => _memberStores.TryGetValue(guildId, out store); | |||||
| public async ValueTask<ReferenceStore<SocketGuildUser, IMemberModel, ulong, IGuildUser>> GetMemberStoreAsync(ulong guildId) | |||||
| { | |||||
| if (_memberStores.TryGetValue(guildId, out var store)) | |||||
| return store; | |||||
| await _memberStoreLock.WaitAsync().ConfigureAwait(false); | |||||
| try | |||||
| { | |||||
| store = new ReferenceStore<SocketGuildUser, IMemberModel, ulong, IGuildUser>( | |||||
| _cacheProvider, | |||||
| m => SocketGuildUser.Create(guildId, _client, m), | |||||
| async (id, options) => await _client.Rest.GetGuildUserAsync(guildId, id, options).ConfigureAwait(false), | |||||
| GetModel<IMemberModel>); | |||||
| await store.InitializeAsync(guildId).ConfigureAwait(false); | |||||
| _memberStores.TryAdd(guildId, store); | |||||
| return store; | |||||
| } | |||||
| finally | |||||
| { | |||||
| _memberStoreLock.Release(); | |||||
| } | |||||
| } | |||||
| #endregion | |||||
| #region Thread Members | |||||
| public async Task<ReferenceStore<SocketThreadUser, IThreadMemberModel, ulong, IThreadUser>> GetThreadMemberStoreAsync(ulong threadId, ulong guildId) | |||||
| { | |||||
| if (_threadMemberStores.TryGetValue(threadId, out var store)) | |||||
| return store; | |||||
| await _threadMemberLock.WaitAsync().ConfigureAwait(false); | |||||
| try | |||||
| { | |||||
| store = new ReferenceStore<SocketThreadUser, IThreadMemberModel, ulong, IThreadUser>( | |||||
| _cacheProvider, | |||||
| m => SocketThreadUser.Create(_client, guildId, threadId, m), | |||||
| async (id, options) => await ThreadHelper.GetUserAsync(id, _client.GetChannel(threadId) as SocketThreadChannel, _client, options).ConfigureAwait(false), | |||||
| GetModel<IThreadMemberModel>); | |||||
| await store.InitializeAsync().ConfigureAwait(false); | |||||
| _threadMemberStores.TryAdd(threadId, store); | |||||
| return store; | |||||
| } | |||||
| finally | |||||
| { | |||||
| _threadMemberLock.Release(); | |||||
| } | |||||
| } | |||||
| public ReferenceStore<SocketThreadUser, IThreadMemberModel, ulong, IThreadUser> GetThreadMemberStore(ulong threadId) | |||||
| => _threadMemberStores.TryGetValue(threadId, out var store) ? store : null; | |||||
| #endregion | |||||
| #region Messages | |||||
| public ReferenceStore<SocketMessage, IMessageModel, ulong, IMessage> GetMessageStore(ulong channelId) | |||||
| => TryGetMessageStore(channelId, out var store) ? store : null; | |||||
| public bool TryGetMessageStore(ulong channelId, out ReferenceStore<SocketMessage, IMessageModel, ulong, IMessage> store) | |||||
| => _messageStores.TryGetValue(channelId, out store); | |||||
| public async ValueTask<ReferenceStore<SocketMessage, IMessageModel, ulong, IMessage>> GetMessageStoreAsync(ulong channelId) | |||||
| { | |||||
| if (_messageStores.TryGetValue(channelId, out var store)) | |||||
| return store; | |||||
| await _messageStoreLock.WaitAsync().ConfigureAwait(false); | |||||
| try | |||||
| { | |||||
| store = new ReferenceStore<SocketMessage, IMessageModel, ulong, IMessage>( | |||||
| _cacheProvider, | |||||
| m => SocketMessage.Create(_client, m, channelId), | |||||
| async (id, options) => await _client.Rest.GetMessageAsync(channelId, id).ConfigureAwait(false), | |||||
| GetModel<IMessageModel>); | |||||
| await store.InitializeAsync(channelId).ConfigureAwait(false); | |||||
| _messageStores.TryAdd(channelId, store); | |||||
| return store; | |||||
| } | |||||
| finally | |||||
| { | |||||
| _memberStoreLock.Release(); | |||||
| } | |||||
| } | |||||
| #endregion | |||||
| } | |||||
| } | |||||
| @@ -5,7 +5,7 @@ using System.Linq; | |||||
| namespace Discord.WebSocket | namespace Discord.WebSocket | ||||
| { | { | ||||
| internal class ClientState | |||||
| internal partial class ClientStateManager | |||||
| { | { | ||||
| private const double AverageChannelsPerGuild = 10.22; //Source: Googie2149 | private const double AverageChannelsPerGuild = 10.22; //Source: Googie2149 | ||||
| private const double AverageUsersPerGuild = 47.78; //Source: Googie2149 | private const double AverageUsersPerGuild = 47.78; //Source: Googie2149 | ||||
| @@ -30,8 +30,17 @@ namespace Discord.WebSocket | |||||
| _groupChannels.Select(x => GetChannel(x) as ISocketPrivateChannel)) | _groupChannels.Select(x => GetChannel(x) as ISocketPrivateChannel)) | ||||
| .ToReadOnlyCollection(() => _dmChannels.Count + _groupChannels.Count); | .ToReadOnlyCollection(() => _dmChannels.Count + _groupChannels.Count); | ||||
| public ClientState(int guildCount, int dmChannelCount) | |||||
| internal bool AllowSyncWaits | |||||
| => _client.AllowSynchronousWaiting; | |||||
| private readonly ICacheProvider _cacheProvider; | |||||
| private readonly DiscordSocketClient _client; | |||||
| public ClientStateManager(DiscordSocketClient client, int guildCount, int dmChannelCount) | |||||
| { | { | ||||
| _client = client; | |||||
| _cacheProvider = client.CacheProvider; | |||||
| double estimatedChannelCount = guildCount * AverageChannelsPerGuild + dmChannelCount; | double estimatedChannelCount = guildCount * AverageChannelsPerGuild + dmChannelCount; | ||||
| double estimatedUsersCount = guildCount * AverageUsersPerGuild; | double estimatedUsersCount = guildCount * AverageUsersPerGuild; | ||||
| _channels = new ConcurrentDictionary<ulong, SocketChannel>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedChannelCount * CollectionMultiplier)); | _channels = new ConcurrentDictionary<ulong, SocketChannel>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedChannelCount * CollectionMultiplier)); | ||||
| @@ -40,6 +49,8 @@ namespace Discord.WebSocket | |||||
| _users = new ConcurrentDictionary<ulong, SocketGlobalUser>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier)); | _users = new ConcurrentDictionary<ulong, SocketGlobalUser>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier)); | ||||
| _groupChannels = new ConcurrentHashSet<ulong>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(10 * CollectionMultiplier)); | _groupChannels = new ConcurrentHashSet<ulong>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(10 * CollectionMultiplier)); | ||||
| _commands = new ConcurrentDictionary<ulong, SocketApplicationCommand>(); | _commands = new ConcurrentDictionary<ulong, SocketApplicationCommand>(); | ||||
| CreateStores(); | |||||
| } | } | ||||
| internal SocketChannel GetChannel(ulong id) | internal SocketChannel GetChannel(ulong id) | ||||
| @@ -121,22 +132,6 @@ namespace Discord.WebSocket | |||||
| return null; | return null; | ||||
| } | } | ||||
| internal SocketGlobalUser GetUser(ulong id) | |||||
| { | |||||
| if (_users.TryGetValue(id, out SocketGlobalUser user)) | |||||
| return user; | |||||
| return null; | |||||
| } | |||||
| internal SocketGlobalUser GetOrAddUser(ulong id, Func<ulong, SocketGlobalUser> userFactory) | |||||
| { | |||||
| return _users.GetOrAdd(id, userFactory); | |||||
| } | |||||
| internal SocketGlobalUser RemoveUser(ulong id) | |||||
| { | |||||
| if (_users.TryRemove(id, out SocketGlobalUser user)) | |||||
| return user; | |||||
| return null; | |||||
| } | |||||
| internal void PurgeUsers() | internal void PurgeUsers() | ||||
| { | { | ||||
| foreach (var guild in _guilds.Values) | foreach (var guild in _guilds.Values) | ||||
| @@ -200,7 +200,7 @@ namespace Discord.WebSocket | |||||
| return _shards[id]; | return _shards[id]; | ||||
| return null; | return null; | ||||
| } | } | ||||
| private int GetShardIdFor(ulong guildId) | |||||
| public int GetShardIdFor(ulong guildId) | |||||
| => (int)((guildId >> 22) % (uint)_totalShards); | => (int)((guildId >> 22) % (uint)_totalShards); | ||||
| public int GetShardIdFor(IGuild guild) | public int GetShardIdFor(IGuild guild) | ||||
| => GetShardIdFor(guild?.Id ?? 0); | => GetShardIdFor(guild?.Id ?? 0); | ||||
| @@ -25,6 +25,17 @@ namespace Discord.WebSocket | |||||
| /// </example> | /// </example> | ||||
| public class DiscordSocketConfig : DiscordRestConfig | public class DiscordSocketConfig : DiscordRestConfig | ||||
| { | { | ||||
| /// <summary> | |||||
| /// Gets or sets the cache provider to use. | |||||
| /// </summary> | |||||
| public ICacheProvider CacheProvider { get; set; } | |||||
| /// <summary> | |||||
| /// Gets or sets whether or not non-async cache lookups would wait for the task to complete | |||||
| /// synchronously or to throw. | |||||
| /// </summary> | |||||
| public bool AllowSynchronousWaiting { get; set; } = false; | |||||
| /// <summary> | /// <summary> | ||||
| /// Returns the encoding gateway should use. | /// Returns the encoding gateway should use. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -37,7 +37,7 @@ namespace Discord.WebSocket | |||||
| : base(discord, id, guild) | : base(discord, id, guild) | ||||
| { | { | ||||
| } | } | ||||
| internal new static SocketCategoryChannel Create(SocketGuild guild, ClientState state, Model model) | |||||
| internal new static SocketCategoryChannel Create(SocketGuild guild, ClientStateManager state, Model model) | |||||
| { | { | ||||
| var entity = new SocketCategoryChannel(guild.Discord, model.Id, guild); | var entity = new SocketCategoryChannel(guild.Discord, model.Id, guild); | ||||
| entity.Update(state, model); | entity.Update(state, model); | ||||
| @@ -29,7 +29,7 @@ namespace Discord.WebSocket | |||||
| } | } | ||||
| /// <exception cref="InvalidOperationException">Unexpected channel type is created.</exception> | /// <exception cref="InvalidOperationException">Unexpected channel type is created.</exception> | ||||
| internal static ISocketPrivateChannel CreatePrivate(DiscordSocketClient discord, ClientState state, Model model) | |||||
| internal static ISocketPrivateChannel CreatePrivate(DiscordSocketClient discord, ClientStateManager state, Model model) | |||||
| { | { | ||||
| return model.Type switch | return model.Type switch | ||||
| { | { | ||||
| @@ -38,7 +38,7 @@ namespace Discord.WebSocket | |||||
| _ => throw new InvalidOperationException($"Unexpected channel type: {model.Type}"), | _ => throw new InvalidOperationException($"Unexpected channel type: {model.Type}"), | ||||
| }; | }; | ||||
| } | } | ||||
| internal abstract void Update(ClientState state, Model model); | |||||
| internal abstract void Update(ClientStateManager state, Model model); | |||||
| #endregion | #endregion | ||||
| #region User | #region User | ||||
| @@ -35,25 +35,25 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| Recipient = recipient; | Recipient = recipient; | ||||
| } | } | ||||
| internal static SocketDMChannel Create(DiscordSocketClient discord, ClientState state, Model model) | |||||
| internal static SocketDMChannel Create(DiscordSocketClient discord, ClientStateManager state, Model model) | |||||
| { | { | ||||
| var entity = new SocketDMChannel(discord, model.Id, discord.GetOrCreateTemporaryUser(state, model.Recipients.Value[0])); | var entity = new SocketDMChannel(discord, model.Id, discord.GetOrCreateTemporaryUser(state, model.Recipients.Value[0])); | ||||
| entity.Update(state, model); | entity.Update(state, model); | ||||
| return entity; | return entity; | ||||
| } | } | ||||
| internal override void Update(ClientState state, Model model) | |||||
| internal override void Update(ClientStateManager state, Model model) | |||||
| { | { | ||||
| Recipient.Update(state, model.Recipients.Value[0]); | |||||
| Recipient.Update(model.Recipients.Value[0]); | |||||
| } | } | ||||
| internal static SocketDMChannel Create(DiscordSocketClient discord, ClientState state, ulong channelId, API.User recipient) | |||||
| internal static SocketDMChannel Create(DiscordSocketClient discord, ClientStateManager state, ulong channelId, API.User recipient) | |||||
| { | { | ||||
| var entity = new SocketDMChannel(discord, channelId, discord.GetOrCreateTemporaryUser(state, recipient)); | var entity = new SocketDMChannel(discord, channelId, discord.GetOrCreateTemporaryUser(state, recipient)); | ||||
| entity.Update(state, recipient); | entity.Update(state, recipient); | ||||
| return entity; | return entity; | ||||
| } | } | ||||
| internal void Update(ClientState state, API.User recipient) | |||||
| internal void Update(ClientStateManager state, API.User recipient) | |||||
| { | { | ||||
| Recipient.Update(state, recipient); | |||||
| Recipient.Update(recipient); | |||||
| } | } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -55,13 +55,13 @@ namespace Discord.WebSocket | |||||
| _voiceStates = new ConcurrentDictionary<ulong, SocketVoiceState>(ConcurrentHashSet.DefaultConcurrencyLevel, 5); | _voiceStates = new ConcurrentDictionary<ulong, SocketVoiceState>(ConcurrentHashSet.DefaultConcurrencyLevel, 5); | ||||
| _users = new ConcurrentDictionary<ulong, SocketGroupUser>(ConcurrentHashSet.DefaultConcurrencyLevel, 5); | _users = new ConcurrentDictionary<ulong, SocketGroupUser>(ConcurrentHashSet.DefaultConcurrencyLevel, 5); | ||||
| } | } | ||||
| internal static SocketGroupChannel Create(DiscordSocketClient discord, ClientState state, Model model) | |||||
| internal static SocketGroupChannel Create(DiscordSocketClient discord, ClientStateManager state, Model model) | |||||
| { | { | ||||
| var entity = new SocketGroupChannel(discord, model.Id); | var entity = new SocketGroupChannel(discord, model.Id); | ||||
| entity.Update(state, model); | entity.Update(state, model); | ||||
| return entity; | return entity; | ||||
| } | } | ||||
| internal override void Update(ClientState state, Model model) | |||||
| internal override void Update(ClientStateManager state, Model model) | |||||
| { | { | ||||
| if (model.Name.IsSpecified) | if (model.Name.IsSpecified) | ||||
| Name = model.Name.Value; | Name = model.Name.Value; | ||||
| @@ -73,11 +73,11 @@ namespace Discord.WebSocket | |||||
| RTCRegion = model.RTCRegion.GetValueOrDefault(null); | RTCRegion = model.RTCRegion.GetValueOrDefault(null); | ||||
| } | } | ||||
| private void UpdateUsers(ClientState state, UserModel[] models) | |||||
| private void UpdateUsers(ClientStateManager state, UserModel[] models) | |||||
| { | { | ||||
| var users = new ConcurrentDictionary<ulong, SocketGroupUser>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(models.Length * 1.05)); | var users = new ConcurrentDictionary<ulong, SocketGroupUser>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(models.Length * 1.05)); | ||||
| for (int i = 0; i < models.Length; i++) | for (int i = 0; i < models.Length; i++) | ||||
| users[models[i].Id] = SocketGroupUser.Create(this, state, models[i]); | |||||
| users[models[i].Id] = SocketGroupUser.Create(this, models[i]); | |||||
| _users = users; | _users = users; | ||||
| } | } | ||||
| @@ -265,8 +265,7 @@ namespace Discord.WebSocket | |||||
| return user; | return user; | ||||
| else | else | ||||
| { | { | ||||
| var privateUser = SocketGroupUser.Create(this, Discord.State, model); | |||||
| privateUser.GlobalUser.AddRef(); | |||||
| var privateUser = SocketGroupUser.Create(this, model); | |||||
| _users[privateUser.Id] = privateUser; | _users[privateUser.Id] = privateUser; | ||||
| return privateUser; | return privateUser; | ||||
| } | } | ||||
| @@ -275,7 +274,6 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| if (_users.TryRemove(id, out SocketGroupUser user)) | if (_users.TryRemove(id, out SocketGroupUser user)) | ||||
| { | { | ||||
| user.GlobalUser.RemoveRef(Discord); | |||||
| return user; | return user; | ||||
| } | } | ||||
| return null; | return null; | ||||
| @@ -283,7 +281,7 @@ namespace Discord.WebSocket | |||||
| #endregion | #endregion | ||||
| #region Voice States | #region Voice States | ||||
| internal SocketVoiceState AddOrUpdateVoiceState(ClientState state, VoiceStateModel model) | |||||
| internal SocketVoiceState AddOrUpdateVoiceState(ClientStateManager state, VoiceStateModel model) | |||||
| { | { | ||||
| var voiceChannel = state.GetChannel(model.ChannelId.Value) as SocketVoiceChannel; | var voiceChannel = state.GetChannel(model.ChannelId.Value) as SocketVoiceChannel; | ||||
| var voiceState = SocketVoiceState.Create(voiceChannel, model); | var voiceState = SocketVoiceState.Create(voiceChannel, model); | ||||
| @@ -49,7 +49,7 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| Guild = guild; | Guild = guild; | ||||
| } | } | ||||
| internal static SocketGuildChannel Create(SocketGuild guild, ClientState state, Model model) | |||||
| internal static SocketGuildChannel Create(SocketGuild guild, ClientStateManager state, Model model) | |||||
| { | { | ||||
| return model.Type switch | return model.Type switch | ||||
| { | { | ||||
| @@ -63,7 +63,7 @@ namespace Discord.WebSocket | |||||
| }; | }; | ||||
| } | } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| internal override void Update(ClientState state, Model model) | |||||
| internal override void Update(ClientStateManager state, Model model) | |||||
| { | { | ||||
| Name = model.Name.Value; | Name = model.Name.Value; | ||||
| Position = model.Position.GetValueOrDefault(0); | Position = model.Position.GetValueOrDefault(0); | ||||
| @@ -21,7 +21,7 @@ namespace Discord.WebSocket | |||||
| :base(discord, id, guild) | :base(discord, id, guild) | ||||
| { | { | ||||
| } | } | ||||
| internal new static SocketNewsChannel Create(SocketGuild guild, ClientState state, Model model) | |||||
| internal new static SocketNewsChannel Create(SocketGuild guild, ClientStateManager state, Model model) | |||||
| { | { | ||||
| var entity = new SocketNewsChannel(guild.Discord, model.Id, guild); | var entity = new SocketNewsChannel(guild.Discord, model.Id, guild); | ||||
| entity.Update(state, model); | entity.Update(state, model); | ||||
| @@ -43,7 +43,7 @@ namespace Discord.WebSocket | |||||
| internal SocketStageChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) | internal SocketStageChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) | ||||
| : base(discord, id, guild) { } | : base(discord, id, guild) { } | ||||
| internal new static SocketStageChannel Create(SocketGuild guild, ClientState state, Model model) | |||||
| internal new static SocketStageChannel Create(SocketGuild guild, ClientStateManager state, Model model) | |||||
| { | { | ||||
| var entity = new SocketStageChannel(guild.Discord, model.Id, guild); | var entity = new SocketStageChannel(guild.Discord, model.Id, guild); | ||||
| entity.Update(state, model); | entity.Update(state, model); | ||||
| @@ -63,13 +63,13 @@ namespace Discord.WebSocket | |||||
| if (Discord.MessageCacheSize > 0) | if (Discord.MessageCacheSize > 0) | ||||
| _messages = new MessageCache(Discord); | _messages = new MessageCache(Discord); | ||||
| } | } | ||||
| internal new static SocketTextChannel Create(SocketGuild guild, ClientState state, Model model) | |||||
| internal new static SocketTextChannel Create(SocketGuild guild, ClientStateManager state, Model model) | |||||
| { | { | ||||
| var entity = new SocketTextChannel(guild.Discord, model.Id, guild); | var entity = new SocketTextChannel(guild.Discord, model.Id, guild); | ||||
| entity.Update(state, model); | entity.Update(state, model); | ||||
| return entity; | return entity; | ||||
| } | } | ||||
| internal override void Update(ClientState state, Model model) | |||||
| internal override void Update(ClientStateManager state, Model model) | |||||
| { | { | ||||
| base.Update(state, model); | base.Update(state, model); | ||||
| CategoryId = model.CategoryId; | CategoryId = model.CategoryId; | ||||
| @@ -117,7 +117,7 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| var model = await ThreadHelper.CreateThreadAsync(Discord, this, name, type, autoArchiveDuration, message, invitable, slowmode, options); | var model = await ThreadHelper.CreateThreadAsync(Discord, this, name, type, autoArchiveDuration, message, invitable, slowmode, options); | ||||
| var thread = (SocketThreadChannel)Guild.AddOrUpdateChannel(Discord.State, model); | |||||
| var thread = (SocketThreadChannel)Guild.AddOrUpdateChannel(Discord.StateManager, model); | |||||
| if(Discord.AlwaysDownloadUsers && Discord.HasGatewayIntent(GatewayIntents.GuildMembers)) | if(Discord.AlwaysDownloadUsers && Discord.HasGatewayIntent(GatewayIntents.GuildMembers)) | ||||
| await thread.DownloadUsersAsync(); | await thread.DownloadUsersAsync(); | ||||
| @@ -118,7 +118,7 @@ namespace Discord.WebSocket | |||||
| CreatedAt = createdAt ?? new DateTimeOffset(2022, 1, 9, 0, 0, 0, TimeSpan.Zero); | CreatedAt = createdAt ?? new DateTimeOffset(2022, 1, 9, 0, 0, 0, TimeSpan.Zero); | ||||
| } | } | ||||
| internal new static SocketThreadChannel Create(SocketGuild guild, ClientState state, Model model) | |||||
| internal new static SocketThreadChannel Create(SocketGuild guild, ClientStateManager state, Model model) | |||||
| { | { | ||||
| var parent = guild.GetChannel(model.CategoryId.Value); | var parent = guild.GetChannel(model.CategoryId.Value); | ||||
| var entity = new SocketThreadChannel(guild.Discord, guild, model.Id, parent, model.ThreadMetadata.GetValueOrDefault()?.CreatedAt.GetValueOrDefault(null)); | var entity = new SocketThreadChannel(guild.Discord, guild, model.Id, parent, model.ThreadMetadata.GetValueOrDefault()?.CreatedAt.GetValueOrDefault(null)); | ||||
| @@ -126,7 +126,7 @@ namespace Discord.WebSocket | |||||
| return entity; | return entity; | ||||
| } | } | ||||
| internal override void Update(ClientState state, Model model) | |||||
| internal override void Update(ClientStateManager state, Model model) | |||||
| { | { | ||||
| base.Update(state, model); | base.Update(state, model); | ||||
| @@ -171,7 +171,6 @@ namespace Discord.WebSocket | |||||
| else | else | ||||
| { | { | ||||
| member = SocketThreadUser.Create(Guild, this, model, guildMember); | member = SocketThreadUser.Create(Guild, this, model, guildMember); | ||||
| member.GlobalUser.AddRef(); | |||||
| _members[member.Id] = member; | _members[member.Id] = member; | ||||
| } | } | ||||
| return member; | return member; | ||||
| @@ -55,14 +55,14 @@ namespace Discord.WebSocket | |||||
| : base(discord, id, guild) | : base(discord, id, guild) | ||||
| { | { | ||||
| } | } | ||||
| internal new static SocketVoiceChannel Create(SocketGuild guild, ClientState state, Model model) | |||||
| internal new static SocketVoiceChannel Create(SocketGuild guild, ClientStateManager state, Model model) | |||||
| { | { | ||||
| var entity = new SocketVoiceChannel(guild.Discord, model.Id, guild); | var entity = new SocketVoiceChannel(guild.Discord, model.Id, guild); | ||||
| entity.Update(state, model); | entity.Update(state, model); | ||||
| return entity; | return entity; | ||||
| } | } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| internal override void Update(ClientState state, Model model) | |||||
| internal override void Update(ClientStateManager state, Model model) | |||||
| { | { | ||||
| base.Update(state, model); | base.Update(state, model); | ||||
| CategoryId = model.CategoryId; | CategoryId = model.CategoryId; | ||||
| @@ -14,11 +14,11 @@ using ChannelModel = Discord.API.Channel; | |||||
| using EmojiUpdateModel = Discord.API.Gateway.GuildEmojiUpdateEvent; | using EmojiUpdateModel = Discord.API.Gateway.GuildEmojiUpdateEvent; | ||||
| using ExtendedModel = Discord.API.Gateway.ExtendedGuild; | using ExtendedModel = Discord.API.Gateway.ExtendedGuild; | ||||
| using GuildSyncModel = Discord.API.Gateway.GuildSyncEvent; | using GuildSyncModel = Discord.API.Gateway.GuildSyncEvent; | ||||
| using MemberModel = Discord.API.GuildMember; | |||||
| using MemberModel = Discord.IMemberModel; | |||||
| using Model = Discord.API.Guild; | using Model = Discord.API.Guild; | ||||
| using PresenceModel = Discord.API.Presence; | using PresenceModel = Discord.API.Presence; | ||||
| using RoleModel = Discord.API.Role; | using RoleModel = Discord.API.Role; | ||||
| using UserModel = Discord.API.User; | |||||
| using UserModel = Discord.IUserModel; | |||||
| using VoiceStateModel = Discord.API.VoiceState; | using VoiceStateModel = Discord.API.VoiceState; | ||||
| using StickerModel = Discord.API.Sticker; | using StickerModel = Discord.API.Sticker; | ||||
| using EventModel = Discord.API.GuildScheduledEvent; | using EventModel = Discord.API.GuildScheduledEvent; | ||||
| @@ -38,7 +38,7 @@ namespace Discord.WebSocket | |||||
| private TaskCompletionSource<bool> _syncPromise, _downloaderPromise; | private TaskCompletionSource<bool> _syncPromise, _downloaderPromise; | ||||
| private TaskCompletionSource<AudioClient> _audioConnectPromise; | private TaskCompletionSource<AudioClient> _audioConnectPromise; | ||||
| private ConcurrentDictionary<ulong, SocketGuildChannel> _channels; | private ConcurrentDictionary<ulong, SocketGuildChannel> _channels; | ||||
| private ConcurrentDictionary<ulong, SocketGuildUser> _members; | |||||
| //private ConcurrentDictionary<ulong, SocketGuildUser> _members; | |||||
| private ConcurrentDictionary<ulong, SocketRole> _roles; | private ConcurrentDictionary<ulong, SocketRole> _roles; | ||||
| private ConcurrentDictionary<ulong, SocketVoiceState> _voiceStates; | private ConcurrentDictionary<ulong, SocketVoiceState> _voiceStates; | ||||
| private ConcurrentDictionary<ulong, SocketCustomSticker> _stickers; | private ConcurrentDictionary<ulong, SocketCustomSticker> _stickers; | ||||
| @@ -305,7 +305,7 @@ namespace Discord.WebSocket | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the current logged-in user. | /// Gets the current logged-in user. | ||||
| /// </summary> | /// </summary> | ||||
| public SocketGuildUser CurrentUser => _members.TryGetValue(Discord.CurrentUser.Id, out SocketGuildUser member) ? member : null; | |||||
| public SocketGuildUser CurrentUser => Discord.StateManager.TryGetMemberStore(Id, out var store) ? store.Get(Discord.CurrentUser.Id) : null; | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the built-in role containing all users in this guild. | /// Gets the built-in role containing all users in this guild. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -324,7 +324,7 @@ namespace Discord.WebSocket | |||||
| get | get | ||||
| { | { | ||||
| var channels = _channels; | var channels = _channels; | ||||
| var state = Discord.State; | |||||
| var state = Discord.StateManager; | |||||
| return channels.Select(x => x.Value).Where(x => x != null).ToReadOnlyCollection(channels); | return channels.Select(x => x.Value).Where(x => x != null).ToReadOnlyCollection(channels); | ||||
| } | } | ||||
| } | } | ||||
| @@ -356,7 +356,7 @@ namespace Discord.WebSocket | |||||
| /// <returns> | /// <returns> | ||||
| /// A collection of guild users found within this guild. | /// A collection of guild users found within this guild. | ||||
| /// </returns> | /// </returns> | ||||
| public IReadOnlyCollection<SocketGuildUser> Users => _members.ToReadOnlyCollection(); | |||||
| public IReadOnlyCollection<SocketGuildUser> Users => Discord.StateManager.TryGetMemberStore(Id, out var store) ? store.GetAll().ToImmutableArray() : ImmutableArray<SocketGuildUser>.Empty; | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets a collection of all roles in this guild. | /// Gets a collection of all roles in this guild. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -382,13 +382,13 @@ namespace Discord.WebSocket | |||||
| _audioLock = new SemaphoreSlim(1, 1); | _audioLock = new SemaphoreSlim(1, 1); | ||||
| _emotes = ImmutableArray.Create<GuildEmote>(); | _emotes = ImmutableArray.Create<GuildEmote>(); | ||||
| } | } | ||||
| internal static SocketGuild Create(DiscordSocketClient discord, ClientState state, ExtendedModel model) | |||||
| internal static SocketGuild Create(DiscordSocketClient discord, ClientStateManager state, ExtendedModel model) | |||||
| { | { | ||||
| var entity = new SocketGuild(discord, model.Id); | var entity = new SocketGuild(discord, model.Id); | ||||
| entity.Update(state, model); | entity.Update(state, model); | ||||
| return entity; | return entity; | ||||
| } | } | ||||
| internal void Update(ClientState state, ExtendedModel model) | |||||
| internal void Update(ClientStateManager state, ExtendedModel model) | |||||
| { | { | ||||
| IsAvailable = !(model.Unavailable ?? false); | IsAvailable = !(model.Unavailable ?? false); | ||||
| if (!IsAvailable) | if (!IsAvailable) | ||||
| @@ -397,8 +397,6 @@ namespace Discord.WebSocket | |||||
| _events = new ConcurrentDictionary<ulong, SocketGuildEvent>(); | _events = new ConcurrentDictionary<ulong, SocketGuildEvent>(); | ||||
| if (_channels == null) | if (_channels == null) | ||||
| _channels = new ConcurrentDictionary<ulong, SocketGuildChannel>(); | _channels = new ConcurrentDictionary<ulong, SocketGuildChannel>(); | ||||
| if (_members == null) | |||||
| _members = new ConcurrentDictionary<ulong, SocketGuildUser>(); | |||||
| if (_roles == null) | if (_roles == null) | ||||
| _roles = new ConcurrentDictionary<ulong, SocketRole>(); | _roles = new ConcurrentDictionary<ulong, SocketRole>(); | ||||
| /*if (Emojis == null) | /*if (Emojis == null) | ||||
| @@ -431,25 +429,6 @@ namespace Discord.WebSocket | |||||
| _channels = channels; | _channels = channels; | ||||
| var members = new ConcurrentDictionary<ulong, SocketGuildUser>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Members.Length * 1.05)); | |||||
| { | |||||
| for (int i = 0; i < model.Members.Length; i++) | |||||
| { | |||||
| var member = SocketGuildUser.Create(this, state, model.Members[i]); | |||||
| if (members.TryAdd(member.Id, member)) | |||||
| member.GlobalUser.AddRef(); | |||||
| } | |||||
| DownloadedMemberCount = members.Count; | |||||
| for (int i = 0; i < model.Presences.Length; i++) | |||||
| { | |||||
| if (members.TryGetValue(model.Presences[i].User.Id, out SocketGuildUser member)) | |||||
| member.Update(state, model.Presences[i], true); | |||||
| } | |||||
| } | |||||
| _members = members; | |||||
| MemberCount = model.MemberCount; | |||||
| var voiceStates = new ConcurrentDictionary<ulong, SocketVoiceState>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.VoiceStates.Length * 1.05)); | var voiceStates = new ConcurrentDictionary<ulong, SocketVoiceState>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.VoiceStates.Length * 1.05)); | ||||
| { | { | ||||
| for (int i = 0; i < model.VoiceStates.Length; i++) | for (int i = 0; i < model.VoiceStates.Length; i++) | ||||
| @@ -473,6 +452,10 @@ namespace Discord.WebSocket | |||||
| } | } | ||||
| _events = events; | _events = events; | ||||
| DownloadedMemberCount = model.Members.Length; | |||||
| MemberCount = model.MemberCount; | |||||
| _syncPromise = new TaskCompletionSource<bool>(); | _syncPromise = new TaskCompletionSource<bool>(); | ||||
| _downloaderPromise = new TaskCompletionSource<bool>(); | _downloaderPromise = new TaskCompletionSource<bool>(); | ||||
| @@ -480,7 +463,7 @@ namespace Discord.WebSocket | |||||
| /*if (!model.Large) | /*if (!model.Large) | ||||
| _ = _downloaderPromise.TrySetResultAsync(true);*/ | _ = _downloaderPromise.TrySetResultAsync(true);*/ | ||||
| } | } | ||||
| internal void Update(ClientState state, Model model) | |||||
| internal void Update(ClientStateManager state, Model model) | |||||
| { | { | ||||
| AFKChannelId = model.AFKChannelId; | AFKChannelId = model.AFKChannelId; | ||||
| if (model.WidgetChannelId.IsSpecified) | if (model.WidgetChannelId.IsSpecified) | ||||
| @@ -561,31 +544,18 @@ namespace Discord.WebSocket | |||||
| else | else | ||||
| _stickers = new ConcurrentDictionary<ulong, SocketCustomSticker>(ConcurrentHashSet.DefaultConcurrencyLevel, 7); | _stickers = new ConcurrentDictionary<ulong, SocketCustomSticker>(ConcurrentHashSet.DefaultConcurrencyLevel, 7); | ||||
| } | } | ||||
| /*internal void Update(ClientState state, GuildSyncModel model) //TODO remove? userbot related | |||||
| internal async ValueTask UpdateCacheAsync(ExtendedModel model) | |||||
| { | { | ||||
| var members = new ConcurrentDictionary<ulong, SocketGuildUser>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Members.Length * 1.05)); | |||||
| { | |||||
| for (int i = 0; i < model.Members.Length; i++) | |||||
| { | |||||
| var member = SocketGuildUser.Create(this, state, model.Members[i]); | |||||
| members.TryAdd(member.Id, member); | |||||
| } | |||||
| DownloadedMemberCount = members.Count; | |||||
| await Discord.StateManager.PresenceStore.BulkAddOrUpdateAsync(model.Presences); | |||||
| for (int i = 0; i < model.Presences.Length; i++) | |||||
| { | |||||
| if (members.TryGetValue(model.Presences[i].User.Id, out SocketGuildUser member)) | |||||
| member.Update(state, model.Presences[i], true); | |||||
| } | |||||
| } | |||||
| _members = members; | |||||
| await Discord.StateManager.UserStore.BulkAddOrUpdateAsync(model.Members.Select(x => x.User)); | |||||
| var _ = _syncPromise.TrySetResultAsync(true); | |||||
| //if (!model.Large) | |||||
| // _ = _downloaderPromise.TrySetResultAsync(true); | |||||
| }*/ | |||||
| if(Discord.StateManager.TryGetMemberStore(Id, out var store)) | |||||
| store.BulkAddOrUpdate(model.Members); | |||||
| } | |||||
| internal void Update(ClientState state, EmojiUpdateModel model) | |||||
| internal void Update(ClientStateManager state, EmojiUpdateModel model) | |||||
| { | { | ||||
| var emotes = ImmutableArray.CreateBuilder<GuildEmote>(model.Emojis.Length); | var emotes = ImmutableArray.CreateBuilder<GuildEmote>(model.Emojis.Length); | ||||
| for (int i = 0; i < model.Emojis.Length; i++) | for (int i = 0; i < model.Emojis.Length; i++) | ||||
| @@ -682,7 +652,7 @@ namespace Discord.WebSocket | |||||
| /// </returns> | /// </returns> | ||||
| public SocketGuildChannel GetChannel(ulong id) | public SocketGuildChannel GetChannel(ulong id) | ||||
| { | { | ||||
| var channel = Discord.State.GetChannel(id) as SocketGuildChannel; | |||||
| var channel = Discord.StateManager.GetChannel(id) as SocketGuildChannel; | |||||
| if (channel?.Guild.Id == Id) | if (channel?.Guild.Id == Id) | ||||
| return channel; | return channel; | ||||
| return null; | return null; | ||||
| @@ -799,7 +769,7 @@ namespace Discord.WebSocket | |||||
| public Task<RestCategoryChannel> CreateCategoryChannelAsync(string name, Action<GuildChannelProperties> func = null, RequestOptions options = null) | public Task<RestCategoryChannel> CreateCategoryChannelAsync(string name, Action<GuildChannelProperties> func = null, RequestOptions options = null) | ||||
| => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options, func); | => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options, func); | ||||
| internal SocketGuildChannel AddChannel(ClientState state, ChannelModel model) | |||||
| internal SocketGuildChannel AddChannel(ClientStateManager state, ChannelModel model) | |||||
| { | { | ||||
| var channel = SocketGuildChannel.Create(this, state, model); | var channel = SocketGuildChannel.Create(this, state, model); | ||||
| _channels.TryAdd(model.Id, channel); | _channels.TryAdd(model.Id, channel); | ||||
| @@ -807,26 +777,26 @@ namespace Discord.WebSocket | |||||
| return channel; | return channel; | ||||
| } | } | ||||
| internal SocketGuildChannel AddOrUpdateChannel(ClientState state, ChannelModel model) | |||||
| internal SocketGuildChannel AddOrUpdateChannel(ClientStateManager state, ChannelModel model) | |||||
| { | { | ||||
| if (_channels.TryGetValue(model.Id, out SocketGuildChannel channel)) | if (_channels.TryGetValue(model.Id, out SocketGuildChannel channel)) | ||||
| channel.Update(Discord.State, model); | |||||
| channel.Update(Discord.StateManager, model); | |||||
| else | else | ||||
| { | { | ||||
| channel = SocketGuildChannel.Create(this, Discord.State, model); | |||||
| channel = SocketGuildChannel.Create(this, Discord.StateManager, model); | |||||
| _channels[channel.Id] = channel; | _channels[channel.Id] = channel; | ||||
| state.AddChannel(channel); | state.AddChannel(channel); | ||||
| } | } | ||||
| return channel; | return channel; | ||||
| } | } | ||||
| internal SocketGuildChannel RemoveChannel(ClientState state, ulong id) | |||||
| internal SocketGuildChannel RemoveChannel(ClientStateManager state, ulong id) | |||||
| { | { | ||||
| if (_channels.TryRemove(id, out var _)) | if (_channels.TryRemove(id, out var _)) | ||||
| return state.RemoveChannel(id) as SocketGuildChannel; | return state.RemoveChannel(id) as SocketGuildChannel; | ||||
| return null; | return null; | ||||
| } | } | ||||
| internal void PurgeChannelCache(ClientState state) | |||||
| internal void PurgeChannelCache(ClientStateManager state) | |||||
| { | { | ||||
| foreach (var channelId in _channels) | foreach (var channelId in _channels) | ||||
| state.RemoveChannel(channelId.Key); | state.RemoveChannel(channelId.Key); | ||||
| @@ -880,7 +850,7 @@ namespace Discord.WebSocket | |||||
| foreach (var command in commands) | foreach (var command in commands) | ||||
| { | { | ||||
| Discord.State.AddCommand(command); | |||||
| Discord.StateManager.AddCommand(command); | |||||
| } | } | ||||
| return commands.ToImmutableArray(); | return commands.ToImmutableArray(); | ||||
| @@ -898,7 +868,7 @@ namespace Discord.WebSocket | |||||
| /// </returns> | /// </returns> | ||||
| public async ValueTask<SocketApplicationCommand> GetApplicationCommandAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) | public async ValueTask<SocketApplicationCommand> GetApplicationCommandAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) | ||||
| { | { | ||||
| var command = Discord.State.GetCommand(id); | |||||
| var command = Discord.StateManager.GetCommand(id); | |||||
| if (command != null) | if (command != null) | ||||
| return command; | return command; | ||||
| @@ -913,7 +883,7 @@ namespace Discord.WebSocket | |||||
| command = SocketApplicationCommand.Create(Discord, model, Id); | command = SocketApplicationCommand.Create(Discord, model, Id); | ||||
| Discord.State.AddCommand(command); | |||||
| Discord.StateManager.AddCommand(command); | |||||
| return command; | return command; | ||||
| } | } | ||||
| @@ -930,7 +900,7 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| var model = await InteractionHelper.CreateGuildCommandAsync(Discord, Id, properties, options); | var model = await InteractionHelper.CreateGuildCommandAsync(Discord, Id, properties, options); | ||||
| var entity = Discord.State.GetOrAddCommand(model.Id, (id) => SocketApplicationCommand.Create(Discord, model)); | |||||
| var entity = Discord.StateManager.GetOrAddCommand(model.Id, (id) => SocketApplicationCommand.Create(Discord, model)); | |||||
| entity.Update(model); | entity.Update(model); | ||||
| @@ -952,11 +922,11 @@ namespace Discord.WebSocket | |||||
| var entities = models.Select(x => SocketApplicationCommand.Create(Discord, x)); | var entities = models.Select(x => SocketApplicationCommand.Create(Discord, x)); | ||||
| Discord.State.PurgeCommands(x => !x.IsGlobalCommand && x.Guild.Id == Id); | |||||
| Discord.StateManager.PurgeCommands(x => !x.IsGlobalCommand && x.Guild.Id == Id); | |||||
| foreach(var entity in entities) | foreach(var entity in entities) | ||||
| { | { | ||||
| Discord.State.AddCommand(entity); | |||||
| Discord.StateManager.AddCommand(entity); | |||||
| } | } | ||||
| return entities.ToImmutableArray(); | return entities.ToImmutableArray(); | ||||
| @@ -1020,7 +990,7 @@ namespace Discord.WebSocket | |||||
| => GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, isMentionable, options); | => GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, isMentionable, options); | ||||
| internal SocketRole AddRole(RoleModel model) | internal SocketRole AddRole(RoleModel model) | ||||
| { | { | ||||
| var role = SocketRole.Create(this, Discord.State, model); | |||||
| var role = SocketRole.Create(this, Discord.StateManager, model); | |||||
| _roles[model.Id] = role; | _roles[model.Id] = role; | ||||
| return role; | return role; | ||||
| } | } | ||||
| @@ -1034,7 +1004,7 @@ namespace Discord.WebSocket | |||||
| internal SocketRole AddOrUpdateRole(RoleModel model) | internal SocketRole AddOrUpdateRole(RoleModel model) | ||||
| { | { | ||||
| if (_roles.TryGetValue(model.Id, out SocketRole role)) | if (_roles.TryGetValue(model.Id, out SocketRole role)) | ||||
| _roles[model.Id].Update(Discord.State, model); | |||||
| _roles[model.Id].Update(Discord.StateManager, model); | |||||
| else | else | ||||
| role = AddRole(model); | role = AddRole(model); | ||||
| @@ -1089,60 +1059,43 @@ namespace Discord.WebSocket | |||||
| /// A guild user associated with the specified <paramref name="id"/>; <see langword="null"/> if none is found. | /// A guild user associated with the specified <paramref name="id"/>; <see langword="null"/> if none is found. | ||||
| /// </returns> | /// </returns> | ||||
| public SocketGuildUser GetUser(ulong id) | public SocketGuildUser GetUser(ulong id) | ||||
| { | |||||
| if (_members.TryGetValue(id, out SocketGuildUser member)) | |||||
| return member; | |||||
| return null; | |||||
| } | |||||
| => Discord.StateManager.TryGetMemberStore(Id, out var store) ? store.Get(id) : null; | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null, IEnumerable<ulong> includeRoleIds = null) | public Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null, IEnumerable<ulong> includeRoleIds = null) | ||||
| => GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options, includeRoleIds); | => GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options, includeRoleIds); | ||||
| internal SocketGuildUser AddOrUpdateUser(UserModel model) | internal SocketGuildUser AddOrUpdateUser(UserModel model) | ||||
| { | { | ||||
| if (_members.TryGetValue(model.Id, out SocketGuildUser member)) | |||||
| member.GlobalUser?.Update(Discord.State, model); | |||||
| SocketGuildUser member; | |||||
| if ((member = GetUser(model.Id)) != null) | |||||
| member.Update(model); | |||||
| else | else | ||||
| { | { | ||||
| member = SocketGuildUser.Create(this, Discord.State, model); | |||||
| member.GlobalUser.AddRef(); | |||||
| _members[member.Id] = member; | |||||
| member = SocketGuildUser.Create(Id, Discord, model); | |||||
| DownloadedMemberCount++; | DownloadedMemberCount++; | ||||
| } | } | ||||
| return member; | return member; | ||||
| } | } | ||||
| internal SocketGuildUser AddOrUpdateUser(MemberModel model) | internal SocketGuildUser AddOrUpdateUser(MemberModel model) | ||||
| { | { | ||||
| if (_members.TryGetValue(model.User.Id, out SocketGuildUser member)) | |||||
| member.Update(Discord.State, model); | |||||
| else | |||||
| { | |||||
| member = SocketGuildUser.Create(this, Discord.State, model); | |||||
| member.GlobalUser.AddRef(); | |||||
| _members[member.Id] = member; | |||||
| DownloadedMemberCount++; | |||||
| } | |||||
| return member; | |||||
| } | |||||
| internal SocketGuildUser AddOrUpdateUser(PresenceModel model) | |||||
| { | |||||
| if (_members.TryGetValue(model.User.Id, out SocketGuildUser member)) | |||||
| member.Update(Discord.State, model, false); | |||||
| SocketGuildUser member; | |||||
| if ((member = GetUser(model.Id)) != null) | |||||
| member.Update(model); | |||||
| else | else | ||||
| { | { | ||||
| member = SocketGuildUser.Create(this, Discord.State, model); | |||||
| member.GlobalUser.AddRef(); | |||||
| _members[member.Id] = member; | |||||
| member = SocketGuildUser.Create(Id, Discord, model); | |||||
| DownloadedMemberCount++; | DownloadedMemberCount++; | ||||
| } | } | ||||
| return member; | return member; | ||||
| } | } | ||||
| internal SocketGuildUser RemoveUser(ulong id) | internal SocketGuildUser RemoveUser(ulong id) | ||||
| { | { | ||||
| if (_members.TryRemove(id, out SocketGuildUser member)) | |||||
| SocketGuildUser member; | |||||
| if ((member = GetUser(id)) != null) | |||||
| { | { | ||||
| DownloadedMemberCount--; | DownloadedMemberCount--; | ||||
| member.GlobalUser.RemoveRef(Discord); | |||||
| if (Discord.StateManager.TryGetMemberStore(Id, out var store)) | |||||
| store.Remove(id); | |||||
| return member; | return member; | ||||
| } | } | ||||
| return null; | return null; | ||||
| @@ -1158,18 +1111,17 @@ namespace Discord.WebSocket | |||||
| /// <param name="predicate">The predicate used to select which users to clear.</param> | /// <param name="predicate">The predicate used to select which users to clear.</param> | ||||
| public void PurgeUserCache(Func<SocketGuildUser, bool> predicate) | public void PurgeUserCache(Func<SocketGuildUser, bool> predicate) | ||||
| { | { | ||||
| var membersToPurge = Users.Where(x => predicate.Invoke(x) && x?.Id != Discord.CurrentUser.Id); | |||||
| var membersToKeep = Users.Where(x => !predicate.Invoke(x) || x?.Id == Discord.CurrentUser.Id); | |||||
| var users = Users.ToArray(); | |||||
| foreach (var member in membersToPurge) | |||||
| if(_members.TryRemove(member.Id, out _)) | |||||
| member.GlobalUser.RemoveRef(Discord); | |||||
| var membersToPurge = users.Where(x => predicate.Invoke(x) && x?.Id != Discord.CurrentUser.Id); | |||||
| var membersToKeep = users.Where(x => !predicate.Invoke(x) || x?.Id == Discord.CurrentUser.Id); | |||||
| foreach (var member in membersToKeep) | |||||
| _members.TryAdd(member.Id, member); | |||||
| if(Discord.StateManager.TryGetMemberStore(Id, out var store)) | |||||
| foreach (var member in membersToPurge) | |||||
| store.Remove(member.Id); | |||||
| _downloaderPromise = new TaskCompletionSource<bool>(); | _downloaderPromise = new TaskCompletionSource<bool>(); | ||||
| DownloadedMemberCount = _members.Count; | |||||
| DownloadedMemberCount = membersToKeep.Count(); | |||||
| } | } | ||||
| /// <summary> | /// <summary> | ||||
| @@ -1291,7 +1243,6 @@ namespace Discord.WebSocket | |||||
| /// in order to use this property. | /// in order to use this property. | ||||
| /// </remarks> | /// </remarks> | ||||
| /// </param> | /// </param> | ||||
| /// <param name="speakers">A collection of speakers for the event.</param> | |||||
| /// <param name="location">The location of the event; links are supported</param> | /// <param name="location">The location of the event; links are supported</param> | ||||
| /// <param name="coverImage">The optional banner image for the event.</param> | /// <param name="coverImage">The optional banner image for the event.</param> | ||||
| /// <param name="options">The options to be used when sending the request.</param> | /// <param name="options">The options to be used when sending the request.</param> | ||||
| @@ -1537,7 +1488,7 @@ namespace Discord.WebSocket | |||||
| #endregion | #endregion | ||||
| #region Voice States | #region Voice States | ||||
| internal async Task<SocketVoiceState> AddOrUpdateVoiceStateAsync(ClientState state, VoiceStateModel model) | |||||
| internal async Task<SocketVoiceState> AddOrUpdateVoiceStateAsync(ClientStateManager state, VoiceStateModel model) | |||||
| { | { | ||||
| var voiceChannel = state.GetChannel(model.ChannelId.Value) as SocketVoiceChannel; | var voiceChannel = state.GetChannel(model.ChannelId.Value) as SocketVoiceChannel; | ||||
| var before = GetVoiceState(model.UserId) ?? SocketVoiceState.Default; | var before = GetVoiceState(model.UserId) ?? SocketVoiceState.Default; | ||||
| @@ -89,13 +89,13 @@ namespace Discord.WebSocket | |||||
| if(guildUser != null) | if(guildUser != null) | ||||
| { | { | ||||
| if(model.Creator.IsSpecified) | if(model.Creator.IsSpecified) | ||||
| guildUser.Update(Discord.State, model.Creator.Value); | |||||
| guildUser.Update(model.Creator.Value); | |||||
| Creator = guildUser; | Creator = guildUser; | ||||
| } | } | ||||
| else if (guildUser == null && model.Creator.IsSpecified) | else if (guildUser == null && model.Creator.IsSpecified) | ||||
| { | { | ||||
| guildUser = SocketGuildUser.Create(Guild, Discord.State, model.Creator.Value); | |||||
| guildUser = SocketGuildUser.Create(Guild.Id, Discord, model.Creator.Value); | |||||
| Creator = guildUser; | Creator = guildUser; | ||||
| } | } | ||||
| } | } | ||||
| @@ -56,18 +56,18 @@ namespace Discord.WebSocket | |||||
| if (Channel is SocketGuildChannel channel) | if (Channel is SocketGuildChannel channel) | ||||
| { | { | ||||
| if (model.Message.Value.WebhookId.IsSpecified) | if (model.Message.Value.WebhookId.IsSpecified) | ||||
| author = SocketWebhookUser.Create(channel.Guild, Discord.State, model.Message.Value.Author.Value, model.Message.Value.WebhookId.Value); | |||||
| author = SocketWebhookUser.Create(channel.Guild, model.Message.Value.Author.Value, model.Message.Value.WebhookId.Value); | |||||
| else if (model.Message.Value.Author.IsSpecified) | else if (model.Message.Value.Author.IsSpecified) | ||||
| author = channel.Guild.GetUser(model.Message.Value.Author.Value.Id); | author = channel.Guild.GetUser(model.Message.Value.Author.Value.Id); | ||||
| } | } | ||||
| else if (model.Message.Value.Author.IsSpecified) | else if (model.Message.Value.Author.IsSpecified) | ||||
| author = (Channel as SocketChannel).GetUser(model.Message.Value.Author.Value.Id); | author = (Channel as SocketChannel).GetUser(model.Message.Value.Author.Value.Id); | ||||
| Message = SocketUserMessage.Create(Discord, Discord.State, author, Channel, model.Message.Value); | |||||
| Message = SocketUserMessage.Create(Discord, Discord.StateManager, author, Channel, model.Message.Value); | |||||
| } | } | ||||
| else | else | ||||
| { | { | ||||
| Message.Update(Discord.State, model.Message.Value); | |||||
| Message.Update(Discord.StateManager, model.Message.Value); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -29,7 +29,7 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| foreach (var user in resolved.Users.Value) | foreach (var user in resolved.Users.Value) | ||||
| { | { | ||||
| var socketUser = discord.GetOrCreateUser(discord.State, user.Value); | |||||
| var socketUser = discord.GetOrCreateUser(discord.StateManager, user.Value); | |||||
| Users.Add(ulong.Parse(user.Key), socketUser); | Users.Add(ulong.Parse(user.Key), socketUser); | ||||
| } | } | ||||
| @@ -50,11 +50,11 @@ namespace Discord.WebSocket | |||||
| : discord.Rest.ApiClient.GetChannelAsync(channel.Value.Id).ConfigureAwait(false).GetAwaiter().GetResult(); | : discord.Rest.ApiClient.GetChannelAsync(channel.Value.Id).ConfigureAwait(false).GetAwaiter().GetResult(); | ||||
| socketChannel = guild != null | socketChannel = guild != null | ||||
| ? SocketGuildChannel.Create(guild, discord.State, channelModel) | |||||
| : (SocketChannel)SocketChannel.CreatePrivate(discord, discord.State, channelModel); | |||||
| ? SocketGuildChannel.Create(guild, discord.StateManager, channelModel) | |||||
| : (SocketChannel)SocketChannel.CreatePrivate(discord, discord.StateManager, channelModel); | |||||
| } | } | ||||
| discord.State.AddChannel(socketChannel); | |||||
| discord.StateManager.AddChannel(socketChannel); | |||||
| Channels.Add(ulong.Parse(channel.Key), socketChannel); | Channels.Add(ulong.Parse(channel.Key), socketChannel); | ||||
| } | } | ||||
| } | } | ||||
| @@ -88,7 +88,7 @@ namespace Discord.WebSocket | |||||
| if (guild != null) | if (guild != null) | ||||
| { | { | ||||
| if (msg.Value.WebhookId.IsSpecified) | if (msg.Value.WebhookId.IsSpecified) | ||||
| author = SocketWebhookUser.Create(guild, discord.State, msg.Value.Author.Value, msg.Value.WebhookId.Value); | |||||
| author = SocketWebhookUser.Create(guild, msg.Value.Author.Value, msg.Value.WebhookId.Value); | |||||
| else | else | ||||
| author = guild.GetUser(msg.Value.Author.Value.Id); | author = guild.GetUser(msg.Value.Author.Value.Id); | ||||
| } | } | ||||
| @@ -99,11 +99,11 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| if (!msg.Value.GuildId.IsSpecified) // assume it is a DM | if (!msg.Value.GuildId.IsSpecified) // assume it is a DM | ||||
| { | { | ||||
| channel = discord.CreateDMChannel(msg.Value.ChannelId, msg.Value.Author.Value, discord.State); | |||||
| channel = discord.CreateDMChannel(msg.Value.ChannelId, msg.Value.Author.Value, discord.StateManager); | |||||
| } | } | ||||
| } | } | ||||
| var message = SocketMessage.Create(discord, discord.State, author, channel, msg.Value); | |||||
| var message = SocketMessage.Create(discord, discord.StateManager, author, channel, msg.Value); | |||||
| Messages.Add(message.Id, message); | Messages.Add(message.Id, message); | ||||
| } | } | ||||
| } | } | ||||
| @@ -5,19 +5,25 @@ using System.Collections.Generic; | |||||
| using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
| using System.Linq; | using System.Linq; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Model = Discord.API.Message; | |||||
| using Model = Discord.IMessageModel; | |||||
| namespace Discord.WebSocket | namespace Discord.WebSocket | ||||
| { | { | ||||
| /// <summary> | /// <summary> | ||||
| /// Represents a WebSocket-based message. | /// Represents a WebSocket-based message. | ||||
| /// </summary> | /// </summary> | ||||
| public abstract class SocketMessage : SocketEntity<ulong>, IMessage | |||||
| public abstract class SocketMessage : SocketEntity<ulong>, IMessage, ICached<Model> | |||||
| { | { | ||||
| #region SocketMessage | #region SocketMessage | ||||
| internal bool IsFreed { get; set; } | |||||
| private long _timestampTicks; | private long _timestampTicks; | ||||
| private readonly List<SocketReaction> _reactions = new List<SocketReaction>(); | private readonly List<SocketReaction> _reactions = new List<SocketReaction>(); | ||||
| private ImmutableArray<SocketUser> _userMentions = ImmutableArray.Create<SocketUser>(); | |||||
| private ulong[] _userMentionIds; | |||||
| private ulong _channelId; | |||||
| private ulong _guildId; | |||||
| private ulong _authorId; | |||||
| private bool _isWebhook; | |||||
| //private ImmutableArray<SocketUser> _userMentions = ImmutableArray.Create<SocketUser>(); | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the author of this message. | /// Gets the author of this message. | ||||
| @@ -54,6 +60,7 @@ namespace Discord.WebSocket | |||||
| public virtual DateTimeOffset? EditedTimestamp => null; | public virtual DateTimeOffset? EditedTimestamp => null; | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public virtual bool MentionedEveryone => false; | public virtual bool MentionedEveryone => false; | ||||
| public virtual ulong? ApplicationId { get; private set; } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public MessageActivity Activity { get; private set; } | public MessageActivity Activity { get; private set; } | ||||
| @@ -115,10 +122,13 @@ namespace Discord.WebSocket | |||||
| /// <summary> | /// <summary> | ||||
| /// Returns the users mentioned in this message. | /// Returns the users mentioned in this message. | ||||
| /// </summary> | /// </summary> | ||||
| /// <remarks> | |||||
| /// The returned enumerable will preform cache lookups per enumeration. | |||||
| /// </remarks> | |||||
| /// <returns> | /// <returns> | ||||
| /// Collection of WebSocket-based users. | /// Collection of WebSocket-based users. | ||||
| /// </returns> | /// </returns> | ||||
| public IReadOnlyCollection<SocketUser> MentionedUsers => _userMentions; | |||||
| public IEnumerable<SocketUser> MentionedUsers => Discord.StateManager.UserStore.GetEnumerable(_userMentionIds); // TODO: async counterpart? | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); | public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); | ||||
| @@ -129,7 +139,13 @@ namespace Discord.WebSocket | |||||
| Author = author; | Author = author; | ||||
| Source = source; | Source = source; | ||||
| } | } | ||||
| internal static SocketMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) | |||||
| //internal static SocketMessage Create(DiscordSocketClient discord, Model model, ulong channelId) | |||||
| //{ | |||||
| //} | |||||
| internal static SocketMessage Create(DiscordSocketClient discord, ClientStateManager state, SocketUser author, ISocketMessageChannel channel, Model model) | |||||
| { | { | ||||
| if (model.Type == MessageType.Default || | if (model.Type == MessageType.Default || | ||||
| model.Type == MessageType.Reply || | model.Type == MessageType.Reply || | ||||
| @@ -140,55 +156,52 @@ namespace Discord.WebSocket | |||||
| else | else | ||||
| return SocketSystemMessage.Create(discord, state, author, channel, model); | return SocketSystemMessage.Create(discord, state, author, channel, model); | ||||
| } | } | ||||
| internal virtual void Update(ClientState state, Model model) | |||||
| internal virtual void Update(Model model) | |||||
| { | { | ||||
| Type = model.Type; | Type = model.Type; | ||||
| if (model.Timestamp.IsSpecified) | |||||
| _timestampTicks = model.Timestamp.Value.UtcTicks; | |||||
| if (model.Content.IsSpecified) | |||||
| { | |||||
| Content = model.Content.Value; | |||||
| } | |||||
| _timestampTicks = model.Timestamp; | |||||
| ApplicationId = model.ApplicationId; | |||||
| Content = model.Content; | |||||
| _userMentionIds = model.UserMentionIds; | |||||
| if (model.Application.IsSpecified) | |||||
| if (model.Application != null) | |||||
| { | { | ||||
| // create a new Application from the API model | // create a new Application from the API model | ||||
| Application = new MessageApplication() | Application = new MessageApplication() | ||||
| { | { | ||||
| Id = model.Application.Value.Id, | |||||
| CoverImage = model.Application.Value.CoverImage, | |||||
| Description = model.Application.Value.Description, | |||||
| Icon = model.Application.Value.Icon, | |||||
| Name = model.Application.Value.Name | |||||
| Id = model.Application.Id, | |||||
| CoverImage = model.Application.CoverImage, | |||||
| Description = model.Application.Description, | |||||
| Icon = model.Application.Icon, | |||||
| Name = model.Application.Name | |||||
| }; | }; | ||||
| } | } | ||||
| if (model.Activity.IsSpecified) | |||||
| if (model.Activity != null) | |||||
| { | { | ||||
| // create a new Activity from the API model | // create a new Activity from the API model | ||||
| Activity = new MessageActivity() | Activity = new MessageActivity() | ||||
| { | { | ||||
| Type = model.Activity.Value.Type.Value, | |||||
| PartyId = model.Activity.Value.PartyId.GetValueOrDefault() | |||||
| Type = model.Activity.Type.Value, | |||||
| PartyId = model.Activity.PartyId | |||||
| }; | }; | ||||
| } | } | ||||
| if (model.Reference.IsSpecified) | |||||
| if (model.ReferenceMessageId.HasValue) | |||||
| { | { | ||||
| // Creates a new Reference from the API model | // Creates a new Reference from the API model | ||||
| Reference = new MessageReference | Reference = new MessageReference | ||||
| { | { | ||||
| GuildId = model.Reference.Value.GuildId, | |||||
| InternalChannelId = model.Reference.Value.ChannelId, | |||||
| MessageId = model.Reference.Value.MessageId | |||||
| GuildId = model.ReferenceMessageGuildId.ToOptional(), | |||||
| InternalChannelId = model.ReferenceMessageChannelId.ToOptional(), | |||||
| MessageId = model.ReferenceMessageId.ToOptional() | |||||
| }; | }; | ||||
| } | } | ||||
| if (model.Components.IsSpecified) | |||||
| if (model.Components != null && model.Components.Length > 0) | |||||
| { | { | ||||
| Components = model.Components.Value.Select(x => new ActionRowComponent(x.Components.Select<IMessageComponent, IMessageComponent>(y => | |||||
| Components = model.Components.Select(x => new ActionRowComponent(x.Components.Select<IMessageComponentModel, IMessageComponent>(y => | |||||
| { | { | ||||
| switch (y.Type) | switch (y.Type) | ||||
| { | { | ||||
| @@ -236,38 +249,16 @@ namespace Discord.WebSocket | |||||
| else | else | ||||
| Components = new List<ActionRowComponent>(); | Components = new List<ActionRowComponent>(); | ||||
| if (model.UserMentions.IsSpecified) | |||||
| if (model.InteractionId.HasValue) | |||||
| { | { | ||||
| var value = model.UserMentions.Value; | |||||
| if (value.Length > 0) | |||||
| { | |||||
| var newMentions = ImmutableArray.CreateBuilder<SocketUser>(value.Length); | |||||
| for (int i = 0; i < value.Length; i++) | |||||
| { | |||||
| var val = value[i]; | |||||
| if (val != null) | |||||
| { | |||||
| var user = Channel.GetUserAsync(val.Id, CacheMode.CacheOnly).GetAwaiter().GetResult() as SocketUser; | |||||
| if (user != null) | |||||
| newMentions.Add(user); | |||||
| else | |||||
| newMentions.Add(SocketUnknownUser.Create(Discord, state, val)); | |||||
| } | |||||
| } | |||||
| _userMentions = newMentions.ToImmutable(); | |||||
| } | |||||
| Interaction = new MessageInteraction<SocketUser>(model.InteractionId.Value, | |||||
| model.InteractionType.Value, | |||||
| model.InteractionName, | |||||
| model.InteractionUserId.Value, | |||||
| Discord.StateManager.UserStore.Get); | |||||
| } | } | ||||
| if (model.Interaction.IsSpecified) | |||||
| { | |||||
| Interaction = new MessageInteraction<SocketUser>(model.Interaction.Value.Id, | |||||
| model.Interaction.Value.Type, | |||||
| model.Interaction.Value.Name, | |||||
| SocketGlobalUser.Create(Discord, state, model.Interaction.Value.User)); | |||||
| } | |||||
| if (model.Flags.IsSpecified) | |||||
| Flags = model.Flags.Value; | |||||
| Flags = model.Flags; | |||||
| } | } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -309,7 +300,6 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| IReadOnlyCollection<IStickerItem> IMessage.Stickers => Stickers; | IReadOnlyCollection<IStickerItem> IMessage.Stickers => Stickers; | ||||
| internal void AddReaction(SocketReaction reaction) | internal void AddReaction(SocketReaction reaction) | ||||
| { | { | ||||
| _reactions.Add(reaction); | _reactions.Add(reaction); | ||||
| @@ -347,5 +337,314 @@ namespace Discord.WebSocket | |||||
| public IAsyncEnumerable<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IEmote emote, int limit, RequestOptions options = null) | public IAsyncEnumerable<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IEmote emote, int limit, RequestOptions options = null) | ||||
| => MessageHelper.GetReactionUsersAsync(this, emote, limit, Discord, options); | => MessageHelper.GetReactionUsersAsync(this, emote, limit, Discord, options); | ||||
| #endregion | #endregion | ||||
| #region Cache | |||||
| internal class CacheModel : Model | |||||
| { | |||||
| public MessageType Type { get; set; } | |||||
| public ulong ChannelId { get; set; } | |||||
| public ulong? GuildId { get; set; } | |||||
| public ulong AuthorId { get; set; } | |||||
| public bool IsWebhookMessage { get; set; } | |||||
| public string Content { get; set; } | |||||
| public long Timestamp { get; set; } | |||||
| public long? EditedTimestamp { get; set; } | |||||
| public bool IsTextToSpeech { get; set; } | |||||
| public bool MentionEveryone { get; set; } | |||||
| public ulong[] UserMentionIds { get; set; } | |||||
| public AttachmentModel[] Attachments { get; set; } | |||||
| public EmbedModel[] Embeds { get; set; } | |||||
| public ReactionModel[] Reactions { get; set; } // TODO: seperate store? | |||||
| public bool Pinned { get; set; } | |||||
| public MessageActivityModel Activity { get; set; } | |||||
| public PartialApplicationModel Application { get; set; } | |||||
| public ulong? ApplicationId { get; set; } | |||||
| public ulong? ReferenceMessageId { get; set; } | |||||
| public ulong? ReferenceMessageChannelId { get; set; } | |||||
| public ulong? ReferenceMessageGuildId { get; set; } | |||||
| public MessageFlags Flags { get; set; } | |||||
| public ulong? InteractionId { get; set; } | |||||
| public string InteractionName { get; set; } | |||||
| public InteractionType? InteractionType { get; set; } | |||||
| public ulong? InteractionUserId { get; set; } | |||||
| public MessageComponentModel[] Components { get; set; } | |||||
| public StickerItemModel[] Stickers { get; set; } | |||||
| public ulong Id { get; set; } | |||||
| internal class AttachmentModel : IAttachmentModel | |||||
| { | |||||
| public string FileName { get; set; } | |||||
| public string Description { get; set; } | |||||
| public string ContentType { get; set; } | |||||
| public int Size { get; set; } | |||||
| public string Url { get; set; } | |||||
| public string ProxyUrl { get; set; } | |||||
| public int? Height { get; set; } | |||||
| public int? Width { get; set; } | |||||
| public bool Ephemeral { get; set; } | |||||
| public ulong Id { get; set; } | |||||
| } | |||||
| internal class EmbedModel : IEmbedModel | |||||
| { | |||||
| public string Title { get; set; } | |||||
| public EmbedType Type { get; set; } | |||||
| public string Description { get; set; } | |||||
| public string Url { get; set; } | |||||
| public long? Timestamp { get; set; } | |||||
| public uint? Color { get; set; } | |||||
| public string FooterText { get; set; } | |||||
| public string FooterIconUrl { get; set; } | |||||
| public string FooterProxyUrl { get; set; } | |||||
| public string ProviderName { get; set; } | |||||
| public string ProviderUrl { get; set; } | |||||
| public string AuthorName { get; set; } | |||||
| public string AuthorUrl { get; set; } | |||||
| public string AuthorIconUrl { get; set; } | |||||
| public string AuthorProxyIconUrl { get; set; } | |||||
| public EmbedMediaModel Image { get; set; } | |||||
| public EmbedMediaModel Thumbnail { get; set; } | |||||
| public EmbedMediaModel Video { get; set; } | |||||
| public EmbedFieldModel[] Fields { get; set; } | |||||
| IEmbedMediaModel IEmbedModel.Image { get => Image; set => Image = value.InterfaceCopy<EmbedMediaModel>(); } | |||||
| IEmbedMediaModel IEmbedModel.Thumbnail { get => Thumbnail; set => Thumbnail = value.InterfaceCopy<EmbedMediaModel>(); } | |||||
| IEmbedMediaModel IEmbedModel.Video { get => Video; set => Video = value.InterfaceCopy<EmbedMediaModel>(); } | |||||
| IEmbedFieldModel[] IEmbedModel.Fields { get => Fields; set => value?.Select(x => x.InterfaceCopy<EmbedFieldModel>()); } | |||||
| internal class EmbedMediaModel : IEmbedMediaModel | |||||
| { | |||||
| public string Url { get; set; } | |||||
| public string ProxyUrl { get; set; } | |||||
| public int? Height { get; set; } | |||||
| public int? Width { get; set; } | |||||
| } | |||||
| internal class EmbedFieldModel : IEmbedFieldModel | |||||
| { | |||||
| public string Name { get; set; } | |||||
| public string Value { get; set; } | |||||
| public bool Inline { get; set; } | |||||
| } | |||||
| } | |||||
| internal class ReactionModel : IReactionMetadataModel | |||||
| { | |||||
| public IEmojiModel Emoji { get; set; } | |||||
| public ulong[] Users { get; set; } | |||||
| } | |||||
| internal class MessageActivityModel : IMessageActivityModel | |||||
| { | |||||
| public MessageActivityType? Type { get; set; } | |||||
| public string PartyId { get; set; } | |||||
| } | |||||
| internal class PartialApplicationModel : IPartialApplicationModel | |||||
| { | |||||
| public string Name { get; set; } | |||||
| public string Icon { get; set; } | |||||
| public string Description { get; set; } | |||||
| public string CoverImage { get; set; } | |||||
| public ulong Id { get; set; } | |||||
| } | |||||
| internal class MessageComponentModel : IMessageComponentModel | |||||
| { | |||||
| public ComponentType Type { get; set; } | |||||
| public string CustomId { get; set; } | |||||
| public bool? Disabled { get; set; } | |||||
| public ButtonStyle? Style { get; set; } | |||||
| public string Label { get; set; } | |||||
| public ulong? EmojiId { get; set; } | |||||
| public string EmojiName { get; set; } | |||||
| public bool? EmojiAnimated { get; set; } | |||||
| public string Url { get; set; } | |||||
| public MessageComponentOptionModel[] Options { get; set; } | |||||
| public string Placeholder { get; set; } | |||||
| public int? MinValues { get; set; } | |||||
| public int? MaxValues { get; set; } | |||||
| public MessageComponentModel[] Components { get; set; } | |||||
| public int? MinLength { get; set; } | |||||
| public int? MaxLength { get; set; } | |||||
| public bool? Required { get; set; } | |||||
| public string Value { get; set; } | |||||
| internal class MessageComponentOptionModel : IMessageComponentOptionModel | |||||
| { | |||||
| public string Label { get; set; } | |||||
| public string Value { get; set; } | |||||
| public string Description { get; set; } | |||||
| public ulong? EmojiId { get; set; } | |||||
| public string EmojiName { get; set; } | |||||
| public bool? EmojiAnimated { get; set; } | |||||
| public bool? Default { get; set; } | |||||
| } | |||||
| IMessageComponentOptionModel[] IMessageComponentModel.Options { get => Options; set => Options = value.Select(x => x.InterfaceCopy(new MessageComponentOptionModel())).ToArray(); } | |||||
| IMessageComponentModel[] IMessageComponentModel.Components { get => Components; set => Components = value.Select(x => x.InterfaceCopy(new MessageComponentModel())).ToArray(); } | |||||
| } | |||||
| internal class StickerItemModel : IStickerItemModel | |||||
| { | |||||
| public ulong Id { get; set; } | |||||
| public string Name { get; set; } | |||||
| public StickerFormatType Format { get; set; } | |||||
| } | |||||
| IAttachmentModel[] Model.Attachments { get => Attachments; set => Attachments = value.Select(x => x.InterfaceCopy<AttachmentModel>()).ToArray(); } | |||||
| IEmbedModel[] Model.Embeds { get => Embeds; set => Embeds = value.Select(x => x.InterfaceCopy<EmbedModel>()).ToArray(); } | |||||
| IReactionMetadataModel[] Model.Reactions { get => Reactions; set => Reactions = value.Select(x => x.InterfaceCopy<ReactionModel>()).ToArray(); } | |||||
| IMessageActivityModel Model.Activity { get => Activity; set => Activity = value.InterfaceCopy<MessageActivityModel>(); } | |||||
| IPartialApplicationModel Model.Application { get => Application; set => Application = value.InterfaceCopy<PartialApplicationModel>(); } | |||||
| IMessageComponentModel[] Model.Components { get => Components; set => Components = value.Select(x => x.InterfaceCopy<MessageComponentModel>()).ToArray(); } | |||||
| IStickerItemModel[] Model.Stickers { get => Stickers; set => Stickers = value.Select(x => x.InterfaceCopy<StickerItemModel>()).ToArray(); } | |||||
| } | |||||
| internal virtual Model ToModel() | |||||
| { | |||||
| var model = Discord.StateManager.GetModel<Model>(); | |||||
| model.Content = Content; | |||||
| model.Type = Type; | |||||
| model.ChannelId = _channelId; | |||||
| model.GuildId = _guildId; | |||||
| model.AuthorId = _authorId; | |||||
| model.IsWebhookMessage = _isWebhook; | |||||
| model.Timestamp = _timestampTicks; | |||||
| model.IsTextToSpeech = IsTTS; | |||||
| model.MentionEveryone = MentionedEveryone; | |||||
| model.UserMentionIds = _userMentionIds; | |||||
| model.ApplicationId = ApplicationId; | |||||
| model.Flags = Flags ?? MessageFlags.None; | |||||
| model.Id = Id; | |||||
| if(Interaction != null) | |||||
| { | |||||
| model.InteractionName = Interaction.Name; | |||||
| model.InteractionId = Interaction.Id; | |||||
| model.InteractionType = Interaction.Type; | |||||
| model.InteractionUserId = Interaction.UserId; | |||||
| } | |||||
| if(Reference != null) | |||||
| { | |||||
| model.ReferenceMessageId = Reference.MessageId.ToNullable(); | |||||
| model.ReferenceMessageGuildId = Reference.GuildId.ToNullable(); | |||||
| model.ReferenceMessageChannelId = Reference.ChannelId; | |||||
| } | |||||
| model.Attachments = Attachments.Select(x => | |||||
| { | |||||
| var attachmentModel = Discord.StateManager.GetModel<IAttachmentModel>(); | |||||
| attachmentModel.Width = x.Width; | |||||
| attachmentModel.Height = x.Height; | |||||
| attachmentModel.Size = x.Size; | |||||
| attachmentModel.Description = x.Description; | |||||
| attachmentModel.Ephemeral = x.Ephemeral; | |||||
| attachmentModel.FileName = x.Filename; | |||||
| attachmentModel.Url = x.Url; | |||||
| attachmentModel.ContentType = x.ContentType; | |||||
| attachmentModel.Id = x.Id; | |||||
| attachmentModel.ProxyUrl = x.ProxyUrl; | |||||
| return attachmentModel; | |||||
| }).ToArray(); | |||||
| model.Embeds = Embeds.Select(x => | |||||
| { | |||||
| var embedModel = Discord.StateManager.GetModel<IEmbedModel>(); | |||||
| embedModel.AuthorName = x.Author?.Name; | |||||
| embedModel.AuthorProxyIconUrl = x.Author?.ProxyIconUrl; | |||||
| embedModel.AuthorIconUrl = x.Author?.IconUrl; | |||||
| embedModel.AuthorUrl = x.Author?.Url; | |||||
| embedModel.Color = x.Color; | |||||
| embedModel.Description = x.Description; | |||||
| embedModel.Title = x.Title; | |||||
| embedModel.Timestamp = x.Timestamp?.UtcTicks; | |||||
| embedModel.Type = x.Type; | |||||
| embedModel.Url = x.Url; | |||||
| embedModel.ProviderName = x.Provider?.Name; | |||||
| embedModel.ProviderUrl = x.Provider?.Url; | |||||
| embedModel.FooterIconUrl = x.Footer?.IconUrl; | |||||
| embedModel.FooterProxyUrl = x.Footer?.ProxyUrl; | |||||
| embedModel.FooterText = x.Footer?.Text; | |||||
| var image = Discord.StateManager.GetModel<IEmbedMediaModel>(); | |||||
| image.Width = x.Image?.Width; | |||||
| image.Height = x.Image?.Height; | |||||
| image.Url = x.Image?.Url; | |||||
| image.ProxyUrl = x.Image?.ProxyUrl; | |||||
| embedModel.Image = image; | |||||
| var thumbnail = Discord.StateManager.GetModel<IEmbedMediaModel>(); | |||||
| thumbnail.Width = x.Thumbnail?.Width; | |||||
| thumbnail.Height = x.Thumbnail?.Height; | |||||
| thumbnail.Url = x.Thumbnail?.Url; | |||||
| thumbnail.ProxyUrl = x.Thumbnail?.ProxyUrl; | |||||
| embedModel.Thumbnail = thumbnail; | |||||
| var video = Discord.StateManager.GetModel<IEmbedMediaModel>(); | |||||
| video.Width = x.Video?.Width; | |||||
| video.Height = x.Video?.Height; | |||||
| video.Url = x.Video?.Url; | |||||
| embedModel.Video = video; | |||||
| embedModel.Fields = x.Fields.Select(x => | |||||
| { | |||||
| var fieldModel = Discord.StateManager.GetModel<IEmbedFieldModel>(); | |||||
| fieldModel.Name = x.Name; | |||||
| fieldModel.Value = x.Value; | |||||
| fieldModel.Inline = x.Inline; | |||||
| return fieldModel; | |||||
| }).ToArray(); | |||||
| return embedModel; | |||||
| }).ToArray(); | |||||
| model.Reactions = _reactions.GroupBy(x => x.Emote).Select(x => | |||||
| { | |||||
| var reactionMetadataModel = Discord.StateManager.GetModel<IReactionMetadataModel>(); | |||||
| reactionMetadataModel.Emoji = x.Key.ToModel(Discord.StateManager.GetModel<IEmojiModel>()); | |||||
| reactionMetadataModel.Users = x.Select(x => x.UserId).ToArray(); | |||||
| return reactionMetadataModel; | |||||
| }).ToArray(); | |||||
| var activityModel = Discord.StateManager.GetModel<IMessageActivityModel>(); | |||||
| activityModel.PartyId = Activity?.PartyId; | |||||
| activityModel.Type = Activity?.Type; | |||||
| model.Activity = activityModel; | |||||
| var applicationModel = Discord.StateManager.GetModel<IPartialApplicationModel>(); | |||||
| applicationModel.Description = Application.Description; | |||||
| applicationModel.Name = Application.Name; | |||||
| applicationModel.CoverImage = Application.CoverImage; | |||||
| applicationModel.Id = Application.Id; | |||||
| applicationModel.Icon = Application.Icon; | |||||
| model.Application = applicationModel; | |||||
| return model; | |||||
| } | |||||
| ~SocketMessage() => Dispose(); | |||||
| public void Dispose() | |||||
| { | |||||
| if (IsFreed) | |||||
| return; | |||||
| IsFreed = true; | |||||
| GC.SuppressFinalize(this); | |||||
| if (Discord.StateManager.TryGetMessageStore(Channel.Id, out var store)) | |||||
| store.RemoveReference(Id); | |||||
| } | |||||
| void ICached<Model>.Update(Model model) => Update(model); | |||||
| Model ICached<Model>.ToModel() => ToModel(); | |||||
| bool ICached.IsFreed => IsFreed; | |||||
| #endregion | |||||
| } | } | ||||
| } | } | ||||
| @@ -20,12 +20,19 @@ namespace Discord.WebSocket | |||||
| /// </returns> | /// </returns> | ||||
| public ulong UserId { get; } | public ulong UserId { get; } | ||||
| /// <summary> | /// <summary> | ||||
| /// Gets the ID of the message that has been reacted to. | |||||
| /// </summary> | |||||
| /// <returns> | |||||
| /// A message snowflake identifier associated with the message. | |||||
| /// </returns> | |||||
| public ulong MessageId { get; } | |||||
| /// <summary> | |||||
| /// Gets the user who added the reaction if possible. | /// Gets the user who added the reaction if possible. | ||||
| /// </summary> | /// </summary> | ||||
| /// <remarks> | /// <remarks> | ||||
| /// <para> | /// <para> | ||||
| /// This property attempts to retrieve a WebSocket-cached user that is responsible for this reaction from | /// This property attempts to retrieve a WebSocket-cached user that is responsible for this reaction from | ||||
| /// the client. In other words, when the user is not in the WebSocket cache, this property may not | |||||
| /// the client. In other words, when the user is not in the cache, this property may not | |||||
| /// contain a value, leaving the only identifiable information to be | /// contain a value, leaving the only identifiable information to be | ||||
| /// <see cref="Discord.WebSocket.SocketReaction.UserId" />. | /// <see cref="Discord.WebSocket.SocketReaction.UserId" />. | ||||
| /// </para> | /// </para> | ||||
| @@ -35,25 +42,16 @@ namespace Discord.WebSocket | |||||
| /// </para> | /// </para> | ||||
| /// </remarks> | /// </remarks> | ||||
| /// <returns> | /// <returns> | ||||
| /// A user object where possible; a value is not always returned. | |||||
| /// A lazily-cached user object. | |||||
| /// </returns> | /// </returns> | ||||
| /// <seealso cref="Optional{T}"/> | |||||
| public Optional<IUser> User { get; } | |||||
| /// <summary> | |||||
| /// Gets the ID of the message that has been reacted to. | |||||
| /// </summary> | |||||
| /// <returns> | |||||
| /// A message snowflake identifier associated with the message. | |||||
| /// </returns> | |||||
| public ulong MessageId { get; } | |||||
| public LazyCached<SocketUser> User { get; } | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the message that has been reacted to if possible. | /// Gets the message that has been reacted to if possible. | ||||
| /// </summary> | /// </summary> | ||||
| /// <returns> | /// <returns> | ||||
| /// A WebSocket-based message where possible; a value is not always returned. | |||||
| /// A lazily-cached message. | |||||
| /// </returns> | /// </returns> | ||||
| /// <seealso cref="Optional{T}"/> | |||||
| public Optional<SocketUserMessage> Message { get; } | |||||
| public LazyCached<SocketMessage> Message { get; } | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the channel where the reaction takes place in. | /// Gets the channel where the reaction takes place in. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -64,16 +62,26 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public IEmote Emote { get; } | public IEmote Emote { get; } | ||||
| internal SocketReaction(ISocketMessageChannel channel, ulong messageId, Optional<SocketUserMessage> message, ulong userId, Optional<IUser> user, IEmote emoji) | |||||
| internal SocketReaction(ISocketMessageChannel channel, ulong messageId, Optional<SocketUserMessage> message, ulong userId, Optional<SocketUser> user, IEmote emoji) | |||||
| { | { | ||||
| Channel = channel; | |||||
| var client = ((SocketChannel)channel).Discord; | |||||
| MessageId = messageId; | MessageId = messageId; | ||||
| Message = message; | |||||
| UserId = userId; | UserId = userId; | ||||
| User = user; | |||||
| Channel = channel; | |||||
| Message = message.IsSpecified | |||||
| ? new LazyCached<SocketMessage>(message.Value) | |||||
| : new LazyCached<SocketMessage>(messageId, client.StateManager.GetMessageStore(channel.Id)); | |||||
| User = user.IsSpecified | |||||
| ? new LazyCached<SocketUser>(user.Value) | |||||
| : new LazyCached<SocketUser>(userId, client.StateManager.UserStore); | |||||
| Emote = emoji; | Emote = emoji; | ||||
| } | } | ||||
| internal static SocketReaction Create(Model model, ISocketMessageChannel channel, Optional<SocketUserMessage> message, Optional<IUser> user) | |||||
| internal static SocketReaction Create(Model model, ISocketMessageChannel channel, Optional<SocketUserMessage> message, Optional<SocketUser> user) | |||||
| { | { | ||||
| IEmote emote; | IEmote emote; | ||||
| if (model.Emoji.Id.HasValue) | if (model.Emoji.Id.HasValue) | ||||
| @@ -86,11 +94,14 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override bool Equals(object other) | public override bool Equals(object other) | ||||
| { | { | ||||
| if (other == null) return false; | |||||
| if (other == this) return true; | |||||
| if (other == null) | |||||
| return false; | |||||
| if (other == this) | |||||
| return true; | |||||
| var otherReaction = other as SocketReaction; | var otherReaction = other as SocketReaction; | ||||
| if (otherReaction == null) return false; | |||||
| if (otherReaction == null) | |||||
| return false; | |||||
| return UserId == otherReaction.UserId && MessageId == otherReaction.MessageId && Emote.Equals(otherReaction.Emote); | return UserId == otherReaction.UserId && MessageId == otherReaction.MessageId && Emote.Equals(otherReaction.Emote); | ||||
| } | } | ||||
| @@ -13,13 +13,13 @@ namespace Discord.WebSocket | |||||
| : base(discord, id, channel, author, MessageSource.System) | : base(discord, id, channel, author, MessageSource.System) | ||||
| { | { | ||||
| } | } | ||||
| internal new static SocketSystemMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) | |||||
| internal new static SocketSystemMessage Create(DiscordSocketClient discord, ClientStateManager state, SocketUser author, ISocketMessageChannel channel, Model model) | |||||
| { | { | ||||
| var entity = new SocketSystemMessage(discord, model.Id, channel, author); | var entity = new SocketSystemMessage(discord, model.Id, channel, author); | ||||
| entity.Update(state, model); | entity.Update(state, model); | ||||
| return entity; | return entity; | ||||
| } | } | ||||
| internal override void Update(ClientState state, Model model) | |||||
| internal override void Update(ClientStateManager state, Model model) | |||||
| { | { | ||||
| base.Update(state, model); | base.Update(state, model); | ||||
| } | } | ||||
| @@ -5,7 +5,7 @@ using System.Collections.Immutable; | |||||
| using System.Diagnostics; | using System.Diagnostics; | ||||
| using System.Linq; | using System.Linq; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Model = Discord.API.Message; | |||||
| using Model = Discord.IMessageModel; | |||||
| namespace Discord.WebSocket | namespace Discord.WebSocket | ||||
| { | { | ||||
| @@ -17,11 +17,12 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| private bool _isMentioningEveryone, _isTTS, _isPinned; | private bool _isMentioningEveryone, _isTTS, _isPinned; | ||||
| private long? _editedTimestampTicks; | private long? _editedTimestampTicks; | ||||
| private IUserMessage _referencedMessage; | |||||
| private ImmutableArray<Attachment> _attachments = ImmutableArray.Create<Attachment>(); | private ImmutableArray<Attachment> _attachments = ImmutableArray.Create<Attachment>(); | ||||
| private ImmutableArray<Embed> _embeds = ImmutableArray.Create<Embed>(); | private ImmutableArray<Embed> _embeds = ImmutableArray.Create<Embed>(); | ||||
| private ImmutableArray<ITag> _tags = ImmutableArray.Create<ITag>(); | private ImmutableArray<ITag> _tags = ImmutableArray.Create<ITag>(); | ||||
| private ImmutableArray<SocketRole> _roleMentions = ImmutableArray.Create<SocketRole>(); | |||||
| private ulong[] _roleMentions; | |||||
| private ulong? _referencedMessageId; | |||||
| //private ImmutableArray<SocketRole> _roleMentions = ImmutableArray.Create<SocketRole>(); | |||||
| private ImmutableArray<SocketSticker> _stickers = ImmutableArray.Create<SocketSticker>(); | private ImmutableArray<SocketSticker> _stickers = ImmutableArray.Create<SocketSticker>(); | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -53,33 +54,29 @@ namespace Discord.WebSocket | |||||
| : base(discord, id, channel, author, source) | : base(discord, id, channel, author, source) | ||||
| { | { | ||||
| } | } | ||||
| internal new static SocketUserMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) | |||||
| internal new static SocketUserMessage Create(DiscordSocketClient discord, ClientStateManager state, SocketUser author, ISocketMessageChannel channel, Model model) | |||||
| { | { | ||||
| var entity = new SocketUserMessage(discord, model.Id, channel, author, MessageHelper.GetSource(model)); | var entity = new SocketUserMessage(discord, model.Id, channel, author, MessageHelper.GetSource(model)); | ||||
| entity.Update(state, model); | |||||
| entity.Update(model); | |||||
| return entity; | return entity; | ||||
| } | } | ||||
| internal override void Update(ClientState state, Model model) | |||||
| internal override void Update(Model model) | |||||
| { | { | ||||
| base.Update(state, model); | |||||
| base.Update(model); | |||||
| SocketGuild guild = (Channel as SocketGuildChannel)?.Guild; | SocketGuild guild = (Channel as SocketGuildChannel)?.Guild; | ||||
| if (model.IsTextToSpeech.IsSpecified) | |||||
| _isTTS = model.IsTextToSpeech.Value; | |||||
| if (model.Pinned.IsSpecified) | |||||
| _isPinned = model.Pinned.Value; | |||||
| if (model.EditedTimestamp.IsSpecified) | |||||
| _editedTimestampTicks = model.EditedTimestamp.Value?.UtcTicks; | |||||
| if (model.MentionEveryone.IsSpecified) | |||||
| _isMentioningEveryone = model.MentionEveryone.Value; | |||||
| if (model.RoleMentions.IsSpecified) | |||||
| _roleMentions = model.RoleMentions.Value.Select(x => guild.GetRole(x)).ToImmutableArray(); | |||||
| if (model.Attachments.IsSpecified) | |||||
| _isTTS = model.IsTextToSpeech; | |||||
| _isPinned = model.Pinned; | |||||
| _editedTimestampTicks = model.EditedTimestamp; | |||||
| _isMentioningEveryone = model.MentionEveryone; | |||||
| _roleMentions = model.RoleMentionIds; | |||||
| _referencedMessageId = model.ReferenceMessageId; | |||||
| if (model.Attachments != null && model.Attachments.Length > 0) | |||||
| { | { | ||||
| var value = model.Attachments.Value; | |||||
| var value = model.Attachments; | |||||
| if (value.Length > 0) | if (value.Length > 0) | ||||
| { | { | ||||
| var attachments = ImmutableArray.CreateBuilder<Attachment>(value.Length); | var attachments = ImmutableArray.CreateBuilder<Attachment>(value.Length); | ||||
| @@ -91,9 +88,9 @@ namespace Discord.WebSocket | |||||
| _attachments = ImmutableArray.Create<Attachment>(); | _attachments = ImmutableArray.Create<Attachment>(); | ||||
| } | } | ||||
| if (model.Embeds.IsSpecified) | |||||
| if (model.Embeds != null && model.Embeds.Length > 0) | |||||
| { | { | ||||
| var value = model.Embeds.Value; | |||||
| var value = model.Embeds; | |||||
| if (value.Length > 0) | if (value.Length > 0) | ||||
| { | { | ||||
| var embeds = ImmutableArray.CreateBuilder<Embed>(value.Length); | var embeds = ImmutableArray.CreateBuilder<Embed>(value.Length); | ||||
| @@ -105,41 +102,16 @@ namespace Discord.WebSocket | |||||
| _embeds = ImmutableArray.Create<Embed>(); | _embeds = ImmutableArray.Create<Embed>(); | ||||
| } | } | ||||
| if (model.Content.IsSpecified) | |||||
| if (model.Content != null) | |||||
| { | { | ||||
| var text = model.Content.Value; | |||||
| var text = model.Content; | |||||
| _tags = MessageHelper.ParseTags(text, Channel, guild, MentionedUsers); | _tags = MessageHelper.ParseTags(text, Channel, guild, MentionedUsers); | ||||
| model.Content = text; | model.Content = text; | ||||
| } | } | ||||
| if (model.ReferencedMessage.IsSpecified && model.ReferencedMessage.Value != null) | |||||
| { | |||||
| var refMsg = model.ReferencedMessage.Value; | |||||
| ulong? webhookId = refMsg.WebhookId.ToNullable(); | |||||
| SocketUser refMsgAuthor = null; | |||||
| if (refMsg.Author.IsSpecified) | |||||
| { | |||||
| if (guild != null) | |||||
| { | |||||
| if (webhookId != null) | |||||
| refMsgAuthor = SocketWebhookUser.Create(guild, state, refMsg.Author.Value, webhookId.Value); | |||||
| else | |||||
| refMsgAuthor = guild.GetUser(refMsg.Author.Value.Id); | |||||
| } | |||||
| else | |||||
| refMsgAuthor = (Channel as SocketChannel).GetUser(refMsg.Author.Value.Id); | |||||
| if (refMsgAuthor == null) | |||||
| refMsgAuthor = SocketUnknownUser.Create(Discord, state, refMsg.Author.Value); | |||||
| } | |||||
| else | |||||
| // Message author wasn't specified in the payload, so create a completely anonymous unknown user | |||||
| refMsgAuthor = new SocketUnknownUser(Discord, id: 0); | |||||
| _referencedMessage = SocketUserMessage.Create(Discord, state, refMsgAuthor, Channel, refMsg); | |||||
| } | |||||
| if (model.StickerItems.IsSpecified) | |||||
| if (model.Stickers != null && model.Stickers.Length > 0) | |||||
| { | { | ||||
| var value = model.StickerItems.Value; | |||||
| var value = model.Stickers; | |||||
| if (value.Length > 0) | if (value.Length > 0) | ||||
| { | { | ||||
| var stickers = ImmutableArray.CreateBuilder<SocketSticker>(value.Length); | var stickers = ImmutableArray.CreateBuilder<SocketSticker>(value.Length); | ||||
| @@ -67,13 +67,13 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| Guild = guild; | Guild = guild; | ||||
| } | } | ||||
| internal static SocketRole Create(SocketGuild guild, ClientState state, Model model) | |||||
| internal static SocketRole Create(SocketGuild guild, ClientStateManager state, Model model) | |||||
| { | { | ||||
| var entity = new SocketRole(guild, model.Id); | var entity = new SocketRole(guild, model.Id); | ||||
| entity.Update(state, model); | entity.Update(state, model); | ||||
| return entity; | return entity; | ||||
| } | } | ||||
| internal void Update(ClientState state, Model model) | |||||
| internal void Update(ClientStateManager state, Model model) | |||||
| { | { | ||||
| Name = model.Name; | Name = model.Name; | ||||
| IsHoisted = model.Hoist; | IsHoisted = model.Hoist; | ||||
| @@ -1,7 +1,7 @@ | |||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Diagnostics; | using System.Diagnostics; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Model = Discord.API.StickerItem; | |||||
| using Model = Discord.IStickerItemModel; | |||||
| namespace Discord.WebSocket | namespace Discord.WebSocket | ||||
| { | { | ||||
| @@ -47,7 +47,7 @@ namespace Discord.WebSocket | |||||
| internal void Update(Model model) | internal void Update(Model model) | ||||
| { | { | ||||
| Name = model.Name; | Name = model.Name; | ||||
| Format = model.FormatType; | |||||
| Format = model.Format; | |||||
| } | } | ||||
| /// <summary> | /// <summary> | ||||
| @@ -1,51 +1,36 @@ | |||||
| using System; | using System; | ||||
| using System.Diagnostics; | using System.Diagnostics; | ||||
| using System.Linq; | using System.Linq; | ||||
| using Model = Discord.API.User; | |||||
| using Model = Discord.IUserModel; | |||||
| namespace Discord.WebSocket | namespace Discord.WebSocket | ||||
| { | { | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| internal class SocketGlobalUser : SocketUser | |||||
| internal class SocketGlobalUser : SocketUser, IDisposable | |||||
| { | { | ||||
| public override bool IsBot { get; internal set; } | public override bool IsBot { get; internal set; } | ||||
| public override string Username { get; internal set; } | public override string Username { get; internal set; } | ||||
| public override ushort DiscriminatorValue { get; internal set; } | public override ushort DiscriminatorValue { get; internal set; } | ||||
| public override string AvatarId { get; internal set; } | public override string AvatarId { get; internal set; } | ||||
| internal override SocketPresence Presence { get; set; } | |||||
| public override bool IsWebhook => false; | public override bool IsWebhook => false; | ||||
| internal override SocketGlobalUser GlobalUser { get => this; set => throw new NotImplementedException(); } | |||||
| private readonly object _lockObj = new object(); | |||||
| private ushort _references; | |||||
| private SocketGlobalUser(DiscordSocketClient discord, ulong id) | private SocketGlobalUser(DiscordSocketClient discord, ulong id) | ||||
| : base(discord, id) | : base(discord, id) | ||||
| { | { | ||||
| } | } | ||||
| internal static SocketGlobalUser Create(DiscordSocketClient discord, ClientState state, Model model) | |||||
| internal static SocketGlobalUser Create(DiscordSocketClient discord, Model model) | |||||
| { | { | ||||
| var entity = new SocketGlobalUser(discord, model.Id); | var entity = new SocketGlobalUser(discord, model.Id); | ||||
| entity.Update(state, model); | |||||
| entity.Update(model); | |||||
| return entity; | return entity; | ||||
| } | } | ||||
| internal void AddRef() | |||||
| { | |||||
| checked | |||||
| { | |||||
| lock (_lockObj) | |||||
| _references++; | |||||
| } | |||||
| } | |||||
| internal void RemoveRef(DiscordSocketClient discord) | |||||
| ~SocketGlobalUser() => Dispose(); | |||||
| public override void Dispose() | |||||
| { | { | ||||
| lock (_lockObj) | |||||
| { | |||||
| if (--_references <= 0) | |||||
| discord.RemoveUser(Id); | |||||
| } | |||||
| GC.SuppressFinalize(this); | |||||
| Discord.StateManager.UserStore.RemoveReference(Id); | |||||
| } | } | ||||
| private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Global)"; | private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Global)"; | ||||
| @@ -18,38 +18,33 @@ namespace Discord.WebSocket | |||||
| /// A <see cref="SocketGroupChannel" /> representing the channel of which the user belongs to. | /// A <see cref="SocketGroupChannel" /> representing the channel of which the user belongs to. | ||||
| /// </returns> | /// </returns> | ||||
| public SocketGroupChannel Channel { get; } | public SocketGroupChannel Channel { get; } | ||||
| /// <inheritdoc /> | |||||
| internal override SocketGlobalUser GlobalUser { get; set; } | |||||
| /// <inheritdoc /> | |||||
| public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } } | |||||
| /// <inheritdoc /> | |||||
| public override string Username { get { return GlobalUser.Username; } internal set { GlobalUser.Username = value; } } | |||||
| /// <inheritdoc /> | |||||
| public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } } | |||||
| /// <inheritdoc /> | |||||
| public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } | |||||
| /// <inheritdoc /> | |||||
| internal override SocketPresence Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override bool IsWebhook => false; | public override bool IsWebhook => false; | ||||
| internal SocketGroupUser(SocketGroupChannel channel, SocketGlobalUser globalUser) | |||||
| : base(channel.Discord, globalUser.Id) | |||||
| internal SocketGroupUser(SocketGroupChannel channel, ulong userId) | |||||
| : base(channel.Discord, userId) | |||||
| { | { | ||||
| Channel = channel; | Channel = channel; | ||||
| GlobalUser = globalUser; | |||||
| } | } | ||||
| internal static SocketGroupUser Create(SocketGroupChannel channel, ClientState state, Model model) | |||||
| internal static SocketGroupUser Create(SocketGroupChannel channel, Model model) | |||||
| { | { | ||||
| var entity = new SocketGroupUser(channel, channel.Discord.GetOrCreateUser(state, model)); | |||||
| entity.Update(state, model); | |||||
| var entity = new SocketGroupUser(channel, model.Id); | |||||
| entity.Update(model); | |||||
| return entity; | return entity; | ||||
| } | } | ||||
| private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Group)"; | private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Group)"; | ||||
| internal new SocketGroupUser Clone() => MemberwiseClone() as SocketGroupUser; | internal new SocketGroupUser Clone() => MemberwiseClone() as SocketGroupUser; | ||||
| public override void Dispose() | |||||
| { | |||||
| GC.SuppressFinalize(this); | |||||
| if (GlobalUser.IsValueCreated) | |||||
| GlobalUser.Value.Dispose(); | |||||
| } | |||||
| ~SocketGroupUser() => Dispose(); | |||||
| #endregion | #endregion | ||||
| #region IVoiceState | #region IVoiceState | ||||
| @@ -6,9 +6,9 @@ using System.Collections.Immutable; | |||||
| using System.Diagnostics; | using System.Diagnostics; | ||||
| using System.Linq; | using System.Linq; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using UserModel = Discord.API.User; | |||||
| using MemberModel = Discord.API.GuildMember; | |||||
| using PresenceModel = Discord.API.Presence; | |||||
| using UserModel = Discord.IUserModel; | |||||
| using MemberModel = Discord.IMemberModel; | |||||
| using PresenceModel = Discord.IPresenceModel; | |||||
| namespace Discord.WebSocket | namespace Discord.WebSocket | ||||
| { | { | ||||
| @@ -16,19 +16,23 @@ namespace Discord.WebSocket | |||||
| /// Represents a WebSocket-based guild user. | /// Represents a WebSocket-based guild user. | ||||
| /// </summary> | /// </summary> | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| public class SocketGuildUser : SocketUser, IGuildUser | |||||
| public class SocketGuildUser : SocketUser, IGuildUser, ICached<MemberModel>, IDisposable | |||||
| { | { | ||||
| #region SocketGuildUser | #region SocketGuildUser | ||||
| private long? _premiumSinceTicks; | private long? _premiumSinceTicks; | ||||
| private long? _timedOutTicks; | private long? _timedOutTicks; | ||||
| private long? _joinedAtTicks; | private long? _joinedAtTicks; | ||||
| private ImmutableArray<ulong> _roleIds; | private ImmutableArray<ulong> _roleIds; | ||||
| private ulong _guildId; | |||||
| internal override SocketGlobalUser GlobalUser { get; set; } | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the guild the user is in. | /// Gets the guild the user is in. | ||||
| /// </summary> | /// </summary> | ||||
| public SocketGuild Guild { get; } | |||||
| public Lazy<SocketGuild> Guild { get; } // TODO: convert to LazyCached once guilds are cached. | |||||
| /// <summary> | |||||
| /// Gets the ID of the guild that the user is in. | |||||
| /// </summary> | |||||
| public ulong GuildId => _guildId; | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public string DisplayName => Nickname ?? Username; | public string DisplayName => Nickname ?? Username; | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -38,17 +42,16 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public string GuildAvatarId { get; private set; } | public string GuildAvatarId { get; private set; } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } } | |||||
| public override bool IsBot { get { return GlobalUser.Value.IsBot; } internal set { GlobalUser.Value.IsBot = value; } } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override string Username { get { return GlobalUser.Username; } internal set { GlobalUser.Username = value; } } | |||||
| public override string Username { get { return GlobalUser.Value.Username; } internal set { GlobalUser.Value.Username = value; } } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } } | |||||
| public override ushort DiscriminatorValue { get { return GlobalUser.Value.DiscriminatorValue; } internal set { GlobalUser.Value.DiscriminatorValue = value; } } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } | |||||
| public override string AvatarId { get { return GlobalUser.Value.AvatarId; } internal set { GlobalUser.Value.AvatarId = value; } } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public GuildPermissions GuildPermissions => new GuildPermissions(Permissions.ResolveGuild(Guild, this)); | |||||
| internal override SocketPresence Presence { get; set; } | |||||
| public GuildPermissions GuildPermissions => new GuildPermissions(Permissions.ResolveGuild(Guild.Value, this)); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override bool IsWebhook => false; | public override bool IsWebhook => false; | ||||
| @@ -71,14 +74,13 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public bool? IsPending { get; private set; } | public bool? IsPending { get; private set; } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); | public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); | ||||
| /// <summary> | /// <summary> | ||||
| /// Returns a collection of roles that the user possesses. | /// Returns a collection of roles that the user possesses. | ||||
| /// </summary> | /// </summary> | ||||
| public IReadOnlyCollection<SocketRole> Roles | public IReadOnlyCollection<SocketRole> Roles | ||||
| => _roleIds.Select(id => Guild.GetRole(id)).Where(x => x != null).ToReadOnlyCollection(() => _roleIds.Length); | |||||
| => _roleIds.Select(id => Guild.Value.GetRole(id)).Where(x => x != null).ToReadOnlyCollection(() => _roleIds.Length); | |||||
| /// <summary> | /// <summary> | ||||
| /// Returns the voice channel the user is in, or <c>null</c> if none. | /// Returns the voice channel the user is in, or <c>null</c> if none. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -92,8 +94,8 @@ namespace Discord.WebSocket | |||||
| /// A <see cref="SocketVoiceState" /> representing the user's voice status; <c>null</c> if the user is not | /// A <see cref="SocketVoiceState" /> representing the user's voice status; <c>null</c> if the user is not | ||||
| /// connected to a voice channel. | /// connected to a voice channel. | ||||
| /// </returns> | /// </returns> | ||||
| public SocketVoiceState? VoiceState => Guild.GetVoiceState(Id); | |||||
| public AudioInStream AudioStream => Guild.GetAudioStream(Id); | |||||
| public SocketVoiceState? VoiceState => Guild.Value.GetVoiceState(Id); | |||||
| public AudioInStream AudioStream => Guild.Value.GetAudioStream(Id); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public DateTimeOffset? PremiumSince => DateTimeUtils.FromTicks(_premiumSinceTicks); | public DateTimeOffset? PremiumSince => DateTimeUtils.FromTicks(_premiumSinceTicks); | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -119,13 +121,13 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| get | get | ||||
| { | { | ||||
| if (Guild.OwnerId == Id) | |||||
| if (Guild.Value.OwnerId == Id) | |||||
| return int.MaxValue; | return int.MaxValue; | ||||
| int maxPos = 0; | int maxPos = 0; | ||||
| for (int i = 0; i < _roleIds.Length; i++) | for (int i = 0; i < _roleIds.Length; i++) | ||||
| { | { | ||||
| var role = Guild.GetRole(_roleIds[i]); | |||||
| var role = Guild.Value.GetRole(_roleIds[i]); | |||||
| if (role != null && role.Position > maxPos) | if (role != null && role.Position > maxPos) | ||||
| maxPos = role.Position; | maxPos = role.Position; | ||||
| } | } | ||||
| @@ -133,79 +135,43 @@ namespace Discord.WebSocket | |||||
| } | } | ||||
| } | } | ||||
| internal SocketGuildUser(SocketGuild guild, SocketGlobalUser globalUser) | |||||
| : base(guild.Discord, globalUser.Id) | |||||
| internal SocketGuildUser(ulong guildId, ulong userId, DiscordSocketClient client) | |||||
| : base(client, userId) | |||||
| { | { | ||||
| Guild = guild; | |||||
| GlobalUser = globalUser; | |||||
| _guildId = guildId; | |||||
| Guild = new Lazy<SocketGuild>(() => client.StateManager.GetGuild(_guildId), System.Threading.LazyThreadSafetyMode.PublicationOnly); | |||||
| } | } | ||||
| internal static SocketGuildUser Create(SocketGuild guild, ClientState state, UserModel model) | |||||
| internal static SocketGuildUser Create(ulong guildId, DiscordSocketClient client, UserModel model) | |||||
| { | { | ||||
| var entity = new SocketGuildUser(guild, guild.Discord.GetOrCreateUser(state, model)); | |||||
| entity.Update(state, model); | |||||
| entity.UpdateRoles(new ulong[0]); | |||||
| var entity = new SocketGuildUser(guildId, model.Id, client); | |||||
| if (entity.Update(model)) | |||||
| client.StateManager.GetMemberStore(guildId)?.AddOrUpdate(entity.ToModel()); | |||||
| entity.UpdateRoles(Array.Empty<ulong>()); | |||||
| return entity; | return entity; | ||||
| } | } | ||||
| internal static SocketGuildUser Create(SocketGuild guild, ClientState state, MemberModel model) | |||||
| internal static SocketGuildUser Create(ulong guildId, DiscordSocketClient client, MemberModel model) | |||||
| { | { | ||||
| var entity = new SocketGuildUser(guild, guild.Discord.GetOrCreateUser(state, model.User)); | |||||
| entity.Update(state, model); | |||||
| if (!model.Roles.IsSpecified) | |||||
| entity.UpdateRoles(new ulong[0]); | |||||
| var entity = new SocketGuildUser(guildId, model.Id, client); | |||||
| entity.Update(model); | |||||
| client.StateManager.GetMemberStore(guildId)?.AddOrUpdate(model); | |||||
| return entity; | return entity; | ||||
| } | } | ||||
| internal static SocketGuildUser Create(SocketGuild guild, ClientState state, PresenceModel model) | |||||
| { | |||||
| var entity = new SocketGuildUser(guild, guild.Discord.GetOrCreateUser(state, model.User)); | |||||
| entity.Update(state, model, false); | |||||
| if (!model.Roles.IsSpecified) | |||||
| entity.UpdateRoles(new ulong[0]); | |||||
| return entity; | |||||
| } | |||||
| internal void Update(ClientState state, MemberModel model) | |||||
| { | |||||
| base.Update(state, model.User); | |||||
| if (model.JoinedAt.IsSpecified) | |||||
| _joinedAtTicks = model.JoinedAt.Value.UtcTicks; | |||||
| if (model.Nick.IsSpecified) | |||||
| Nickname = model.Nick.Value; | |||||
| if (model.Avatar.IsSpecified) | |||||
| GuildAvatarId = model.Avatar.Value; | |||||
| if (model.Roles.IsSpecified) | |||||
| UpdateRoles(model.Roles.Value); | |||||
| if (model.PremiumSince.IsSpecified) | |||||
| _premiumSinceTicks = model.PremiumSince.Value?.UtcTicks; | |||||
| if (model.TimedOutUntil.IsSpecified) | |||||
| _timedOutTicks = model.TimedOutUntil.Value?.UtcTicks; | |||||
| if (model.Pending.IsSpecified) | |||||
| IsPending = model.Pending.Value; | |||||
| } | |||||
| internal void Update(ClientState state, PresenceModel model, bool updatePresence) | |||||
| { | |||||
| if (updatePresence) | |||||
| { | |||||
| Update(model); | |||||
| } | |||||
| if (model.Nick.IsSpecified) | |||||
| Nickname = model.Nick.Value; | |||||
| if (model.Roles.IsSpecified) | |||||
| UpdateRoles(model.Roles.Value); | |||||
| if (model.PremiumSince.IsSpecified) | |||||
| _premiumSinceTicks = model.PremiumSince.Value?.UtcTicks; | |||||
| } | |||||
| internal override void Update(PresenceModel model) | |||||
| internal void Update(MemberModel model) | |||||
| { | { | ||||
| Presence ??= new SocketPresence(); | |||||
| Presence.Update(model); | |||||
| GlobalUser.Update(model); | |||||
| _joinedAtTicks = model.JoinedAt.HasValue ? model.JoinedAt.Value.UtcTicks : null; | |||||
| Nickname = model.Nickname; | |||||
| GuildAvatarId = model.GuildAvatar; | |||||
| UpdateRoles(model.Roles); | |||||
| if (model.PremiumSince.HasValue) | |||||
| _premiumSinceTicks = model.PremiumSince.Value.UtcTicks; | |||||
| if (model.CommunicationsDisabledUntil.HasValue) | |||||
| _timedOutTicks = model.CommunicationsDisabledUntil.Value.UtcTicks; | |||||
| IsPending = model.IsPending.GetValueOrDefault(false); | |||||
| } | } | ||||
| 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); | ||||
| roles.Add(Guild.Id); | |||||
| roles.Add(_guildId); | |||||
| for (int i = 0; i < roleIds.Length; i++) | for (int i = 0; i < roleIds.Length; i++) | ||||
| roles.Add(roleIds[i]); | roles.Add(roleIds[i]); | ||||
| _roleIds = roles.ToImmutable(); | _roleIds = roles.ToImmutable(); | ||||
| @@ -249,7 +215,7 @@ namespace Discord.WebSocket | |||||
| => UserHelper.RemoveTimeOutAsync(this, Discord, options); | => UserHelper.RemoveTimeOutAsync(this, Discord, options); | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public ChannelPermissions GetPermissions(IGuildChannel channel) | public ChannelPermissions GetPermissions(IGuildChannel channel) | ||||
| => new ChannelPermissions(Permissions.ResolveChannel(Guild, this, channel, GuildPermissions.RawValue)); | |||||
| => new ChannelPermissions(Permissions.ResolveChannel(Guild.Value, this, channel, GuildPermissions.RawValue)); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public string GetDisplayAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) | public string GetDisplayAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) | ||||
| @@ -259,23 +225,30 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public string GetGuildAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) | public string GetGuildAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) | ||||
| => CDN.GetGuildUserAvatarUrl(Id, Guild.Id, GuildAvatarId, size, format); | |||||
| => CDN.GetGuildUserAvatarUrl(Id, _guildId, GuildAvatarId, size, format); | |||||
| private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Guild)"; | private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Guild)"; | ||||
| internal new SocketGuildUser Clone() | |||||
| internal new SocketGuildUser Clone() => MemberwiseClone() as SocketGuildUser; | |||||
| public override void Dispose() | |||||
| { | { | ||||
| var clone = MemberwiseClone() as SocketGuildUser; | |||||
| clone.GlobalUser = GlobalUser.Clone(); | |||||
| return clone; | |||||
| if (IsFreed) | |||||
| return; | |||||
| GC.SuppressFinalize(this); | |||||
| Discord.StateManager.GetMemberStore(_guildId)?.RemoveReference(Id); | |||||
| IsFreed = true; | |||||
| } | } | ||||
| ~SocketGuildUser() => Dispose(); | |||||
| #endregion | #endregion | ||||
| #region IGuildUser | #region IGuildUser | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| IGuild IGuildUser.Guild => Guild; | |||||
| IGuild IGuildUser.Guild => Guild.Value; | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| ulong IGuildUser.GuildId => Guild.Id; | |||||
| ulong IGuildUser.GuildId => _guildId; | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| IReadOnlyCollection<ulong> IGuildUser.RoleIds => _roleIds; | IReadOnlyCollection<ulong> IGuildUser.RoleIds => _roleIds; | ||||
| @@ -283,5 +256,50 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| IVoiceChannel IVoiceState.VoiceChannel => VoiceChannel; | IVoiceChannel IVoiceState.VoiceChannel => VoiceChannel; | ||||
| #endregion | #endregion | ||||
| #region Cache | |||||
| internal new class CacheModel : MemberModel | |||||
| { | |||||
| public ulong Id { get; set; } | |||||
| public string Nickname { get; set; } | |||||
| public string GuildAvatar { get; set; } | |||||
| public ulong[] Roles { get; set; } | |||||
| public DateTimeOffset? JoinedAt { get; set; } | |||||
| public DateTimeOffset? PremiumSince { get; set; } | |||||
| public bool IsDeaf { get; set; } | |||||
| public bool IsMute { get; set; } | |||||
| public bool? IsPending { get; set; } | |||||
| public DateTimeOffset? CommunicationsDisabledUntil { get; set; } | |||||
| } | |||||
| internal new MemberModel ToModel() | |||||
| { | |||||
| var model = Discord.StateManager.GetModel<MemberModel, CacheModel>(); | |||||
| model.Id = Id; | |||||
| model.Nickname = Nickname; | |||||
| model.GuildAvatar = GuildAvatarId; | |||||
| model.Roles = _roleIds.ToArray(); | |||||
| model.JoinedAt = JoinedAt; | |||||
| model.PremiumSince = PremiumSince; | |||||
| model.IsDeaf = IsDeafened; | |||||
| model.IsMute = IsMuted; | |||||
| model.IsPending = IsPending; | |||||
| model.CommunicationsDisabledUntil = TimedOutUntil; | |||||
| return model; | |||||
| } | |||||
| MemberModel ICached<MemberModel>.ToModel() | |||||
| => ToModel(); | |||||
| void ICached<MemberModel>.Update(MemberModel model) => Update(model); | |||||
| #endregion | |||||
| } | } | ||||
| } | } | ||||
| @@ -3,7 +3,7 @@ using System.Collections.Generic; | |||||
| using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
| using System.Diagnostics; | using System.Diagnostics; | ||||
| using System.Linq; | using System.Linq; | ||||
| using Model = Discord.API.Presence; | |||||
| using Model = Discord.IPresenceModel; | |||||
| namespace Discord.WebSocket | namespace Discord.WebSocket | ||||
| { | { | ||||
| @@ -11,8 +11,13 @@ 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 class SocketPresence : IPresence | |||||
| public class SocketPresence : IPresence, ICached<Model> | |||||
| { | { | ||||
| internal ulong UserId; | |||||
| internal ulong? GuildId; | |||||
| internal bool IsFreed; | |||||
| internal DiscordSocketClient Discord; | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public UserStatus Status { get; private set; } | public UserStatus Status { get; private set; } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -20,17 +25,24 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public IReadOnlyCollection<IActivity> Activities { get; private set; } | public IReadOnlyCollection<IActivity> Activities { get; private set; } | ||||
| internal SocketPresence() { } | |||||
| internal SocketPresence(UserStatus status, IImmutableSet<ClientType> activeClients, IImmutableList<IActivity> activities) | |||||
| public static SocketPresence Default | |||||
| => new SocketPresence(null, UserStatus.Offline, null, null); | |||||
| internal SocketPresence(DiscordSocketClient discord) | |||||
| { | |||||
| Discord = discord; | |||||
| } | |||||
| internal SocketPresence(DiscordSocketClient discord, UserStatus status, IImmutableSet<ClientType> activeClients, IImmutableList<IActivity> activities) | |||||
| : this(discord) | |||||
| { | { | ||||
| 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(DiscordSocketClient client, Model model) | |||||
| { | { | ||||
| var entity = new SocketPresence(); | |||||
| var entity = new SocketPresence(client); | |||||
| entity.Update(model); | entity.Update(model); | ||||
| return entity; | return entity; | ||||
| } | } | ||||
| @@ -38,8 +50,10 @@ namespace Discord.WebSocket | |||||
| internal void Update(Model model) | internal void Update(Model model) | ||||
| { | { | ||||
| Status = model.Status; | Status = model.Status; | ||||
| ActiveClients = ConvertClientTypesDict(model.ClientStatus.GetValueOrDefault()) ?? ImmutableArray<ClientType>.Empty; | |||||
| ActiveClients = model.ActiveClients.Length > 0 ? model.ActiveClients.ToImmutableArray() : ImmutableArray<ClientType>.Empty; | |||||
| Activities = ConvertActivitiesList(model.Activities) ?? ImmutableArray<IActivity>.Empty; | Activities = ConvertActivitiesList(model.Activities) ?? ImmutableArray<IActivity>.Empty; | ||||
| UserId = model.UserId; | |||||
| GuildId = model.GuildId; | |||||
| } | } | ||||
| /// <summary> | /// <summary> | ||||
| @@ -76,9 +90,9 @@ namespace Discord.WebSocket | |||||
| /// <returns> | /// <returns> | ||||
| /// A list of all <see cref="IActivity"/> that this user currently has available. | /// A list of all <see cref="IActivity"/> that this user currently has available. | ||||
| /// </returns> | /// </returns> | ||||
| private static IImmutableList<IActivity> ConvertActivitiesList(IList<API.Game> activities) | |||||
| private static IImmutableList<IActivity> ConvertActivitiesList(IActivityModel[] activities) | |||||
| { | { | ||||
| if (activities == null || activities.Count == 0) | |||||
| if (activities == null || activities.Length == 0) | |||||
| return ImmutableList<IActivity>.Empty; | return ImmutableList<IActivity>.Empty; | ||||
| var list = new List<IActivity>(); | var list = new List<IActivity>(); | ||||
| foreach (var activity in activities) | foreach (var activity in activities) | ||||
| @@ -96,5 +110,122 @@ namespace Discord.WebSocket | |||||
| private string DebuggerDisplay => $"{Status}{(Activities?.FirstOrDefault()?.Name ?? "")}"; | private string DebuggerDisplay => $"{Status}{(Activities?.FirstOrDefault()?.Name ?? "")}"; | ||||
| internal SocketPresence Clone() => MemberwiseClone() as SocketPresence; | internal SocketPresence Clone() => MemberwiseClone() as SocketPresence; | ||||
| ~SocketPresence() => Dispose(); | |||||
| public void Dispose() | |||||
| { | |||||
| if (IsFreed) | |||||
| return; | |||||
| GC.SuppressFinalize(this); | |||||
| if(Discord != null) | |||||
| { | |||||
| Discord.StateManager.PresenceStore.RemoveReference(UserId); | |||||
| IsFreed = true; | |||||
| } | |||||
| } | |||||
| #region Cache | |||||
| internal class CacheModel : Model | |||||
| { | |||||
| public UserStatus Status { get; set; } | |||||
| public ClientType[] ActiveClients { get; set; } | |||||
| public IActivityModel[] Activities { get; set; } | |||||
| public ulong UserId { get; set; } | |||||
| public ulong? GuildId { get; set; } | |||||
| ulong IEntityModel<ulong>.Id | |||||
| { | |||||
| get => UserId; | |||||
| set => UserId = value; | |||||
| } | |||||
| } | |||||
| internal class ActivityCacheModel : IActivityModel | |||||
| { | |||||
| public string Id { get; set; } | |||||
| public string Url { get; set; } | |||||
| public string Name { get; set; } | |||||
| public ActivityType Type { get; set; } | |||||
| public string Details { get; set; } | |||||
| public string State { get; set; } | |||||
| public ActivityProperties Flags { get; set; } | |||||
| public DateTimeOffset CreatedAt { get; set; } | |||||
| public IEmojiModel Emoji { get; set; } | |||||
| public ulong? ApplicationId { get; set; } | |||||
| public string SyncId { get; set; } | |||||
| public string SessionId { get; set; } | |||||
| public string LargeImage { get; set; } | |||||
| public string LargeText { get; set; } | |||||
| public string SmallImage { get; set; } | |||||
| public string SmallText { get; set; } | |||||
| public string PartyId { get; set; } | |||||
| public long[] PartySize { get; set; } | |||||
| public string JoinSecret { get; set; } | |||||
| public string SpectateSecret { get; set; } | |||||
| public string MatchSecret { get; set; } | |||||
| public DateTimeOffset? TimestampStart { get; set; } | |||||
| public DateTimeOffset? TimestampEnd { get; set; } | |||||
| } | |||||
| private class EmojiCacheModel : IEmojiModel | |||||
| { | |||||
| public ulong? Id { get; set; } | |||||
| public string Name { get; set; } | |||||
| public ulong[] Roles { get; set; } | |||||
| public bool RequireColons { get; set; } | |||||
| public bool IsManaged { get; set; } | |||||
| public bool IsAnimated { get; set; } | |||||
| public bool IsAvailable { get; set; } | |||||
| public ulong? CreatorId { get; set; } | |||||
| } | |||||
| internal Model ToModel() | |||||
| { | |||||
| var model = Discord.StateManager.GetModel<Model, CacheModel>(); | |||||
| model.Status = Status; | |||||
| model.ActiveClients = ActiveClients.ToArray(); | |||||
| model.UserId = UserId; | |||||
| model.GuildId = GuildId; | |||||
| model.Activities = Activities.Select(x => | |||||
| { | |||||
| switch (x) | |||||
| { | |||||
| case Game game: | |||||
| switch (game) | |||||
| { | |||||
| case RichGame richGame: | |||||
| return richGame.ToModel<ActivityCacheModel>(); | |||||
| case SpotifyGame spotify: | |||||
| return spotify.ToModel<ActivityCacheModel>(); | |||||
| case CustomStatusGame custom: | |||||
| return custom.ToModel<ActivityCacheModel, EmojiCacheModel>(); | |||||
| case StreamingGame stream: | |||||
| return stream.ToModel<ActivityCacheModel>(); | |||||
| } | |||||
| break; | |||||
| } | |||||
| return new ActivityCacheModel | |||||
| { | |||||
| Name = x.Name, | |||||
| Details = x.Details, | |||||
| Flags = x.Flags, | |||||
| Type = x.Type | |||||
| }; | |||||
| }).ToArray(); | |||||
| return model; | |||||
| } | |||||
| Model ICached<Model>.ToModel() => ToModel(); | |||||
| void ICached<Model>.Update(Model model) => Update(model); | |||||
| bool ICached.IsFreed => IsFreed; | |||||
| #endregion | |||||
| } | } | ||||
| } | } | ||||
| @@ -2,7 +2,8 @@ using Discord.Rest; | |||||
| using System; | using System; | ||||
| using System.Diagnostics; | using System.Diagnostics; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Model = Discord.API.User; | |||||
| using Model = Discord.ICurrentUserModel; | |||||
| using UserModel = Discord.IUserModel; | |||||
| namespace Discord.WebSocket | namespace Discord.WebSocket | ||||
| { | { | ||||
| @@ -10,7 +11,7 @@ namespace Discord.WebSocket | |||||
| /// Represents the logged-in WebSocket-based user. | /// Represents the logged-in WebSocket-based user. | ||||
| /// </summary> | /// </summary> | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| public class SocketSelfUser : SocketUser, ISelfUser | |||||
| public class SocketSelfUser : SocketUser, ISelfUser, ICached<Model> | |||||
| { | { | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public string Email { get; private set; } | public string Email { get; private set; } | ||||
| @@ -18,18 +19,6 @@ namespace Discord.WebSocket | |||||
| public bool IsVerified { get; private set; } | public bool IsVerified { get; private set; } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public bool IsMfaEnabled { get; private set; } | public bool IsMfaEnabled { get; private set; } | ||||
| internal override SocketGlobalUser GlobalUser { get; set; } | |||||
| /// <inheritdoc /> | |||||
| public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } } | |||||
| /// <inheritdoc /> | |||||
| public override string Username { get { return GlobalUser.Username; } internal set { GlobalUser.Username = value; } } | |||||
| /// <inheritdoc /> | |||||
| public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } } | |||||
| /// <inheritdoc /> | |||||
| public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } | |||||
| /// <inheritdoc /> | |||||
| internal override SocketPresence Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public UserProperties Flags { get; internal set; } | public UserProperties Flags { get; internal set; } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -40,48 +29,52 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override bool IsWebhook => false; | public override bool IsWebhook => false; | ||||
| internal SocketSelfUser(DiscordSocketClient discord, SocketGlobalUser globalUser) | |||||
| : base(discord, globalUser.Id) | |||||
| internal SocketSelfUser(DiscordSocketClient discord, ulong userId) | |||||
| : base(discord, userId) | |||||
| { | { | ||||
| GlobalUser = globalUser; | |||||
| } | } | ||||
| internal static SocketSelfUser Create(DiscordSocketClient discord, ClientState state, Model model) | |||||
| internal static SocketSelfUser Create(DiscordSocketClient discord, Model model) | |||||
| { | { | ||||
| var entity = new SocketSelfUser(discord, discord.GetOrCreateSelfUser(state, model)); | |||||
| entity.Update(state, model); | |||||
| var entity = new SocketSelfUser(discord, model.Id); | |||||
| entity.Update(model); | |||||
| return entity; | return entity; | ||||
| } | } | ||||
| internal override bool Update(ClientState state, Model model) | |||||
| internal override bool Update(UserModel model) | |||||
| { | { | ||||
| bool hasGlobalChanges = base.Update(state, model); | |||||
| if (model.Email.IsSpecified) | |||||
| bool hasGlobalChanges = base.Update(model); | |||||
| if (model is not Model currentUserModel) | |||||
| throw new ArgumentException($"Got unexpected model type \"{model?.GetType()}\""); | |||||
| if(currentUserModel.Email != Email) | |||||
| { | { | ||||
| Email = model.Email.Value; | |||||
| Email = currentUserModel.Email; | |||||
| hasGlobalChanges = true; | hasGlobalChanges = true; | ||||
| } | } | ||||
| if (model.Verified.IsSpecified) | |||||
| if (currentUserModel.IsVerified.HasValue) | |||||
| { | { | ||||
| IsVerified = model.Verified.Value; | |||||
| IsVerified = currentUserModel.IsVerified.Value; | |||||
| hasGlobalChanges = true; | hasGlobalChanges = true; | ||||
| } | } | ||||
| if (model.MfaEnabled.IsSpecified) | |||||
| if (currentUserModel.IsMfaEnabled.HasValue) | |||||
| { | { | ||||
| IsMfaEnabled = model.MfaEnabled.Value; | |||||
| IsMfaEnabled = currentUserModel.IsMfaEnabled.Value; | |||||
| hasGlobalChanges = true; | hasGlobalChanges = true; | ||||
| } | } | ||||
| if (model.Flags.IsSpecified && model.Flags.Value != Flags) | |||||
| if (currentUserModel.Flags != Flags) | |||||
| { | { | ||||
| Flags = (UserProperties)model.Flags.Value; | |||||
| Flags = currentUserModel.Flags; | |||||
| hasGlobalChanges = true; | hasGlobalChanges = true; | ||||
| } | } | ||||
| if (model.PremiumType.IsSpecified && model.PremiumType.Value != PremiumType) | |||||
| if (currentUserModel.PremiumType != PremiumType) | |||||
| { | { | ||||
| PremiumType = model.PremiumType.Value; | |||||
| PremiumType = currentUserModel.PremiumType; | |||||
| hasGlobalChanges = true; | hasGlobalChanges = true; | ||||
| } | } | ||||
| if (model.Locale.IsSpecified && model.Locale.Value != Locale) | |||||
| if (currentUserModel.Locale != Locale) | |||||
| { | { | ||||
| Locale = model.Locale.Value; | |||||
| Locale = currentUserModel.Locale; | |||||
| hasGlobalChanges = true; | hasGlobalChanges = true; | ||||
| } | } | ||||
| return hasGlobalChanges; | return hasGlobalChanges; | ||||
| @@ -93,5 +86,63 @@ namespace Discord.WebSocket | |||||
| private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Self)"; | private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Self)"; | ||||
| internal new SocketSelfUser Clone() => MemberwiseClone() as SocketSelfUser; | internal new SocketSelfUser Clone() => MemberwiseClone() as SocketSelfUser; | ||||
| public override void Dispose() | |||||
| { | |||||
| if (IsFreed) | |||||
| return; | |||||
| GC.SuppressFinalize(this); | |||||
| Discord.StateManager.UserStore.RemoveReference(Id); | |||||
| IsFreed = true; | |||||
| } | |||||
| #region Cache | |||||
| internal new class CacheModel : Model | |||||
| { | |||||
| public bool? IsVerified { get; set; } | |||||
| public string Email { get; set; } | |||||
| public bool? IsMfaEnabled { get; set; } | |||||
| public UserProperties Flags { get; set; } | |||||
| public PremiumType PremiumType { get; set; } | |||||
| public string Locale { get; set; } | |||||
| public UserProperties PublicFlags { get; set; } | |||||
| public string Username { get; set; } | |||||
| public string Discriminator { get; set; } | |||||
| public bool? IsBot { get; set; } | |||||
| public string Avatar { get; set; } | |||||
| public ulong Id { get; set; } | |||||
| } | |||||
| internal new Model ToModel() | |||||
| { | |||||
| var model = Discord.StateManager.GetModel<Model, CacheModel>(); | |||||
| model.Avatar = AvatarId; | |||||
| model.Discriminator = Discriminator; | |||||
| model.Email = Email; | |||||
| model.Flags = Flags; | |||||
| model.IsBot = IsBot; | |||||
| model.IsMfaEnabled = IsMfaEnabled; | |||||
| model.Locale = Locale; | |||||
| model.PremiumType = PremiumType; | |||||
| model.PublicFlags = PublicFlags ?? UserProperties.None; | |||||
| model.Username = Username; | |||||
| model.Id = Id; | |||||
| return model; | |||||
| } | |||||
| Model ICached<Model>.ToModel() => ToModel(); | |||||
| void ICached<Model>.Update(Model model) => Update(model); | |||||
| #endregion | |||||
| } | } | ||||
| } | } | ||||
| @@ -2,7 +2,7 @@ using System; | |||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Linq; | using System.Linq; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Model = Discord.API.ThreadMember; | |||||
| using Model = Discord.IThreadMemberModel; | |||||
| using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
| namespace Discord.WebSocket | namespace Discord.WebSocket | ||||
| @@ -10,12 +10,12 @@ namespace Discord.WebSocket | |||||
| /// <summary> | /// <summary> | ||||
| /// Represents a thread user received over the gateway. | /// Represents a thread user received over the gateway. | ||||
| /// </summary> | /// </summary> | ||||
| public class SocketThreadUser : SocketUser, IThreadUser, IGuildUser | |||||
| public class SocketThreadUser : SocketUser, IThreadUser, IGuildUser, ICached<Model> | |||||
| { | { | ||||
| /// <summary> | /// <summary> | ||||
| /// Gets the <see cref="SocketThreadChannel"/> this user is in. | /// Gets the <see cref="SocketThreadChannel"/> this user is in. | ||||
| /// </summary> | /// </summary> | ||||
| public SocketThreadChannel Thread { get; private set; } | |||||
| public Lazy<SocketThreadChannel> Thread { get; private set; } | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public DateTimeOffset ThreadJoinedAt { get; private set; } | public DateTimeOffset ThreadJoinedAt { get; private set; } | ||||
| @@ -23,126 +23,142 @@ namespace Discord.WebSocket | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the guild this user is in. | /// Gets the guild this user is in. | ||||
| /// </summary> | /// </summary> | ||||
| public SocketGuild Guild { get; private set; } | |||||
| public Lazy<SocketGuild> Guild { get; private set; } | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public DateTimeOffset? JoinedAt | public DateTimeOffset? JoinedAt | ||||
| => GuildUser.JoinedAt; | |||||
| => GuildUser.Value.JoinedAt; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public string DisplayName | public string DisplayName | ||||
| => GuildUser.Nickname ?? GuildUser.Username; | |||||
| => GuildUser.Value.Nickname ?? GuildUser.Value.Username; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public string Nickname | public string Nickname | ||||
| => GuildUser.Nickname; | |||||
| => GuildUser.Value.Nickname; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public DateTimeOffset? PremiumSince | public DateTimeOffset? PremiumSince | ||||
| => GuildUser.PremiumSince; | |||||
| => GuildUser.Value.PremiumSince; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public DateTimeOffset? TimedOutUntil | public DateTimeOffset? TimedOutUntil | ||||
| => GuildUser.TimedOutUntil; | |||||
| => GuildUser.Value.TimedOutUntil; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public bool? IsPending | public bool? IsPending | ||||
| => GuildUser.IsPending; | |||||
| => GuildUser.Value.IsPending; | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public int Hierarchy | public int Hierarchy | ||||
| => GuildUser.Hierarchy; | |||||
| => GuildUser.Value.Hierarchy; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public override string AvatarId | public override string AvatarId | ||||
| { | { | ||||
| get => GuildUser.AvatarId; | |||||
| internal set => GuildUser.AvatarId = value; | |||||
| get => GuildUser.Value.AvatarId; | |||||
| internal set => GuildUser.Value.AvatarId = value; | |||||
| } | } | ||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public string DisplayAvatarId => GuildAvatarId ?? AvatarId; | public string DisplayAvatarId => GuildAvatarId ?? AvatarId; | ||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public string GuildAvatarId | public string GuildAvatarId | ||||
| => GuildUser.GuildAvatarId; | |||||
| => GuildUser.Value.GuildAvatarId; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public override ushort DiscriminatorValue | public override ushort DiscriminatorValue | ||||
| { | { | ||||
| get => GuildUser.DiscriminatorValue; | |||||
| internal set => GuildUser.DiscriminatorValue = value; | |||||
| get => GuildUser.Value.DiscriminatorValue; | |||||
| internal set => GuildUser.Value.DiscriminatorValue = value; | |||||
| } | } | ||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public override bool IsBot | public override bool IsBot | ||||
| { | { | ||||
| get => GuildUser.IsBot; | |||||
| internal set => GuildUser.IsBot = value; | |||||
| get => GuildUser.Value.IsBot; | |||||
| internal set => GuildUser.Value.IsBot = value; | |||||
| } | } | ||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public override bool IsWebhook | public override bool IsWebhook | ||||
| => GuildUser.IsWebhook; | |||||
| => GuildUser.Value.IsWebhook; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public override string Username | public override string Username | ||||
| { | { | ||||
| get => GuildUser.Username; | |||||
| internal set => GuildUser.Username = value; | |||||
| get => GuildUser.Value.Username; | |||||
| internal set => GuildUser.Value.Username = value; | |||||
| } | } | ||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public bool IsDeafened | public bool IsDeafened | ||||
| => GuildUser.IsDeafened; | |||||
| => GuildUser.Value.IsDeafened; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public bool IsMuted | public bool IsMuted | ||||
| => GuildUser.IsMuted; | |||||
| => GuildUser.Value.IsMuted; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public bool IsSelfDeafened | public bool IsSelfDeafened | ||||
| => GuildUser.IsSelfDeafened; | |||||
| => GuildUser.Value.IsSelfDeafened; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public bool IsSelfMuted | public bool IsSelfMuted | ||||
| => GuildUser.IsSelfMuted; | |||||
| => GuildUser.Value.IsSelfMuted; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public bool IsSuppressed | public bool IsSuppressed | ||||
| => GuildUser.IsSuppressed; | |||||
| => GuildUser.Value.IsSuppressed; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public IVoiceChannel VoiceChannel | public IVoiceChannel VoiceChannel | ||||
| => GuildUser.VoiceChannel; | |||||
| => GuildUser.Value.VoiceChannel; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public string VoiceSessionId | public string VoiceSessionId | ||||
| => GuildUser.VoiceSessionId; | |||||
| => GuildUser.Value.VoiceSessionId; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public bool IsStreaming | public bool IsStreaming | ||||
| => GuildUser.IsStreaming; | |||||
| => GuildUser.Value.IsStreaming; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public bool IsVideoing | public bool IsVideoing | ||||
| => GuildUser.IsVideoing; | |||||
| => GuildUser.Value.IsVideoing; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public DateTimeOffset? RequestToSpeakTimestamp | public DateTimeOffset? RequestToSpeakTimestamp | ||||
| => GuildUser.RequestToSpeakTimestamp; | |||||
| => GuildUser.Value.RequestToSpeakTimestamp; | |||||
| private Lazy<SocketGuildUser> GuildUser { get; set; } | |||||
| private SocketGuildUser GuildUser { get; set; } | |||||
| private ulong _threadId; | |||||
| private ulong _guildId; | |||||
| internal SocketThreadUser(SocketGuild guild, SocketThreadChannel thread, SocketGuildUser member, ulong userId) | |||||
| : base(guild.Discord, userId) | |||||
| internal SocketThreadUser(DiscordSocketClient client, ulong guildId, ulong threadId, ulong userId) | |||||
| : base(client, userId) | |||||
| { | { | ||||
| Thread = thread; | |||||
| Guild = guild; | |||||
| GuildUser = member; | |||||
| _guildId = guildId; | |||||
| _threadId = threadId; | |||||
| GuildUser = new(() => client.StateManager.TryGetMemberStore(guildId, out var store) ? store.Get(userId) : null); | |||||
| Thread = new(() => client.GetChannel(threadId) as SocketThreadChannel); | |||||
| Guild = new(() => client.GetGuild(guildId)); | |||||
| } | } | ||||
| internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, Model model, SocketGuildUser member) | internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, Model model, SocketGuildUser member) | ||||
| { | { | ||||
| var entity = new SocketThreadUser(guild, thread, member, model.UserId.Value); | |||||
| var entity = new SocketThreadUser(guild.Discord, guild.Id, thread.Id, model.Id); | |||||
| entity.Update(model); | |||||
| return entity; | |||||
| } | |||||
| internal static SocketThreadUser Create(DiscordSocketClient client, ulong guildId, ulong threadId, Model model) | |||||
| { | |||||
| var entity = new SocketThreadUser(client, guildId, threadId, model.Id); | |||||
| entity.Update(model); | entity.Update(model); | ||||
| return entity; | return entity; | ||||
| } | } | ||||
| @@ -150,89 +166,116 @@ namespace Discord.WebSocket | |||||
| internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, SocketGuildUser owner) | internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, SocketGuildUser owner) | ||||
| { | { | ||||
| // this is used for creating the owner of the thread. | // this is used for creating the owner of the thread. | ||||
| var entity = new SocketThreadUser(guild, thread, owner, owner.Id); | |||||
| entity.Update(new Model | |||||
| { | |||||
| JoinTimestamp = thread.CreatedAt, | |||||
| }); | |||||
| var entity = new SocketThreadUser(guild.Discord, guild.Id, thread.Id, owner.Id); | |||||
| entity.ThreadJoinedAt = thread.CreatedAt; | |||||
| return entity; | return entity; | ||||
| } | } | ||||
| internal void Update(Model model) | internal void Update(Model model) | ||||
| { | { | ||||
| ThreadJoinedAt = model.JoinTimestamp; | |||||
| ThreadJoinedAt = model.JoinedAt; | |||||
| } | } | ||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public ChannelPermissions GetPermissions(IGuildChannel channel) => GuildUser.GetPermissions(channel); | |||||
| public ChannelPermissions GetPermissions(IGuildChannel channel) => GuildUser.Value.GetPermissions(channel); | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public Task KickAsync(string reason = null, RequestOptions options = null) => GuildUser.KickAsync(reason, options); | |||||
| public Task KickAsync(string reason = null, RequestOptions options = null) => GuildUser.Value.KickAsync(reason, options); | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public Task ModifyAsync(Action<GuildUserProperties> func, RequestOptions options = null) => GuildUser.ModifyAsync(func, options); | |||||
| public Task ModifyAsync(Action<GuildUserProperties> func, RequestOptions options = null) => GuildUser.Value.ModifyAsync(func, options); | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public Task AddRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.AddRoleAsync(roleId, options); | |||||
| public Task AddRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.Value.AddRoleAsync(roleId, options); | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public Task AddRoleAsync(IRole role, RequestOptions options = null) => GuildUser.AddRoleAsync(role, options); | |||||
| public Task AddRoleAsync(IRole role, RequestOptions options = null) => GuildUser.Value.AddRoleAsync(role, options); | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public Task AddRolesAsync(IEnumerable<ulong> roleIds, RequestOptions options = null) => GuildUser.AddRolesAsync(roleIds, options); | |||||
| public Task AddRolesAsync(IEnumerable<ulong> roleIds, RequestOptions options = null) => GuildUser.Value.AddRolesAsync(roleIds, options); | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public Task AddRolesAsync(IEnumerable<IRole> roles, RequestOptions options = null) => GuildUser.AddRolesAsync(roles, options); | |||||
| public Task AddRolesAsync(IEnumerable<IRole> roles, RequestOptions options = null) => GuildUser.Value.AddRolesAsync(roles, options); | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public Task RemoveRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.RemoveRoleAsync(roleId, options); | |||||
| public Task RemoveRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.Value.RemoveRoleAsync(roleId, options); | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public Task RemoveRoleAsync(IRole role, RequestOptions options = null) => GuildUser.RemoveRoleAsync(role, options); | |||||
| public Task RemoveRoleAsync(IRole role, RequestOptions options = null) => GuildUser.Value.RemoveRoleAsync(role, options); | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public Task RemoveRolesAsync(IEnumerable<ulong> roleIds, RequestOptions options = null) => GuildUser.RemoveRolesAsync(roleIds, options); | |||||
| public Task RemoveRolesAsync(IEnumerable<ulong> roleIds, RequestOptions options = null) => GuildUser.Value.RemoveRolesAsync(roleIds, options); | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public Task RemoveRolesAsync(IEnumerable<IRole> roles, RequestOptions options = null) => GuildUser.RemoveRolesAsync(roles, options); | |||||
| public Task RemoveRolesAsync(IEnumerable<IRole> roles, RequestOptions options = null) => GuildUser.Value.RemoveRolesAsync(roles, options); | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public Task SetTimeOutAsync(TimeSpan span, RequestOptions options = null) => GuildUser.SetTimeOutAsync(span, options); | |||||
| public Task SetTimeOutAsync(TimeSpan span, RequestOptions options = null) => GuildUser.Value.SetTimeOutAsync(span, options); | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public Task RemoveTimeOutAsync(RequestOptions options = null) => GuildUser.RemoveTimeOutAsync(options); | |||||
| public Task RemoveTimeOutAsync(RequestOptions options = null) => GuildUser.Value.RemoveTimeOutAsync(options); | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| IThreadChannel IThreadUser.Thread => Thread; | |||||
| IThreadChannel IThreadUser.Thread => Thread.Value; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| IGuild IThreadUser.Guild => Guild; | |||||
| IGuild IThreadUser.Guild => Guild.Value; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| IGuild IGuildUser.Guild => Guild; | |||||
| IGuild IGuildUser.Guild => Guild.Value; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| ulong IGuildUser.GuildId => Guild.Id; | |||||
| ulong IGuildUser.GuildId => Guild.Value.Id; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| GuildPermissions IGuildUser.GuildPermissions => GuildUser.GuildPermissions; | |||||
| GuildPermissions IGuildUser.GuildPermissions => GuildUser.Value.GuildPermissions; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| IReadOnlyCollection<ulong> IGuildUser.RoleIds => GuildUser.Roles.Select(x => x.Id).ToImmutableArray(); | |||||
| IReadOnlyCollection<ulong> IGuildUser.RoleIds => GuildUser.Value.Roles.Select(x => x.Id).ToImmutableArray(); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| string IGuildUser.GetDisplayAvatarUrl(ImageFormat format, ushort size) => GuildUser.GetDisplayAvatarUrl(format, size); | |||||
| string IGuildUser.GetDisplayAvatarUrl(ImageFormat format, ushort size) => GuildUser.Value.GetDisplayAvatarUrl(format, size); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| string IGuildUser.GetGuildAvatarUrl(ImageFormat format, ushort size) => GuildUser.GetGuildAvatarUrl(format, size); | |||||
| string IGuildUser.GetGuildAvatarUrl(ImageFormat format, ushort size) => GuildUser.Value.GetGuildAvatarUrl(format, size); | |||||
| internal override SocketGlobalUser GlobalUser { get => GuildUser.GlobalUser; set => GuildUser.GlobalUser = value; } | |||||
| internal override LazyCached<SocketPresence> Presence { get => GuildUser.Value.Presence; set => GuildUser.Value.Presence = value; } | |||||
| public override void Dispose() | |||||
| { | |||||
| if (IsFreed) | |||||
| return; | |||||
| GC.SuppressFinalize(this); | |||||
| Discord.StateManager.GetThreadMemberStore(_threadId)?.RemoveReference(Id); | |||||
| IsFreed = true; | |||||
| } | |||||
| internal override SocketPresence Presence { get => GuildUser.Presence; set => GuildUser.Presence = value; } | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the guild user of this thread user. | /// Gets the guild user of this thread user. | ||||
| /// </summary> | /// </summary> | ||||
| /// <param name="user"></param> | /// <param name="user"></param> | ||||
| public static explicit operator SocketGuildUser(SocketThreadUser user) => user.GuildUser; | |||||
| public static explicit operator SocketGuildUser(SocketThreadUser user) => user.GuildUser.Value; | |||||
| #region Cache | |||||
| internal new class CacheModel : Model | |||||
| { | |||||
| public ulong Id { get; set; } | |||||
| public ulong? ThreadId { get; set; } | |||||
| public DateTimeOffset JoinedAt { get; set; } | |||||
| } | |||||
| internal new Model ToModel() | |||||
| { | |||||
| var model = Discord.StateManager.GetModel<Model, CacheModel>(); | |||||
| model.Id = Id; | |||||
| model.ThreadId = _threadId; | |||||
| model.JoinedAt = ThreadJoinedAt; | |||||
| return model; | |||||
| } | |||||
| Model ICached<Model>.ToModel() => ToModel(); | |||||
| void ICached<Model>.Update(Model model) => Update(model); | |||||
| #endregion | |||||
| } | } | ||||
| } | } | ||||
| @@ -26,22 +26,22 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override bool IsWebhook => false; | public override bool IsWebhook => false; | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null, null); } set { } } | |||||
| /// <inheritdoc /> | |||||
| /// <exception cref="NotSupportedException">This field is not supported for an unknown user.</exception> | |||||
| internal override SocketGlobalUser GlobalUser { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } | |||||
| internal override LazyCached<SocketPresence> Presence { get { return new(SocketPresence.Default); } set { } } | |||||
| internal override LazyCached<SocketGlobalUser> GlobalUser { get => new(null); set { } } | |||||
| internal SocketUnknownUser(DiscordSocketClient discord, ulong id) | internal SocketUnknownUser(DiscordSocketClient discord, ulong id) | ||||
| : base(discord, id) | : base(discord, id) | ||||
| { | { | ||||
| } | } | ||||
| internal static SocketUnknownUser Create(DiscordSocketClient discord, ClientState state, Model model) | |||||
| internal static SocketUnknownUser Create(DiscordSocketClient discord, Model model) | |||||
| { | { | ||||
| var entity = new SocketUnknownUser(discord, model.Id); | var entity = new SocketUnknownUser(discord, model.Id); | ||||
| entity.Update(state, model); | |||||
| entity.Update(model); | |||||
| return entity; | return entity; | ||||
| } | } | ||||
| public override void Dispose() { } | |||||
| private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Unknown)"; | private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Unknown)"; | ||||
| internal new SocketUnknownUser Clone() => MemberwiseClone() as SocketUnknownUser; | internal new SocketUnknownUser Clone() => MemberwiseClone() as SocketUnknownUser; | ||||
| } | } | ||||
| @@ -6,8 +6,8 @@ using System.Globalization; | |||||
| using System.Linq; | using System.Linq; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Discord.Rest; | using Discord.Rest; | ||||
| using Model = Discord.API.User; | |||||
| using PresenceModel = Discord.API.Presence; | |||||
| using Model = Discord.IUserModel; | |||||
| using PresenceModel = Discord.IPresenceModel; | |||||
| namespace Discord.WebSocket | namespace Discord.WebSocket | ||||
| { | { | ||||
| @@ -15,23 +15,23 @@ namespace Discord.WebSocket | |||||
| /// Represents a WebSocket-based user. | /// Represents a WebSocket-based user. | ||||
| /// </summary> | /// </summary> | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| public abstract class SocketUser : SocketEntity<ulong>, IUser | |||||
| public abstract class SocketUser : SocketEntity<ulong>, IUser, ICached<Model> | |||||
| { | { | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public abstract bool IsBot { get; internal set; } | |||||
| public virtual bool IsBot { get; internal set; } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public abstract string Username { get; internal set; } | |||||
| public virtual string Username { get; internal set; } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public abstract ushort DiscriminatorValue { get; internal set; } | |||||
| public virtual ushort DiscriminatorValue { get; internal set; } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public abstract string AvatarId { get; internal set; } | |||||
| public virtual string AvatarId { get; internal set; } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public abstract bool IsWebhook { get; } | |||||
| public virtual bool IsWebhook { get; } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public UserProperties? PublicFlags { get; private set; } | public UserProperties? PublicFlags { get; private set; } | ||||
| internal abstract SocketGlobalUser GlobalUser { get; set; } | |||||
| internal abstract SocketPresence Presence { get; set; } | |||||
| internal virtual LazyCached<SocketGlobalUser> GlobalUser { get; set; } | |||||
| internal virtual LazyCached<SocketPresence> Presence { get; set; } | |||||
| internal bool IsFreed { get; set; } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -39,11 +39,11 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public string Mention => MentionUtils.MentionUser(Id); | public string Mention => MentionUtils.MentionUser(Id); | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public UserStatus Status => Presence.Status; | |||||
| public UserStatus Status => Presence.Value.Status; | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public IReadOnlyCollection<ClientType> ActiveClients => Presence.ActiveClients ?? ImmutableHashSet<ClientType>.Empty; | |||||
| public IReadOnlyCollection<ClientType> ActiveClients => Presence.Value?.ActiveClients ?? ImmutableHashSet<ClientType>.Empty; | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public IReadOnlyCollection<IActivity> Activities => Presence.Activities ?? ImmutableList<IActivity>.Empty; | |||||
| public IReadOnlyCollection<IActivity> Activities => Presence.Value?.Activities ?? ImmutableList<IActivity>.Empty; | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets mutual guilds shared with this user. | /// Gets mutual guilds shared with this user. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -56,48 +56,50 @@ namespace Discord.WebSocket | |||||
| internal SocketUser(DiscordSocketClient discord, ulong id) | internal SocketUser(DiscordSocketClient discord, ulong id) | ||||
| : base(discord, id) | : base(discord, id) | ||||
| { | { | ||||
| Presence = new LazyCached<SocketPresence>(id, discord.StateManager.PresenceStore); | |||||
| GlobalUser = new LazyCached<SocketGlobalUser>(id, discord.StateManager.UserStore); | |||||
| } | } | ||||
| internal virtual bool Update(ClientState state, Model model) | |||||
| internal virtual bool Update(Model model) | |||||
| { | { | ||||
| Presence ??= new SocketPresence(); | |||||
| bool hasChanges = false; | bool hasChanges = false; | ||||
| if (model.Avatar.IsSpecified && model.Avatar.Value != AvatarId) | |||||
| if (model.Avatar != AvatarId) | |||||
| { | { | ||||
| AvatarId = model.Avatar.Value; | |||||
| AvatarId = model.Avatar; | |||||
| hasChanges = true; | hasChanges = true; | ||||
| } | } | ||||
| if (model.Discriminator.IsSpecified) | |||||
| if (model.Discriminator != null) | |||||
| { | { | ||||
| var newVal = ushort.Parse(model.Discriminator.Value, NumberStyles.None, CultureInfo.InvariantCulture); | |||||
| var newVal = ushort.Parse(model.Discriminator, NumberStyles.None, CultureInfo.InvariantCulture); | |||||
| if (newVal != DiscriminatorValue) | if (newVal != DiscriminatorValue) | ||||
| { | { | ||||
| DiscriminatorValue = ushort.Parse(model.Discriminator.Value, NumberStyles.None, CultureInfo.InvariantCulture); | |||||
| DiscriminatorValue = ushort.Parse(model.Discriminator, NumberStyles.None, CultureInfo.InvariantCulture); | |||||
| hasChanges = true; | hasChanges = true; | ||||
| } | } | ||||
| } | } | ||||
| if (model.Bot.IsSpecified && model.Bot.Value != IsBot) | |||||
| if (model.IsBot.HasValue && model.IsBot.Value != IsBot) | |||||
| { | { | ||||
| IsBot = model.Bot.Value; | |||||
| IsBot = model.IsBot.Value; | |||||
| hasChanges = true; | hasChanges = true; | ||||
| } | } | ||||
| if (model.Username.IsSpecified && model.Username.Value != Username) | |||||
| if (model.Username != Username) | |||||
| { | { | ||||
| Username = model.Username.Value; | |||||
| Username = model.Username; | |||||
| hasChanges = true; | hasChanges = true; | ||||
| } | } | ||||
| if (model.PublicFlags.IsSpecified && model.PublicFlags.Value != PublicFlags) | |||||
| if(model is ICurrentUserModel currentUserModel) | |||||
| { | { | ||||
| PublicFlags = model.PublicFlags.Value; | |||||
| hasChanges = true; | |||||
| if (currentUserModel.PublicFlags != PublicFlags) | |||||
| { | |||||
| PublicFlags = currentUserModel.PublicFlags; | |||||
| hasChanges = true; | |||||
| } | |||||
| } | } | ||||
| return hasChanges; | return hasChanges; | ||||
| } | } | ||||
| internal virtual void Update(PresenceModel model) | |||||
| { | |||||
| Presence ??= new SocketPresence(); | |||||
| Presence.Update(model); | |||||
| } | |||||
| public abstract void Dispose(); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public async Task<IDMChannel> CreateDMChannelAsync(RequestOptions options = null) | public async Task<IDMChannel> CreateDMChannelAsync(RequestOptions options = null) | ||||
| @@ -120,5 +122,37 @@ namespace Discord.WebSocket | |||||
| public override string ToString() => Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode); | public override string ToString() => Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode); | ||||
| private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode)} ({Id}{(IsBot ? ", Bot" : "")})"; | private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode)} ({Id}{(IsBot ? ", Bot" : "")})"; | ||||
| internal SocketUser Clone() => MemberwiseClone() as SocketUser; | internal SocketUser Clone() => MemberwiseClone() as SocketUser; | ||||
| #region Cache | |||||
| internal class CacheModel : Model | |||||
| { | |||||
| public string Username { get; set; } | |||||
| public string Discriminator { get; set; } | |||||
| public bool? IsBot { get; set; } | |||||
| public string Avatar { get; set; } | |||||
| public ulong Id { get; set; } | |||||
| } | |||||
| internal Model ToModel() | |||||
| { | |||||
| var model = Discord.StateManager.GetModel<Model, CacheModel>(); | |||||
| model.Avatar = AvatarId; | |||||
| model.Discriminator = Discriminator; | |||||
| model.Id = Id; | |||||
| model.IsBot = IsBot; | |||||
| model.Username = Username; | |||||
| return model; | |||||
| } | |||||
| Model ICached<Model>.ToModel() | |||||
| => ToModel(); | |||||
| void ICached<Model>.Update(Model model) => Update(model); | |||||
| bool ICached.IsFreed => IsFreed; | |||||
| #endregion | |||||
| } | } | ||||
| } | } | ||||
| @@ -33,8 +33,8 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override bool IsWebhook => true; | public override bool IsWebhook => true; | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null, null); } set { } } | |||||
| internal override SocketGlobalUser GlobalUser { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } | |||||
| internal override LazyCached<SocketPresence> Presence { get { return new(SocketPresence.Default); } set { } } | |||||
| internal override LazyCached<SocketGlobalUser> GlobalUser { get => new(null); set { } } | |||||
| internal SocketWebhookUser(SocketGuild guild, ulong id, ulong webhookId) | internal SocketWebhookUser(SocketGuild guild, ulong id, ulong webhookId) | ||||
| : base(guild.Discord, id) | : base(guild.Discord, id) | ||||
| @@ -42,16 +42,17 @@ namespace Discord.WebSocket | |||||
| Guild = guild; | Guild = guild; | ||||
| WebhookId = webhookId; | WebhookId = webhookId; | ||||
| } | } | ||||
| internal static SocketWebhookUser Create(SocketGuild guild, ClientState state, Model model, ulong webhookId) | |||||
| internal static SocketWebhookUser Create(SocketGuild guild, Model model, ulong webhookId) | |||||
| { | { | ||||
| var entity = new SocketWebhookUser(guild, model.Id, webhookId); | var entity = new SocketWebhookUser(guild, model.Id, webhookId); | ||||
| entity.Update(state, model); | |||||
| entity.Update(model); | |||||
| return entity; | return entity; | ||||
| } | } | ||||
| private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Webhook)"; | private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Webhook)"; | ||||
| internal new SocketWebhookUser Clone() => MemberwiseClone() as SocketWebhookUser; | internal new SocketWebhookUser Clone() => MemberwiseClone() as SocketWebhookUser; | ||||
| #endregion | |||||
| public override void Dispose() { } | |||||
| #endregion | |||||
| #region IGuildUser | #region IGuildUser | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -0,0 +1,70 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Reflection; | |||||
| using System.Text.RegularExpressions; | |||||
| namespace Discord.WebSocket | |||||
| { | |||||
| internal static class CacheModelExtensions | |||||
| { | |||||
| public static TDest InterfaceCopy<TDest>(this object source) | |||||
| where TDest : class, new() | |||||
| => source.InterfaceCopy(new TDest()); | |||||
| public static TDest InterfaceCopy<TSource, TDest>(this TSource source, TDest dest) | |||||
| where TSource : class | |||||
| where TDest : class | |||||
| { | |||||
| if (source == null || dest == null) | |||||
| throw new ArgumentNullException(source == null ? nameof(source) : nameof(dest)); | |||||
| if (source == null || dest == null) | |||||
| throw new ArgumentNullException(source == null ? nameof(source) : nameof(dest)); | |||||
| // get the shared model interface | |||||
| var sourceType = source.GetType(); | |||||
| var destType = dest.GetType(); | |||||
| if (sourceType == destType) | |||||
| return source as TDest; | |||||
| List<Type> sharedInterfaceModels = new(); | |||||
| foreach (var intf in sourceType.GetInterfaces()) | |||||
| { | |||||
| if (destType.GetInterface(intf.Name) != null && intf.Name.Contains("Model")) | |||||
| sharedInterfaceModels.Add(intf); | |||||
| } | |||||
| if (sharedInterfaceModels.Count == 0) | |||||
| throw new NotSupportedException($"cannot find common shared model interface between {sourceType.Name} and {destType.Name}"); | |||||
| foreach (var interfaceType in sharedInterfaceModels) | |||||
| { | |||||
| var intfName = interfaceType.GenericTypeArguments.Length == 0 ? interfaceType.FullName : | |||||
| $"{interfaceType.Namespace}.{Regex.Replace(interfaceType.Name, @"`\d+?$", "")}<{string.Join(", ", interfaceType.GenericTypeArguments.Select(x => x.FullName))}>"; | |||||
| foreach (var prop in interfaceType.GetProperties()) | |||||
| { | |||||
| var sProp = sourceType.GetProperty($"{intfName}.{prop.Name}", BindingFlags.NonPublic | BindingFlags.Instance) ?? sourceType.GetProperty(prop.Name); | |||||
| var dProp = destType.GetProperty($"{intfName}.{prop.Name}", BindingFlags.NonPublic | BindingFlags.Instance) ?? destType.GetProperty(prop.Name); | |||||
| if (sProp == null || dProp == null) | |||||
| throw new NotSupportedException($"Couldn't find common interface property {prop.Name}"); | |||||
| dProp.SetValue(dest, sProp.GetValue(source)); | |||||
| } | |||||
| } | |||||
| return dest; | |||||
| } | |||||
| public static TDest ToSpecifiedModel<TId, TDest>(this IEntityModel<TId> source, TDest dest) | |||||
| where TId : IEquatable<TId> | |||||
| where TDest : class, IEntityModel<TId> | |||||
| { | |||||
| return source.InterfaceCopy(dest); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,16 @@ | |||||
| using Discord.WebSocket; | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| public static class EntityCacheExtensions | |||||
| { | |||||
| public static ValueTask<IUser> GetUserAsync(this MessageInteraction<SocketUser> interaction, DiscordSocketClient client, | |||||
| CacheMode mode, RequestOptions options = null) | |||||
| => client.StateManager.UserStore.GetAsync(interaction.UserId, mode, options); | |||||
| } | |||||
| } | |||||
| @@ -7,86 +7,108 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| internal static class EntityExtensions | internal static class EntityExtensions | ||||
| { | { | ||||
| public static IActivity ToEntity(this API.Game model) | |||||
| #region Emotes | |||||
| public static IEmote ToEntity(this IEmojiModel model) | |||||
| { | |||||
| if (model.Id.HasValue) | |||||
| return new Emote(model.Id.Value, model.Name, model.IsAnimated); | |||||
| else | |||||
| return new Emoji(model.Name); | |||||
| } | |||||
| #endregion | |||||
| #region Activity | |||||
| public static IActivity ToEntity(this IActivityModel model) | |||||
| { | { | ||||
| #region Custom Status Game | #region Custom Status Game | ||||
| if (model.Id.IsSpecified && model.Id.Value == "custom") | |||||
| if (model.Id != null && model.Id == "custom") | |||||
| { | { | ||||
| return new CustomStatusGame() | return new CustomStatusGame() | ||||
| { | { | ||||
| Type = ActivityType.CustomStatus, | Type = ActivityType.CustomStatus, | ||||
| Name = model.Name, | Name = model.Name, | ||||
| State = model.State.IsSpecified ? model.State.Value : null, | |||||
| Emote = model.Emoji.IsSpecified ? model.Emoji.Value.ToIEmote() : null, | |||||
| CreatedAt = DateTimeOffset.FromUnixTimeMilliseconds(model.CreatedAt.Value), | |||||
| State = model.State, | |||||
| Emote = model.Emoji?.ToIEmote(), | |||||
| CreatedAt = model.CreatedAt, | |||||
| }; | }; | ||||
| } | } | ||||
| #endregion | #endregion | ||||
| #region Spotify Game | #region Spotify Game | ||||
| if (model.SyncId.IsSpecified) | |||||
| if (model.SyncId != null) | |||||
| { | { | ||||
| var assets = model.Assets.GetValueOrDefault()?.ToEntity(); | |||||
| string albumText = assets?[1]?.Text; | |||||
| string albumArtId = assets?[1]?.ImageId?.Replace("spotify:", ""); | |||||
| var timestamps = model.Timestamps.IsSpecified ? model.Timestamps.Value.ToEntity() : null; | |||||
| string albumText = model.LargeText; | |||||
| string albumArtId = model.LargeImage?.Replace("spotify:", ""); | |||||
| return new SpotifyGame | return new SpotifyGame | ||||
| { | { | ||||
| Name = model.Name, | Name = model.Name, | ||||
| SessionId = model.SessionId.GetValueOrDefault(), | |||||
| TrackId = model.SyncId.Value, | |||||
| TrackUrl = CDN.GetSpotifyDirectUrl(model.SyncId.Value), | |||||
| SessionId = model.SessionId, | |||||
| TrackId = model.SyncId, | |||||
| TrackUrl = CDN.GetSpotifyDirectUrl(model.SyncId), | |||||
| AlbumTitle = albumText, | AlbumTitle = albumText, | ||||
| TrackTitle = model.Details.GetValueOrDefault(), | |||||
| Artists = model.State.GetValueOrDefault()?.Split(';').Select(x => x?.Trim()).ToImmutableArray(), | |||||
| StartedAt = timestamps?.Start, | |||||
| EndsAt = timestamps?.End, | |||||
| Duration = timestamps?.End - timestamps?.Start, | |||||
| TrackTitle = model.Details, | |||||
| Artists = model.State?.Split(';').Select(x => x?.Trim()).ToImmutableArray(), | |||||
| StartedAt = model.TimestampStart, | |||||
| EndsAt = model.TimestampEnd, | |||||
| Duration = model.TimestampEnd - model.TimestampStart, | |||||
| AlbumArtUrl = albumArtId != null ? CDN.GetSpotifyAlbumArtUrl(albumArtId) : null, | AlbumArtUrl = albumArtId != null ? CDN.GetSpotifyAlbumArtUrl(albumArtId) : null, | ||||
| Type = ActivityType.Listening, | Type = ActivityType.Listening, | ||||
| Flags = model.Flags.GetValueOrDefault(), | |||||
| Flags = model.Flags, | |||||
| AlbumArt = model.LargeImage, | |||||
| }; | }; | ||||
| } | } | ||||
| #endregion | #endregion | ||||
| #region Rich Game | #region Rich Game | ||||
| if (model.ApplicationId.IsSpecified) | |||||
| if (model.ApplicationId.HasValue) | |||||
| { | { | ||||
| ulong appId = model.ApplicationId.Value; | ulong appId = model.ApplicationId.Value; | ||||
| var assets = model.Assets.GetValueOrDefault()?.ToEntity(appId); | |||||
| return new RichGame | return new RichGame | ||||
| { | { | ||||
| ApplicationId = appId, | ApplicationId = appId, | ||||
| Name = model.Name, | Name = model.Name, | ||||
| Details = model.Details.GetValueOrDefault(), | |||||
| State = model.State.GetValueOrDefault(), | |||||
| SmallAsset = assets?[0], | |||||
| LargeAsset = assets?[1], | |||||
| Party = model.Party.IsSpecified ? model.Party.Value.ToEntity() : null, | |||||
| Secrets = model.Secrets.IsSpecified ? model.Secrets.Value.ToEntity() : null, | |||||
| Timestamps = model.Timestamps.IsSpecified ? model.Timestamps.Value.ToEntity() : null, | |||||
| Flags = model.Flags.GetValueOrDefault() | |||||
| Details = model.Details, | |||||
| State = model.State, | |||||
| SmallAsset = new GameAsset | |||||
| { | |||||
| Text = model.SmallText, | |||||
| ImageId = model.SmallImage, | |||||
| ApplicationId = appId, | |||||
| }, | |||||
| LargeAsset = new GameAsset | |||||
| { | |||||
| Text = model.LargeText, | |||||
| ApplicationId = appId, | |||||
| ImageId = model.LargeImage | |||||
| }, | |||||
| Party = model.PartyId != null ? new GameParty | |||||
| { | |||||
| Id = model.PartyId, | |||||
| Capacity = model.PartySize?.Length > 1 ? model.PartySize[1] : 0, | |||||
| Members = model.PartySize?.Length > 0 ? model.PartySize[0] : 0 | |||||
| } : null, | |||||
| Secrets = model.JoinSecret != null || model.SpectateSecret != null || model.MatchSecret != null ? new GameSecrets(model.MatchSecret, model.JoinSecret, model.SpectateSecret) : null, | |||||
| Timestamps = model.TimestampStart.HasValue || model.TimestampEnd.HasValue ? new GameTimestamps(model.TimestampStart, model.TimestampEnd) : null, | |||||
| Flags = model.Flags | |||||
| }; | }; | ||||
| } | } | ||||
| #endregion | #endregion | ||||
| #region Stream Game | #region Stream Game | ||||
| if (model.StreamUrl.IsSpecified) | |||||
| if (model.Url != null) | |||||
| { | { | ||||
| return new StreamingGame( | return new StreamingGame( | ||||
| model.Name, | model.Name, | ||||
| model.StreamUrl.Value) | |||||
| model.Url) | |||||
| { | { | ||||
| Flags = model.Flags.GetValueOrDefault(), | |||||
| Details = model.Details.GetValueOrDefault() | |||||
| Flags = model.Flags, | |||||
| Details = model.Details | |||||
| }; | }; | ||||
| } | } | ||||
| #endregion | #endregion | ||||
| #region Normal Game | #region Normal Game | ||||
| return new Game(model.Name, model.Type.GetValueOrDefault() ?? ActivityType.Playing, | |||||
| model.Flags.IsSpecified ? model.Flags.Value : ActivityProperties.None, | |||||
| model.Details.GetValueOrDefault()); | |||||
| return new Game(model.Name, model.Type, model.Flags, model.Details); | |||||
| #endregion | #endregion | ||||
| } | } | ||||
| @@ -136,5 +158,6 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| return new GameTimestamps(model.Start.ToNullable(), model.End.ToNullable()); | return new GameTimestamps(model.Start.ToNullable(), model.End.ToNullable()); | ||||
| } | } | ||||
| #endregion | |||||
| } | } | ||||
| } | } | ||||