Compare commits

...

12 Commits

Author SHA1 Message Date
  Quin Lynch 67c5094462 more work on cache provider 3 years ago
  Quin Lynch e14e54061e Merge branch 'dev' into v4/state-cache-providers 3 years ago
  Quin Lynch dc2dafa3ac custom model factory 3 years ago
  Quin Lynch 157063c7ed default model maps 3 years ago
  Quin Lynch 59c334ac60 refactor models and remove cache run mode 3 years ago
  Quin Lynch 9826cf699f
Update src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs 3 years ago
  Quin Lynch a4e1f54d6e
Update src/Discord.Net.WebSocket/DiscordSocketConfig.cs 3 years ago
  Quin Lynch d89d13d703 Final POC for users 3 years ago
  Quin Lynch 17306d5139 updates 3 years ago
  Quin Lynch adcb58e473 refactor default state provider 3 years ago
  Quin Lynch 627f88795b Change up model flows 3 years ago
  Quin Lynch 3e36fbb854 initial implementation 3 years ago
100 changed files with 3299 additions and 978 deletions
Split View
  1. +116
    -0
      src/Discord.Net.Core/Cache/CacheableEntityExtensions.cs
  2. +20
    -0
      src/Discord.Net.Core/Cache/ICached.cs
  3. +16
    -0
      src/Discord.Net.Core/Cache/Models/Application/IPartialApplicationModel.cs
  4. +21
    -0
      src/Discord.Net.Core/Cache/Models/Emoji/IEmojiModel.cs
  5. +13
    -0
      src/Discord.Net.Core/Cache/Models/IEntityModel.cs
  6. +35
    -0
      src/Discord.Net.Core/Cache/Models/Message/Components/IMessageComponentModel.cs
  7. +22
    -0
      src/Discord.Net.Core/Cache/Models/Message/Components/IMessageComponentOptionModel.cs
  8. +21
    -0
      src/Discord.Net.Core/Cache/Models/Message/IAttachmentModel.cs
  9. +44
    -0
      src/Discord.Net.Core/Cache/Models/Message/IEmbedModel.cs
  10. +14
    -0
      src/Discord.Net.Core/Cache/Models/Message/IMessageActivityModel.cs
  11. +48
    -0
      src/Discord.Net.Core/Cache/Models/Message/IMessageModel.cs
  12. +14
    -0
      src/Discord.Net.Core/Cache/Models/Message/IReactionModel.cs
  13. +15
    -0
      src/Discord.Net.Core/Cache/Models/Message/IStickerModel.cs
  14. +48
    -0
      src/Discord.Net.Core/Cache/Models/Presense/IActivityModel.cs
  15. +17
    -0
      src/Discord.Net.Core/Cache/Models/Presense/IPresenceModel.cs
  16. +19
    -0
      src/Discord.Net.Core/Cache/Models/Users/ICurrentUserModel.cs
  17. +22
    -0
      src/Discord.Net.Core/Cache/Models/Users/IMemberModel.cs
  18. +14
    -0
      src/Discord.Net.Core/Cache/Models/Users/IThreadMemberModel.cs
  19. +16
    -0
      src/Discord.Net.Core/Cache/Models/Users/IUserModel.cs
  20. +2
    -0
      src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs
  21. +9
    -1
      src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs
  22. +5
    -0
      src/Discord.Net.Core/Entities/Messages/IMessage.cs
  23. +25
    -3
      src/Discord.Net.Core/Entities/Messages/MessageInteraction.cs
  24. +4
    -0
      src/Discord.Net.Core/Utils/Optional.cs
  25. +23
    -1
      src/Discord.Net.Rest/API/Common/ActionRowComponent.cs
  26. +12
    -1
      src/Discord.Net.Rest/API/Common/Attachment.cs
  27. +23
    -1
      src/Discord.Net.Rest/API/Common/ButtonComponent.cs
  28. +70
    -0
      src/Discord.Net.Rest/API/Common/CurrentUser.cs
  29. +11
    -1
      src/Discord.Net.Rest/API/Common/Embed.cs
  30. +1
    -1
      src/Discord.Net.Rest/API/Common/EmbedAuthor.cs
  31. +2
    -2
      src/Discord.Net.Rest/API/Common/EmbedField.cs
  32. +1
    -1
      src/Discord.Net.Rest/API/Common/EmbedFooter.cs
  33. +4
    -1
      src/Discord.Net.Rest/API/Common/EmbedImage.cs
  34. +1
    -1
      src/Discord.Net.Rest/API/Common/EmbedProvider.cs
  35. +4
    -1
      src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs
  36. +6
    -1
      src/Discord.Net.Rest/API/Common/EmbedVideo.cs
  37. +52
    -1
      src/Discord.Net.Rest/API/Common/Emoji.cs
  38. +97
    -2
      src/Discord.Net.Rest/API/Common/Game.cs
  39. +42
    -1
      src/Discord.Net.Rest/API/Common/GuildMember.cs
  40. +34
    -1
      src/Discord.Net.Rest/API/Common/Message.cs
  41. +4
    -1
      src/Discord.Net.Rest/API/Common/MessageActivity.cs
  42. +6
    -1
      src/Discord.Net.Rest/API/Common/MessageApplication.cs
  43. +25
    -1
      src/Discord.Net.Rest/API/Common/Presence.cs
  44. +6
    -2
      src/Discord.Net.Rest/API/Common/Reaction.cs
  45. +24
    -1
      src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs
  46. +9
    -1
      src/Discord.Net.Rest/API/Common/SelectMenuOption.cs
  47. +6
    -1
      src/Discord.Net.Rest/API/Common/StickerItem.cs
  48. +25
    -1
      src/Discord.Net.Rest/API/Common/TextInputComponent.cs
  49. +5
    -4
      src/Discord.Net.Rest/API/Common/ThreadMember.cs
  50. +30
    -16
      src/Discord.Net.Rest/API/Common/User.cs
  51. +20
    -0
      src/Discord.Net.Rest/ClientHelper.cs
  52. +2
    -2
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  53. +5
    -0
      src/Discord.Net.Rest/DiscordRestClient.cs
  54. +6
    -6
      src/Discord.Net.Rest/Entities/Messages/Attachment.cs
  55. +4
    -6
      src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs
  56. +1
    -0
      src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs
  57. +13
    -15
      src/Discord.Net.Rest/Entities/Users/RestSelfUser.cs
  58. +9
    -13
      src/Discord.Net.Rest/Entities/Users/RestUser.cs
  59. +37
    -83
      src/Discord.Net.Rest/Extensions/EntityExtensions.cs
  60. +1
    -1
      src/Discord.Net.WebSocket/API/Gateway/ReadyEvent.cs
  61. +105
    -0
      src/Discord.Net.WebSocket/Cache/DefaultConcurrentCacheProvider.cs
  62. +39
    -0
      src/Discord.Net.WebSocket/Cache/ICacheProvider.cs
  63. +76
    -0
      src/Discord.Net.WebSocket/Cache/LazyCached.cs
  64. +479
    -0
      src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs
  65. +13
    -18
      src/Discord.Net.WebSocket/ClientStateManager.cs
  66. +1
    -1
      src/Discord.Net.WebSocket/DiscordShardedClient.cs
  67. +173
    -179
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  68. +11
    -0
      src/Discord.Net.WebSocket/DiscordSocketConfig.cs
  69. +1
    -1
      src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs
  70. +2
    -2
      src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs
  71. +6
    -6
      src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs
  72. +6
    -8
      src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs
  73. +2
    -2
      src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs
  74. +1
    -1
      src/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs
  75. +1
    -1
      src/Discord.Net.WebSocket/Entities/Channels/SocketStageChannel.cs
  76. +3
    -3
      src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs
  77. +2
    -3
      src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs
  78. +2
    -2
      src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs
  79. +57
    -106
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
  80. +2
    -2
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs
  81. +3
    -3
      src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs
  82. +7
    -7
      src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs
  83. +357
    -58
      src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs
  84. +33
    -22
      src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs
  85. +2
    -2
      src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs
  86. +23
    -51
      src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs
  87. +2
    -2
      src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs
  88. +2
    -2
      src/Discord.Net.WebSocket/Entities/Stickers/SocketUnknownSticker.cs
  89. +9
    -24
      src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs
  90. +14
    -19
      src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs
  91. +104
    -86
      src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs
  92. +140
    -9
      src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs
  93. +85
    -34
      src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs
  94. +111
    -68
      src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs
  95. +6
    -6
      src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs
  96. +67
    -33
      src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs
  97. +6
    -5
      src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs
  98. +70
    -0
      src/Discord.Net.WebSocket/Extensions/CacheModelExtensions.cs
  99. +16
    -0
      src/Discord.Net.WebSocket/Extensions/EntityCacheExtensions.cs
  100. +59
    -36
      src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs

+ 116
- 0
src/Discord.Net.Core/Cache/CacheableEntityExtensions.cs View File

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

+ 20
- 0
src/Discord.Net.Core/Cache/ICached.cs View File

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

+ 16
- 0
src/Discord.Net.Core/Cache/Models/Application/IPartialApplicationModel.cs View File

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

+ 21
- 0
src/Discord.Net.Core/Cache/Models/Emoji/IEmojiModel.cs View File

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

+ 13
- 0
src/Discord.Net.Core/Cache/Models/IEntityModel.cs View File

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

+ 35
- 0
src/Discord.Net.Core/Cache/Models/Message/Components/IMessageComponentModel.cs View File

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

+ 22
- 0
src/Discord.Net.Core/Cache/Models/Message/Components/IMessageComponentOptionModel.cs View File

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

+ 21
- 0
src/Discord.Net.Core/Cache/Models/Message/IAttachmentModel.cs View File

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

+ 44
- 0
src/Discord.Net.Core/Cache/Models/Message/IEmbedModel.cs View File

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

+ 14
- 0
src/Discord.Net.Core/Cache/Models/Message/IMessageActivityModel.cs View File

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

+ 48
- 0
src/Discord.Net.Core/Cache/Models/Message/IMessageModel.cs View File

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

+ 14
- 0
src/Discord.Net.Core/Cache/Models/Message/IReactionModel.cs View File

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

+ 15
- 0
src/Discord.Net.Core/Cache/Models/Message/IStickerModel.cs View File

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

+ 48
- 0
src/Discord.Net.Core/Cache/Models/Presense/IActivityModel.cs View File

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

+ 17
- 0
src/Discord.Net.Core/Cache/Models/Presense/IPresenceModel.cs View File

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

+ 19
- 0
src/Discord.Net.Core/Cache/Models/Users/ICurrentUserModel.cs View File

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

+ 22
- 0
src/Discord.Net.Core/Cache/Models/Users/IMemberModel.cs View File

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

+ 14
- 0
src/Discord.Net.Core/Cache/Models/Users/IThreadMemberModel.cs View File

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

+ 16
- 0
src/Discord.Net.Core/Cache/Models/Users/IUserModel.cs View File

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

+ 2
- 0
src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs View File

@@ -107,6 +107,8 @@ namespace Discord
/// </returns>
public string TrackUrl { get; internal set; }

internal string AlbumArt { get; set; }

internal SpotifyGame() { }

/// <summary>


+ 9
- 1
src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs View File

@@ -24,6 +24,13 @@ namespace Discord
/// </returns>
public bool RequireColons { get; }
/// <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.
/// </summary>
/// <returns>
@@ -39,12 +46,13 @@ namespace Discord
/// </returns>
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;
RequireColons = requireColons;
RoleIds = roleIds;
CreatorId = userId;
IsAvailable = isAvailable;
}

private string DebuggerDisplay => $"{Name} ({Id})";


+ 5
- 0
src/Discord.Net.Core/Entities/Messages/IMessage.cs View File

@@ -46,6 +46,11 @@ namespace Discord
/// </returns>
bool MentionedEveryone { get; }
/// <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.
/// </summary>
/// <returns>


+ 25
- 3
src/Discord.Net.Core/Entities/Messages/MessageInteraction.cs View File

@@ -10,7 +10,7 @@ namespace Discord
/// Represents a partial <see cref="IDiscordInteraction"/> within a message.
/// </summary>
/// <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>
/// Gets the snowflake id of the interaction.
@@ -30,14 +30,36 @@ namespace Discord
/// <summary>
/// Gets the <typeparamref name="TUser"/> who invoked the interaction.
/// </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)
{
Id = id;
Type = type;
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;


+ 4
- 0
src/Discord.Net.Core/Utils/Optional.cs View File

@@ -56,5 +56,9 @@ namespace Discord
public static T? ToNullable<T>(this Optional<T> val)
where T : struct
=> 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();
}
}

+ 23
- 1
src/Discord.Net.Rest/API/Common/ActionRowComponent.cs View File

@@ -3,7 +3,7 @@ using System.Linq;

namespace Discord.API
{
internal class ActionRowComponent : IMessageComponent
internal class ActionRowComponent : IMessageComponent, IMessageComponentModel
{
[JsonProperty("type")]
public ComponentType Type { get; set; }
@@ -29,5 +29,27 @@ namespace Discord.API

[JsonIgnore]
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
}
}

+ 12
- 1
src/Discord.Net.Rest/API/Common/Attachment.cs View File

@@ -2,7 +2,7 @@ using Newtonsoft.Json;

namespace Discord.API
{
internal class Attachment
internal class Attachment : IAttachmentModel
{
[JsonProperty("id")]
public ulong Id { get; set; }
@@ -24,5 +24,16 @@ namespace Discord.API
public Optional<int> Width { get; set; }
[JsonProperty("ephemeral")]
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(); }
}
}

+ 23
- 1
src/Discord.Net.Rest/API/Common/ButtonComponent.cs View File

@@ -2,7 +2,7 @@ using Newtonsoft.Json;

namespace Discord.API
{
internal class ButtonComponent : IMessageComponent
internal class ButtonComponent : IMessageComponent, IMessageComponentModel
{
[JsonProperty("type")]
public ComponentType Type { get; set; }
@@ -59,5 +59,27 @@ namespace Discord.API

[JsonIgnore]
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
}
}

+ 70
- 0
src/Discord.Net.Rest/API/Common/CurrentUser.cs View File

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

+ 11
- 1
src/Discord.Net.Rest/API/Common/Embed.cs View File

@@ -4,7 +4,7 @@ using Discord.Net.Converters;

namespace Discord.API
{
internal class Embed
internal class Embed : IEmbedModel
{
[JsonProperty("title")]
public string Title { get; set; }
@@ -32,5 +32,15 @@ namespace Discord.API
public Optional<EmbedProvider> Provider { get; set; }
[JsonProperty("fields")]
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(); }
}
}

+ 1
- 1
src/Discord.Net.Rest/API/Common/EmbedAuthor.cs View File

@@ -2,7 +2,7 @@ using Newtonsoft.Json;

namespace Discord.API
{
internal class EmbedAuthor
internal class EmbedAuthor : IEmbedAuthorModel
{
[JsonProperty("name")]
public string Name { get; set; }


+ 2
- 2
src/Discord.Net.Rest/API/Common/EmbedField.cs View File

@@ -1,8 +1,8 @@
using Newtonsoft.Json;
using Newtonsoft.Json;

namespace Discord.API
{
internal class EmbedField
internal class EmbedField : IEmbedFieldModel
{
[JsonProperty("name")]
public string Name { get; set; }


+ 1
- 1
src/Discord.Net.Rest/API/Common/EmbedFooter.cs View File

@@ -2,7 +2,7 @@ using Newtonsoft.Json;

namespace Discord.API
{
internal class EmbedFooter
internal class EmbedFooter : IEmbedFooterModel
{
[JsonProperty("text")]
public string Text { get; set; }


+ 4
- 1
src/Discord.Net.Rest/API/Common/EmbedImage.cs View File

@@ -2,7 +2,7 @@ using Newtonsoft.Json;

namespace Discord.API
{
internal class EmbedImage
internal class EmbedImage : IEmbedMediaModel
{
[JsonProperty("url")]
public string Url { get; set; }
@@ -12,5 +12,8 @@ namespace Discord.API
public Optional<int> Height { get; set; }
[JsonProperty("width")]
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
- 1
src/Discord.Net.Rest/API/Common/EmbedProvider.cs View File

@@ -2,7 +2,7 @@ using Newtonsoft.Json;

namespace Discord.API
{
internal class EmbedProvider
internal class EmbedProvider : IEmbedProviderModel
{
[JsonProperty("name")]
public string Name { get; set; }


+ 4
- 1
src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs View File

@@ -2,7 +2,7 @@ using Newtonsoft.Json;

namespace Discord.API
{
internal class EmbedThumbnail
internal class EmbedThumbnail : IEmbedMediaModel
{
[JsonProperty("url")]
public string Url { get; set; }
@@ -12,5 +12,8 @@ namespace Discord.API
public Optional<int> Height { get; set; }
[JsonProperty("width")]
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(); }
}
}

+ 6
- 1
src/Discord.Net.Rest/API/Common/EmbedVideo.cs View File

@@ -2,13 +2,18 @@ using Newtonsoft.Json;

namespace Discord.API
{
internal class EmbedVideo
internal class EmbedVideo : IEmbedMediaModel
{
[JsonProperty("url")]
public string Url { get; set; }
[JsonProperty("proxy_url")]
public string ProxyUrl { get; set; }
[JsonProperty("height")]
public Optional<int> Height { get; set; }
[JsonProperty("width")]
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(); }
}
}

+ 52
- 1
src/Discord.Net.Rest/API/Common/Emoji.cs View File

@@ -1,8 +1,9 @@
using Newtonsoft.Json;
using System;

namespace Discord.API
{
internal class Emoji
internal class Emoji : IEmojiModel
{
[JsonProperty("id")]
public ulong? Id { get; set; }
@@ -16,7 +17,57 @@ namespace Discord.API
public bool RequireColons { get; set; }
[JsonProperty("managed")]
public bool Managed { get; set; }
[JsonProperty("available")]
public Optional<bool> Available { get; set; }
[JsonProperty("user")]
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();
}
}
}

+ 97
- 2
src/Discord.Net.Rest/API/Common/Game.cs View File

@@ -1,10 +1,11 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System;
using System.Runtime.Serialization;

namespace Discord.API
{
internal class Game
internal class Game : IActivityModel
{
[JsonProperty("name")]
public string Name { get; set; }
@@ -32,7 +33,7 @@ namespace Discord.API
public Optional<string> SyncId { get; set; }
[JsonProperty("session_id")]
public Optional<string> SessionId { get; set; }
[JsonProperty("Flags")]
[JsonProperty("flags")]
public Optional<ActivityProperties> Flags { get; set; }
[JsonProperty("id")]
public Optional<string> Id { get; set; }
@@ -40,6 +41,100 @@ namespace Discord.API
public Optional<Emoji> Emoji { get; set; }
[JsonProperty("created_at")]
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")]
//public Optional<RichPresenceButton[]> Buttons { get; set; }



+ 42
- 1
src/Discord.Net.Rest/API/Common/GuildMember.cs View File

@@ -3,7 +3,7 @@ using System;

namespace Discord.API
{
internal class GuildMember
internal class GuildMember : IMemberModel
{
[JsonProperty("user")]
public User User { get; set; }
@@ -25,5 +25,46 @@ namespace Discord.API
public Optional<DateTimeOffset?> PremiumSince { get; set; }
[JsonProperty("communication_disabled_until")]
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();
}
}
}

+ 34
- 1
src/Discord.Net.Rest/API/Common/Message.cs View File

@@ -1,9 +1,10 @@
using Newtonsoft.Json;
using System;
using System.Linq;

namespace Discord.API
{
internal class Message
internal class Message : IMessageModel
{
[JsonProperty("id")]
public ulong Id { get; set; }
@@ -49,6 +50,8 @@ namespace Discord.API
// sent with Rich Presence-related chat embeds
[JsonProperty("application")]
public Optional<MessageApplication> Application { get; set; }
[JsonProperty("application_id")]
public Optional<ulong> ApplicationId { get; set; }
[JsonProperty("message_reference")]
public Optional<MessageReference> Reference { get; set; }
[JsonProperty("flags")]
@@ -62,5 +65,35 @@ namespace Discord.API
public Optional<MessageInteraction> Interaction { get; set; }
[JsonProperty("sticker_items")]
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(); }
}
}

+ 4
- 1
src/Discord.Net.Rest/API/Common/MessageActivity.cs View File

@@ -7,11 +7,14 @@ using System.Threading.Tasks;

namespace Discord.API
{
public class MessageActivity
public class MessageActivity : IMessageActivityModel
{
[JsonProperty("type")]
public Optional<MessageActivityType> Type { get; set; }
[JsonProperty("party_id")]
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(); }
}
}

+ 6
- 1
src/Discord.Net.Rest/API/Common/MessageApplication.cs View File

@@ -7,7 +7,7 @@ using System.Threading.Tasks;

namespace Discord.API
{
public class MessageApplication
internal class MessageApplication : IPartialApplicationModel
{
/// <summary>
/// Gets the snowflake ID of the application.
@@ -34,5 +34,10 @@ namespace Discord.API
/// </summary>
[JsonProperty("name")]
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(); }
}
}

+ 25
- 1
src/Discord.Net.Rest/API/Common/Presence.cs View File

@@ -1,10 +1,11 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Discord.API
{
internal class Presence
internal class Presence : IPresenceModel
{
[JsonProperty("user")]
public User User { get; set; }
@@ -28,5 +29,28 @@ namespace Discord.API
public List<Game> Activities { get; set; }
[JsonProperty("premium_since")]
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();
}
}
}

+ 6
- 2
src/Discord.Net.Rest/API/Common/Reaction.cs View File

@@ -1,8 +1,8 @@
using Newtonsoft.Json;
using Newtonsoft.Json;

namespace Discord.API
{
internal class Reaction
internal class Reaction : IReactionMetadataModel
{
[JsonProperty("count")]
public int Count { get; set; }
@@ -10,5 +10,9 @@ namespace Discord.API
public bool Me { get; set; }
[JsonProperty("emoji")]
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(); }
}
}

+ 24
- 1
src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs View File

@@ -3,7 +3,7 @@ using System.Linq;

namespace Discord.API
{
internal class SelectMenuComponent : IMessageComponent
internal class SelectMenuComponent : IMessageComponent, IMessageComponentModel
{
[JsonProperty("type")]
public ComponentType Type { get; set; }
@@ -28,6 +28,7 @@ namespace Discord.API

[JsonProperty("values")]
public Optional<string[]> Values { get; set; }

public SelectMenuComponent() { }

public SelectMenuComponent(Discord.SelectMenuComponent component)
@@ -40,5 +41,27 @@ namespace Discord.API
MaxValues = component.MaxValues;
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
}
}

+ 9
- 1
src/Discord.Net.Rest/API/Common/SelectMenuOption.cs View File

@@ -2,7 +2,7 @@ using Newtonsoft.Json;

namespace Discord.API
{
internal class SelectMenuOption
internal class SelectMenuOption : IMessageComponentOptionModel
{
[JsonProperty("label")]
public string Label { get; set; }
@@ -49,5 +49,13 @@ namespace Discord.API

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

+ 6
- 1
src/Discord.Net.Rest/API/Common/StickerItem.cs View File

@@ -2,7 +2,7 @@ using Newtonsoft.Json;

namespace Discord.API
{
internal class StickerItem
internal class StickerItem : IStickerItemModel
{
[JsonProperty("id")]
public ulong Id { get; set; }
@@ -12,5 +12,10 @@ namespace Discord.API

[JsonProperty("format_type")]
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(); }
}
}

+ 25
- 1
src/Discord.Net.Rest/API/Common/TextInputComponent.cs View File

@@ -2,7 +2,7 @@ using Newtonsoft.Json;

namespace Discord.API
{
internal class TextInputComponent : IMessageComponent
internal class TextInputComponent : IMessageComponent, IMessageComponentModel
{
[JsonProperty("type")]
public ComponentType Type { get; set; }
@@ -45,5 +45,29 @@ namespace Discord.API
Required = component.Required ?? Optional<bool>.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
}
}

+ 5
- 4
src/Discord.Net.Rest/API/Common/ThreadMember.cs View File

@@ -3,10 +3,10 @@ using System;

namespace Discord.API
{
internal class ThreadMember
internal class ThreadMember : IThreadMemberModel
{
[JsonProperty("id")]
public Optional<ulong> Id { get; set; }
public Optional<ulong> ThreadId { get; set; }

[JsonProperty("user_id")]
public Optional<ulong> UserId { get; set; }
@@ -14,7 +14,8 @@ namespace Discord.API
[JsonProperty("join_timestamp")]
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(); }
}
}

+ 30
- 16
src/Discord.Net.Rest/API/Common/User.cs View File

@@ -1,8 +1,9 @@
using Newtonsoft.Json;
using System;

namespace Discord.API
{
internal class User
internal class User : IUserModel
{
[JsonProperty("id")]
public ulong Id { get; set; }
@@ -19,20 +20,33 @@ namespace Discord.API
[JsonProperty("accent_color")]
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();
}
}
}

+ 20
- 0
src/Discord.Net.Rest/ClientHelper.cs View File

@@ -46,6 +46,16 @@ namespace Discord.Rest
.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)
{
var models = await client.ApiClient.GetMyConnectionsAsync(options).ConfigureAwait(false);
@@ -151,6 +161,16 @@ namespace Discord.Rest
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)
{
var model = await client.ApiClient.GetWebhookAsync(id).ConfigureAwait(false);


+ 2
- 2
src/Discord.Net.Rest/DiscordRestApiClient.cs View File

@@ -2063,10 +2063,10 @@ namespace Discord.API
#endregion

#region Current User/DMs
public async Task<User> GetMyUserAsync(RequestOptions options = null)
public async Task<CurrentUser> GetMyUserAsync(RequestOptions options = null)
{
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)
{


+ 5
- 0
src/Discord.Net.Rest/DiscordRestClient.cs View File

@@ -158,6 +158,9 @@ namespace Discord.Rest
public Task<IReadOnlyCollection<RestGroupChannel>> GetGroupChannelsAsync(RequestOptions options = null)
=> 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)
=> ClientHelper.GetConnectionsAsync(this, options);

@@ -185,6 +188,8 @@ namespace Discord.Rest
=> ClientHelper.GetUserAsync(this, id, options);
public Task<RestGuildUser> GetGuildUserAsync(ulong guildId, ulong id, RequestOptions options = null)
=> 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)
=> ClientHelper.GetVoiceRegionsAsync(this, options);


+ 6
- 6
src/Discord.Net.Rest/Entities/Messages/Attachment.cs View File

@@ -1,5 +1,5 @@
using System.Diagnostics;
using Model = Discord.API.Attachment;
using Model = Discord.IAttachmentModel;

namespace Discord
{
@@ -44,11 +44,11 @@ namespace Discord
}
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>


+ 4
- 6
src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs View File

@@ -221,7 +221,7 @@ namespace Discord.Rest
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>();
int index = 0;
@@ -278,11 +278,9 @@ namespace Discord.Rest
IUser mentionedUser = null;
foreach (var mention in userMentions)
{
if (mention.Id == id)
if (mention == id)
{
mentionedUser = channel?.GetUserAsync(id, CacheMode.CacheOnly).GetAwaiter().GetResult();
if (mentionedUser == null)
mentionedUser = mention;
break;
}
}
@@ -372,11 +370,11 @@ namespace Discord.Rest
.ToImmutableArray();
}

public static MessageSource GetSource(Model msg)
public static MessageSource GetSource(IMessageModel msg)
{
if (msg.Type != MessageType.Default && msg.Type != MessageType.Reply)
return MessageSource.System;
else if (msg.WebhookId.IsSpecified)
else if (msg.IsWebhookMessage)
return MessageSource.Webhook;
else if (msg.Author.GetValueOrDefault()?.Bot.GetValueOrDefault(false) == true)
return MessageSource.Bot;


+ 1
- 0
src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs View File

@@ -94,6 +94,7 @@ namespace Discord.Rest
internal void Update(Model model)
{
base.Update(model.User);

if (model.JoinedAt.IsSpecified)
_joinedAtTicks = model.JoinedAt.Value.UtcTicks;
if (model.Nick.IsSpecified)


+ 13
- 15
src/Discord.Net.Rest/Entities/Users/RestSelfUser.cs View File

@@ -1,7 +1,8 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Model = Discord.API.User;
using UserModel = Discord.API.User;
using Model = Discord.API.CurrentUser;

namespace Discord.Rest
{
@@ -28,29 +29,26 @@ namespace Discord.Rest
: 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);
entity.Update(model);
return entity;
}
/// <inheritdoc />
internal override void Update(Model model)
internal override void Update(UserModel 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 />


+ 9
- 13
src/Discord.Net.Rest/Entities/Users/RestUser.cs View File

@@ -78,20 +78,16 @@ namespace Discord.Rest

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);
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 />


+ 37
- 83
src/Discord.Net.Rest/Extensions/EntityExtensions.cs View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
@@ -6,6 +7,23 @@ namespace Discord.Rest
{
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)
{
if (model.Id.HasValue)
@@ -18,21 +36,30 @@ namespace Discord.Rest
model.Name,
model.Animated.GetValueOrDefault(),
model.Managed,
model.Available.GetValueOrDefault(),
model.RequireColons,
ImmutableArray.Create(model.Roles),
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.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)
{
@@ -98,15 +125,11 @@ namespace Discord.Rest
if (mentionTypes.HasFlag(AllowedMentionTypes.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)
{
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);
}
@@ -114,48 +137,22 @@ namespace Discord.Rest
{
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)
{
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)
{
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)
{
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)
{
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)
{
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));
}

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

+ 1
- 1
src/Discord.Net.WebSocket/API/Gateway/ReadyEvent.cs View File

@@ -17,7 +17,7 @@ namespace Discord.API.Gateway
[JsonProperty("v")]
public int Version { get; set; }
[JsonProperty("user")]
public User User { get; set; }
public CurrentUser User { get; set; }
[JsonProperty("session_id")]
public string SessionId { get; set; }
[JsonProperty("read_state")]


+ 105
- 0
src/Discord.Net.WebSocket/Cache/DefaultConcurrentCacheProvider.cs View File

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

+ 39
- 0
src/Discord.Net.WebSocket/Cache/ICacheProvider.cs View File

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

+ 76
- 0
src/Discord.Net.WebSocket/Cache/LazyCached.cs View File

@@ -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) { }
}
}

+ 479
- 0
src/Discord.Net.WebSocket/ClientStateManager.Experiment.cs View File

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

src/Discord.Net.WebSocket/ClientState.cs → src/Discord.Net.WebSocket/ClientStateManager.cs View File

@@ -5,7 +5,7 @@ using System.Linq;

namespace Discord.WebSocket
{
internal class ClientState
internal partial class ClientStateManager
{
private const double AverageChannelsPerGuild = 10.22; //Source: Googie2149
private const double AverageUsersPerGuild = 47.78; //Source: Googie2149
@@ -30,8 +30,17 @@ namespace Discord.WebSocket
_groupChannels.Select(x => GetChannel(x) as ISocketPrivateChannel))
.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 estimatedUsersCount = guildCount * AverageUsersPerGuild;
_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));
_groupChannels = new ConcurrentHashSet<ulong>(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(10 * CollectionMultiplier));
_commands = new ConcurrentDictionary<ulong, SocketApplicationCommand>();

CreateStores();
}

internal SocketChannel GetChannel(ulong id)
@@ -121,22 +132,6 @@ namespace Discord.WebSocket
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()
{
foreach (var guild in _guilds.Values)

+ 1
- 1
src/Discord.Net.WebSocket/DiscordShardedClient.cs View File

@@ -200,7 +200,7 @@ namespace Discord.WebSocket
return _shards[id];
return null;
}
private int GetShardIdFor(ulong guildId)
public int GetShardIdFor(ulong guildId)
=> (int)((guildId >> 22) % (uint)_totalShards);
public int GetShardIdFor(IGuild guild)
=> GetShardIdFor(guild?.Id ?? 0);


+ 173
- 179
src/Discord.Net.WebSocket/DiscordSocketClient.cs
File diff suppressed because it is too large
View File


+ 11
- 0
src/Discord.Net.WebSocket/DiscordSocketConfig.cs View File

@@ -26,6 +26,17 @@ namespace Discord.WebSocket
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>
/// Returns the encoding gateway should use.
/// </summary>
public const string GatewayEncoding = "json";


+ 1
- 1
src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs View File

@@ -37,7 +37,7 @@ namespace Discord.WebSocket
: 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);
entity.Update(state, model);


+ 2
- 2
src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs View File

@@ -29,7 +29,7 @@ namespace Discord.WebSocket
}

/// <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
{
@@ -38,7 +38,7 @@ namespace Discord.WebSocket
_ => 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

#region User


+ 6
- 6
src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs View File

@@ -35,25 +35,25 @@ namespace Discord.WebSocket
{
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]));
entity.Update(state, model);
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));
entity.Update(state, recipient);
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 />


+ 6
- 8
src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs View File

@@ -55,13 +55,13 @@ namespace Discord.WebSocket
_voiceStates = new ConcurrentDictionary<ulong, SocketVoiceState>(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);
entity.Update(state, model);
return entity;
}
internal override void Update(ClientState state, Model model)
internal override void Update(ClientStateManager state, Model model)
{
if (model.Name.IsSpecified)
Name = model.Name.Value;
@@ -73,11 +73,11 @@ namespace Discord.WebSocket

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

@@ -265,8 +265,7 @@ namespace Discord.WebSocket
return user;
else
{
var privateUser = SocketGroupUser.Create(this, Discord.State, model);
privateUser.GlobalUser.AddRef();
var privateUser = SocketGroupUser.Create(this, model);
_users[privateUser.Id] = privateUser;
return privateUser;
}
@@ -275,7 +274,6 @@ namespace Discord.WebSocket
{
if (_users.TryRemove(id, out SocketGroupUser user))
{
user.GlobalUser.RemoveRef(Discord);
return user;
}
return null;
@@ -283,7 +281,7 @@ namespace Discord.WebSocket
#endregion

#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 voiceState = SocketVoiceState.Create(voiceChannel, model);


+ 2
- 2
src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs View File

@@ -49,7 +49,7 @@ namespace Discord.WebSocket
{
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
{
@@ -63,7 +63,7 @@ namespace Discord.WebSocket
};
}
/// <inheritdoc />
internal override void Update(ClientState state, Model model)
internal override void Update(ClientStateManager state, Model model)
{
Name = model.Name.Value;
Position = model.Position.GetValueOrDefault(0);


+ 1
- 1
src/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs View File

@@ -21,7 +21,7 @@ namespace Discord.WebSocket
: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);
entity.Update(state, model);


+ 1
- 1
src/Discord.Net.WebSocket/Entities/Channels/SocketStageChannel.cs View File

@@ -43,7 +43,7 @@ namespace Discord.WebSocket
internal SocketStageChannel(DiscordSocketClient discord, ulong id, SocketGuild 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);
entity.Update(state, model);


+ 3
- 3
src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs View File

@@ -63,13 +63,13 @@ namespace Discord.WebSocket
if (Discord.MessageCacheSize > 0)
_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);
entity.Update(state, model);
return entity;
}
internal override void Update(ClientState state, Model model)
internal override void Update(ClientStateManager state, Model model)
{
base.Update(state, model);
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 thread = (SocketThreadChannel)Guild.AddOrUpdateChannel(Discord.State, model);
var thread = (SocketThreadChannel)Guild.AddOrUpdateChannel(Discord.StateManager, model);

if(Discord.AlwaysDownloadUsers && Discord.HasGatewayIntent(GatewayIntents.GuildMembers))
await thread.DownloadUsersAsync();


+ 2
- 3
src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs View File

@@ -118,7 +118,7 @@ namespace Discord.WebSocket
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 entity = new SocketThreadChannel(guild.Discord, guild, model.Id, parent, model.ThreadMetadata.GetValueOrDefault()?.CreatedAt.GetValueOrDefault(null));
@@ -126,7 +126,7 @@ namespace Discord.WebSocket
return entity;
}

internal override void Update(ClientState state, Model model)
internal override void Update(ClientStateManager state, Model model)
{
base.Update(state, model);

@@ -171,7 +171,6 @@ namespace Discord.WebSocket
else
{
member = SocketThreadUser.Create(Guild, this, model, guildMember);
member.GlobalUser.AddRef();
_members[member.Id] = member;
}
return member;


+ 2
- 2
src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs View File

@@ -55,14 +55,14 @@ namespace Discord.WebSocket
: 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);
entity.Update(state, model);
return entity;
}
/// <inheritdoc />
internal override void Update(ClientState state, Model model)
internal override void Update(ClientStateManager state, Model model)
{
base.Update(state, model);
CategoryId = model.CategoryId;


+ 57
- 106
src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs View File

@@ -14,11 +14,11 @@ using ChannelModel = Discord.API.Channel;
using EmojiUpdateModel = Discord.API.Gateway.GuildEmojiUpdateEvent;
using ExtendedModel = Discord.API.Gateway.ExtendedGuild;
using GuildSyncModel = Discord.API.Gateway.GuildSyncEvent;
using MemberModel = Discord.API.GuildMember;
using MemberModel = Discord.IMemberModel;
using Model = Discord.API.Guild;
using PresenceModel = Discord.API.Presence;
using RoleModel = Discord.API.Role;
using UserModel = Discord.API.User;
using UserModel = Discord.IUserModel;
using VoiceStateModel = Discord.API.VoiceState;
using StickerModel = Discord.API.Sticker;
using EventModel = Discord.API.GuildScheduledEvent;
@@ -38,7 +38,7 @@ namespace Discord.WebSocket
private TaskCompletionSource<bool> _syncPromise, _downloaderPromise;
private TaskCompletionSource<AudioClient> _audioConnectPromise;
private ConcurrentDictionary<ulong, SocketGuildChannel> _channels;
private ConcurrentDictionary<ulong, SocketGuildUser> _members;
//private ConcurrentDictionary<ulong, SocketGuildUser> _members;
private ConcurrentDictionary<ulong, SocketRole> _roles;
private ConcurrentDictionary<ulong, SocketVoiceState> _voiceStates;
private ConcurrentDictionary<ulong, SocketCustomSticker> _stickers;
@@ -305,7 +305,7 @@ namespace Discord.WebSocket
/// <summary>
/// Gets the current logged-in user.
/// </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>
/// Gets the built-in role containing all users in this guild.
/// </summary>
@@ -324,7 +324,7 @@ namespace Discord.WebSocket
get
{
var channels = _channels;
var state = Discord.State;
var state = Discord.StateManager;
return channels.Select(x => x.Value).Where(x => x != null).ToReadOnlyCollection(channels);
}
}
@@ -356,7 +356,7 @@ namespace Discord.WebSocket
/// <returns>
/// A collection of guild users found within this guild.
/// </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>
/// Gets a collection of all roles in this guild.
/// </summary>
@@ -382,13 +382,13 @@ namespace Discord.WebSocket
_audioLock = new SemaphoreSlim(1, 1);
_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);
entity.Update(state, model);
return entity;
}
internal void Update(ClientState state, ExtendedModel model)
internal void Update(ClientStateManager state, ExtendedModel model)
{
IsAvailable = !(model.Unavailable ?? false);
if (!IsAvailable)
@@ -397,8 +397,6 @@ namespace Discord.WebSocket
_events = new ConcurrentDictionary<ulong, SocketGuildEvent>();
if (_channels == null)
_channels = new ConcurrentDictionary<ulong, SocketGuildChannel>();
if (_members == null)
_members = new ConcurrentDictionary<ulong, SocketGuildUser>();
if (_roles == null)
_roles = new ConcurrentDictionary<ulong, SocketRole>();
/*if (Emojis == null)
@@ -431,25 +429,6 @@ namespace Discord.WebSocket

_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));
{
for (int i = 0; i < model.VoiceStates.Length; i++)
@@ -473,6 +452,10 @@ namespace Discord.WebSocket
}
_events = events;

DownloadedMemberCount = model.Members.Length;

MemberCount = model.MemberCount;


_syncPromise = new TaskCompletionSource<bool>();
_downloaderPromise = new TaskCompletionSource<bool>();
@@ -480,7 +463,7 @@ namespace Discord.WebSocket
/*if (!model.Large)
_ = _downloaderPromise.TrySetResultAsync(true);*/
}
internal void Update(ClientState state, Model model)
internal void Update(ClientStateManager state, Model model)
{
AFKChannelId = model.AFKChannelId;
if (model.WidgetChannelId.IsSpecified)
@@ -561,31 +544,18 @@ namespace Discord.WebSocket
else
_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);
for (int i = 0; i < model.Emojis.Length; i++)
@@ -682,7 +652,7 @@ namespace Discord.WebSocket
/// </returns>
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)
return channel;
return null;
@@ -799,7 +769,7 @@ namespace Discord.WebSocket
public Task<RestCategoryChannel> CreateCategoryChannelAsync(string name, Action<GuildChannelProperties> func = null, RequestOptions options = null)
=> 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);
_channels.TryAdd(model.Id, channel);
@@ -807,26 +777,26 @@ namespace Discord.WebSocket
return channel;
}

internal SocketGuildChannel AddOrUpdateChannel(ClientState state, ChannelModel model)
internal SocketGuildChannel AddOrUpdateChannel(ClientStateManager state, ChannelModel model)
{
if (_channels.TryGetValue(model.Id, out SocketGuildChannel channel))
channel.Update(Discord.State, model);
channel.Update(Discord.StateManager, model);
else
{
channel = SocketGuildChannel.Create(this, Discord.State, model);
channel = SocketGuildChannel.Create(this, Discord.StateManager, model);
_channels[channel.Id] = channel;
state.AddChannel(channel);
}
return channel;
}

internal SocketGuildChannel RemoveChannel(ClientState state, ulong id)
internal SocketGuildChannel RemoveChannel(ClientStateManager state, ulong id)
{
if (_channels.TryRemove(id, out var _))
return state.RemoveChannel(id) as SocketGuildChannel;
return null;
}
internal void PurgeChannelCache(ClientState state)
internal void PurgeChannelCache(ClientStateManager state)
{
foreach (var channelId in _channels)
state.RemoveChannel(channelId.Key);
@@ -880,7 +850,7 @@ namespace Discord.WebSocket

foreach (var command in commands)
{
Discord.State.AddCommand(command);
Discord.StateManager.AddCommand(command);
}

return commands.ToImmutableArray();
@@ -898,7 +868,7 @@ namespace Discord.WebSocket
/// </returns>
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)
return command;
@@ -913,7 +883,7 @@ namespace Discord.WebSocket

command = SocketApplicationCommand.Create(Discord, model, Id);

Discord.State.AddCommand(command);
Discord.StateManager.AddCommand(command);

return command;
}
@@ -930,7 +900,7 @@ namespace Discord.WebSocket
{
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);

@@ -952,11 +922,11 @@ namespace Discord.WebSocket

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)
{
Discord.State.AddCommand(entity);
Discord.StateManager.AddCommand(entity);
}

return entities.ToImmutableArray();
@@ -1020,7 +990,7 @@ namespace Discord.WebSocket
=> GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, isMentionable, options);
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;
return role;
}
@@ -1034,7 +1004,7 @@ namespace Discord.WebSocket
internal SocketRole AddOrUpdateRole(RoleModel model)
{
if (_roles.TryGetValue(model.Id, out SocketRole role))
_roles[model.Id].Update(Discord.State, model);
_roles[model.Id].Update(Discord.StateManager, model);
else
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.
/// </returns>
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 />
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);

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
{
member = SocketGuildUser.Create(this, Discord.State, model);
member.GlobalUser.AddRef();
_members[member.Id] = member;
member = SocketGuildUser.Create(Id, Discord, model);
DownloadedMemberCount++;
}
return member;
}
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
{
member = SocketGuildUser.Create(this, Discord.State, model);
member.GlobalUser.AddRef();
_members[member.Id] = member;
member = SocketGuildUser.Create(Id, Discord, model);
DownloadedMemberCount++;
}
return member;
}
internal SocketGuildUser RemoveUser(ulong id)
{
if (_members.TryRemove(id, out SocketGuildUser member))
SocketGuildUser member;
if ((member = GetUser(id)) != null)
{
DownloadedMemberCount--;
member.GlobalUser.RemoveRef(Discord);
if (Discord.StateManager.TryGetMemberStore(Id, out var store))
store.Remove(id);
return member;
}
return null;
@@ -1158,18 +1111,17 @@ namespace Discord.WebSocket
/// <param name="predicate">The predicate used to select which users to clear.</param>
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>();
DownloadedMemberCount = _members.Count;
DownloadedMemberCount = membersToKeep.Count();
}

/// <summary>
@@ -1291,7 +1243,6 @@ namespace Discord.WebSocket
/// in order to use this property.
/// </remarks>
/// </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="coverImage">The optional banner image for the event.</param>
/// <param name="options">The options to be used when sending the request.</param>
@@ -1537,7 +1488,7 @@ namespace Discord.WebSocket
#endregion

#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 before = GetVoiceState(model.UserId) ?? SocketVoiceState.Default;


+ 2
- 2
src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs View File

@@ -89,13 +89,13 @@ namespace Discord.WebSocket
if(guildUser != null)
{
if(model.Creator.IsSpecified)
guildUser.Update(Discord.State, model.Creator.Value);
guildUser.Update(model.Creator.Value);

Creator = guildUser;
}
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;
}
}


+ 3
- 3
src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs View File

@@ -56,18 +56,18 @@ namespace Discord.WebSocket
if (Channel is SocketGuildChannel channel)
{
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)
author = channel.Guild.GetUser(model.Message.Value.Author.Value.Id);
}
else if (model.Message.Value.Author.IsSpecified)
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
{
Message.Update(Discord.State, model.Message.Value);
Message.Update(Discord.StateManager, model.Message.Value);
}
}
}


+ 7
- 7
src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs View File

@@ -29,7 +29,7 @@ namespace Discord.WebSocket
{
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);
}
@@ -50,11 +50,11 @@ namespace Discord.WebSocket
: discord.Rest.ApiClient.GetChannelAsync(channel.Value.Id).ConfigureAwait(false).GetAwaiter().GetResult();

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);
}
}
@@ -88,7 +88,7 @@ namespace Discord.WebSocket
if (guild != null)
{
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
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
{
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);
}
}


+ 357
- 58
src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs View File

@@ -5,19 +5,25 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Model = Discord.API.Message;
using Model = Discord.IMessageModel;

namespace Discord.WebSocket
{
/// <summary>
/// Represents a WebSocket-based message.
/// </summary>
public abstract class SocketMessage : SocketEntity<ulong>, IMessage
public abstract class SocketMessage : SocketEntity<ulong>, IMessage, ICached<Model>
{
#region SocketMessage
internal bool IsFreed { get; set; }
private long _timestampTicks;
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>
/// Gets the author of this message.
@@ -54,6 +60,7 @@ namespace Discord.WebSocket
public virtual DateTimeOffset? EditedTimestamp => null;
/// <inheritdoc />
public virtual bool MentionedEveryone => false;
public virtual ulong? ApplicationId { get; private set; }

/// <inheritdoc />
public MessageActivity Activity { get; private set; }
@@ -115,10 +122,13 @@ namespace Discord.WebSocket
/// <summary>
/// Returns the users mentioned in this message.
/// </summary>
/// <remarks>
/// The returned enumerable will preform cache lookups per enumeration.
/// </remarks>
/// <returns>
/// Collection of WebSocket-based users.
/// </returns>
public IReadOnlyCollection<SocketUser> MentionedUsers => _userMentions;
public IEnumerable<SocketUser> MentionedUsers => Discord.StateManager.UserStore.GetEnumerable(_userMentionIds); // TODO: async counterpart?
/// <inheritdoc />
public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks);

@@ -129,7 +139,13 @@ namespace Discord.WebSocket
Author = author;
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 ||
model.Type == MessageType.Reply ||
@@ -140,55 +156,52 @@ namespace Discord.WebSocket
else
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;

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
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
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
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)
{
@@ -236,38 +249,16 @@ namespace Discord.WebSocket
else
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 />
@@ -309,7 +300,6 @@ namespace Discord.WebSocket
/// <inheritdoc />
IReadOnlyCollection<IStickerItem> IMessage.Stickers => Stickers;


internal void AddReaction(SocketReaction reaction)
{
_reactions.Add(reaction);
@@ -347,5 +337,314 @@ namespace Discord.WebSocket
public IAsyncEnumerable<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IEmote emote, int limit, RequestOptions options = null)
=> MessageHelper.GetReactionUsersAsync(this, emote, limit, Discord, options);
#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
}
}

+ 33
- 22
src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs View File

@@ -20,12 +20,19 @@ namespace Discord.WebSocket
/// </returns>
public ulong UserId { 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; }
/// <summary>
/// Gets the user who added the reaction if possible.
/// </summary>
/// <remarks>
/// <para>
/// 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
/// <see cref="Discord.WebSocket.SocketReaction.UserId" />.
/// </para>
@@ -35,25 +42,16 @@ namespace Discord.WebSocket
/// </para>
/// </remarks>
/// <returns>
/// A user object where possible; a value is not always returned.
/// A lazily-cached user object.
/// </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>
/// Gets the message that has been reacted to if possible.
/// </summary>
/// <returns>
/// A WebSocket-based message where possible; a value is not always returned.
/// A lazily-cached message.
/// </returns>
/// <seealso cref="Optional{T}"/>
public Optional<SocketUserMessage> Message { get; }
public LazyCached<SocketMessage> Message { get; }
/// <summary>
/// Gets the channel where the reaction takes place in.
/// </summary>
@@ -64,16 +62,26 @@ namespace Discord.WebSocket
/// <inheritdoc />
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;
Message = message;
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;
}
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;
if (model.Emoji.Id.HasValue)
@@ -86,11 +94,14 @@ namespace Discord.WebSocket
/// <inheritdoc />
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;
if (otherReaction == null) return false;
if (otherReaction == null)
return false;

return UserId == otherReaction.UserId && MessageId == otherReaction.MessageId && Emote.Equals(otherReaction.Emote);
}


+ 2
- 2
src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs View File

@@ -13,13 +13,13 @@ namespace Discord.WebSocket
: 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);
entity.Update(state, model);
return entity;
}
internal override void Update(ClientState state, Model model)
internal override void Update(ClientStateManager state, Model model)
{
base.Update(state, model);
}


+ 23
- 51
src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs View File

@@ -5,7 +5,7 @@ using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Model = Discord.API.Message;
using Model = Discord.IMessageModel;

namespace Discord.WebSocket
{
@@ -17,11 +17,12 @@ namespace Discord.WebSocket
{
private bool _isMentioningEveryone, _isTTS, _isPinned;
private long? _editedTimestampTicks;
private IUserMessage _referencedMessage;
private ImmutableArray<Attachment> _attachments = ImmutableArray.Create<Attachment>();
private ImmutableArray<Embed> _embeds = ImmutableArray.Create<Embed>();
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>();

/// <inheritdoc />
@@ -53,33 +54,29 @@ namespace Discord.WebSocket
: 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));
entity.Update(state, model);
entity.Update(model);
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;

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)
{
var attachments = ImmutableArray.CreateBuilder<Attachment>(value.Length);
@@ -91,9 +88,9 @@ namespace Discord.WebSocket
_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)
{
var embeds = ImmutableArray.CreateBuilder<Embed>(value.Length);
@@ -105,41 +102,16 @@ namespace Discord.WebSocket
_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);
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)
{
var stickers = ImmutableArray.CreateBuilder<SocketSticker>(value.Length);


+ 2
- 2
src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs View File

@@ -67,13 +67,13 @@ namespace Discord.WebSocket
{
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);
entity.Update(state, model);
return entity;
}
internal void Update(ClientState state, Model model)
internal void Update(ClientStateManager state, Model model)
{
Name = model.Name;
IsHoisted = model.Hoist;


+ 2
- 2
src/Discord.Net.WebSocket/Entities/Stickers/SocketUnknownSticker.cs View File

@@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Model = Discord.API.StickerItem;
using Model = Discord.IStickerItemModel;

namespace Discord.WebSocket
{
@@ -47,7 +47,7 @@ namespace Discord.WebSocket
internal void Update(Model model)
{
Name = model.Name;
Format = model.FormatType;
Format = model.Format;
}

/// <summary>


+ 9
- 24
src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs View File

@@ -1,51 +1,36 @@
using System;
using System.Diagnostics;
using System.Linq;
using Model = Discord.API.User;
using Model = Discord.IUserModel;

namespace Discord.WebSocket
{
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
internal class SocketGlobalUser : SocketUser
internal class SocketGlobalUser : SocketUser, IDisposable
{
public override bool IsBot { get; internal set; }
public override string Username { get; internal set; }
public override ushort DiscriminatorValue { get; internal set; }
public override string AvatarId { get; internal set; }
internal override SocketPresence Presence { get; set; }

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)
: 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);
entity.Update(state, model);
entity.Update(model);
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)";


+ 14
- 19
src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs View File

@@ -18,38 +18,33 @@ namespace Discord.WebSocket
/// A <see cref="SocketGroupChannel" /> representing the channel of which the user belongs to.
/// </returns>
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 />
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;
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;
}

private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Group)";
internal new SocketGroupUser Clone() => MemberwiseClone() as SocketGroupUser;
public override void Dispose()
{
GC.SuppressFinalize(this);

if (GlobalUser.IsValueCreated)
GlobalUser.Value.Dispose();
}
~SocketGroupUser() => Dispose();

#endregion

#region IVoiceState


+ 104
- 86
src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs View File

@@ -6,9 +6,9 @@ using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
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
{
@@ -16,19 +16,23 @@ namespace Discord.WebSocket
/// Represents a WebSocket-based guild user.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketGuildUser : SocketUser, IGuildUser
public class SocketGuildUser : SocketUser, IGuildUser, ICached<MemberModel>, IDisposable
{
#region SocketGuildUser
private long? _premiumSinceTicks;
private long? _timedOutTicks;
private long? _joinedAtTicks;
private ImmutableArray<ulong> _roleIds;
private ulong _guildId;

internal override SocketGlobalUser GlobalUser { get; set; }
/// <summary>
/// Gets the guild the user is in.
/// </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 />
public string DisplayName => Nickname ?? Username;
/// <inheritdoc />
@@ -38,17 +42,16 @@ namespace Discord.WebSocket
/// <inheritdoc/>
public string GuildAvatarId { get; private set; }
/// <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 />
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 />
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 />
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 />
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 />
public override bool IsWebhook => false;
@@ -71,14 +74,13 @@ namespace Discord.WebSocket
/// <inheritdoc />
public bool? IsPending { get; private set; }


/// <inheritdoc />
public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks);
/// <summary>
/// Returns a collection of roles that the user possesses.
/// </summary>
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>
/// Returns the voice channel the user is in, or <c>null</c> if none.
/// </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
/// connected to a voice channel.
/// </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 />
public DateTimeOffset? PremiumSince => DateTimeUtils.FromTicks(_premiumSinceTicks);
/// <inheritdoc />
@@ -119,13 +121,13 @@ namespace Discord.WebSocket
{
get
{
if (Guild.OwnerId == Id)
if (Guild.Value.OwnerId == Id)
return int.MaxValue;

int maxPos = 0;
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)
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;
}
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;
}
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)
{
var roles = ImmutableArray.CreateBuilder<ulong>(roleIds.Length + 1);
roles.Add(Guild.Id);
roles.Add(_guildId);
for (int i = 0; i < roleIds.Length; i++)
roles.Add(roleIds[i]);
_roleIds = roles.ToImmutable();
@@ -249,7 +215,7 @@ namespace Discord.WebSocket
=> UserHelper.RemoveTimeOutAsync(this, Discord, options);
/// <inheritdoc />
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 />
public string GetDisplayAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128)
@@ -259,23 +225,30 @@ namespace Discord.WebSocket

/// <inheritdoc />
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)";

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

#region IGuildUser
/// <inheritdoc />
IGuild IGuildUser.Guild => Guild;
IGuild IGuildUser.Guild => Guild.Value;
/// <inheritdoc />
ulong IGuildUser.GuildId => Guild.Id;
ulong IGuildUser.GuildId => _guildId;
/// <inheritdoc />
IReadOnlyCollection<ulong> IGuildUser.RoleIds => _roleIds;

@@ -283,5 +256,50 @@ namespace Discord.WebSocket
/// <inheritdoc />
IVoiceChannel IVoiceState.VoiceChannel => VoiceChannel;
#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
}
}

+ 140
- 9
src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs View File

@@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using Model = Discord.API.Presence;
using Model = Discord.IPresenceModel;

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.
/// </summary>
[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 />
public UserStatus Status { get; private set; }
/// <inheritdoc />
@@ -20,17 +25,24 @@ namespace Discord.WebSocket
/// <inheritdoc />
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;
ActiveClients = activeClients ?? ImmutableHashSet<ClientType>.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);
return entity;
}
@@ -38,8 +50,10 @@ namespace Discord.WebSocket
internal void Update(Model model)
{
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;
UserId = model.UserId;
GuildId = model.GuildId;
}

/// <summary>
@@ -76,9 +90,9 @@ namespace Discord.WebSocket
/// <returns>
/// A list of all <see cref="IActivity"/> that this user currently has available.
/// </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;
var list = new List<IActivity>();
foreach (var activity in activities)
@@ -96,5 +110,122 @@ namespace Discord.WebSocket
private string DebuggerDisplay => $"{Status}{(Activities?.FirstOrDefault()?.Name ?? "")}";

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

+ 85
- 34
src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs View File

@@ -2,7 +2,8 @@ using Discord.Rest;
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Model = Discord.API.User;
using Model = Discord.ICurrentUserModel;
using UserModel = Discord.IUserModel;

namespace Discord.WebSocket
{
@@ -10,7 +11,7 @@ namespace Discord.WebSocket
/// Represents the logged-in WebSocket-based user.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SocketSelfUser : SocketUser, ISelfUser
public class SocketSelfUser : SocketUser, ISelfUser, ICached<Model>
{
/// <inheritdoc />
public string Email { get; private set; }
@@ -18,18 +19,6 @@ namespace Discord.WebSocket
public bool IsVerified { get; private set; }
/// <inheritdoc />
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 />
public UserProperties Flags { get; internal set; }
/// <inheritdoc />
@@ -40,48 +29,52 @@ namespace Discord.WebSocket
/// <inheritdoc />
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;
}
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;
}
if (model.Verified.IsSpecified)
if (currentUserModel.IsVerified.HasValue)
{
IsVerified = model.Verified.Value;
IsVerified = currentUserModel.IsVerified.Value;
hasGlobalChanges = true;
}
if (model.MfaEnabled.IsSpecified)
if (currentUserModel.IsMfaEnabled.HasValue)
{
IsMfaEnabled = model.MfaEnabled.Value;
IsMfaEnabled = currentUserModel.IsMfaEnabled.Value;
hasGlobalChanges = true;
}
if (model.Flags.IsSpecified && model.Flags.Value != Flags)
if (currentUserModel.Flags != Flags)
{
Flags = (UserProperties)model.Flags.Value;
Flags = currentUserModel.Flags;
hasGlobalChanges = true;
}
if (model.PremiumType.IsSpecified && model.PremiumType.Value != PremiumType)
if (currentUserModel.PremiumType != PremiumType)
{
PremiumType = model.PremiumType.Value;
PremiumType = currentUserModel.PremiumType;
hasGlobalChanges = true;
}
if (model.Locale.IsSpecified && model.Locale.Value != Locale)
if (currentUserModel.Locale != Locale)
{
Locale = model.Locale.Value;
Locale = currentUserModel.Locale;
hasGlobalChanges = true;
}
return hasGlobalChanges;
@@ -93,5 +86,63 @@ namespace Discord.WebSocket

private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Self)";
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
}
}

+ 111
- 68
src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs View File

@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Model = Discord.API.ThreadMember;
using Model = Discord.IThreadMemberModel;
using System.Collections.Immutable;

namespace Discord.WebSocket
@@ -10,12 +10,12 @@ namespace Discord.WebSocket
/// <summary>
/// Represents a thread user received over the gateway.
/// </summary>
public class SocketThreadUser : SocketUser, IThreadUser, IGuildUser
public class SocketThreadUser : SocketUser, IThreadUser, IGuildUser, ICached<Model>
{
/// <summary>
/// Gets the <see cref="SocketThreadChannel"/> this user is in.
/// </summary>
public SocketThreadChannel Thread { get; private set; }
public Lazy<SocketThreadChannel> Thread { get; private set; }

/// <inheritdoc/>
public DateTimeOffset ThreadJoinedAt { get; private set; }
@@ -23,126 +23,142 @@ namespace Discord.WebSocket
/// <summary>
/// Gets the guild this user is in.
/// </summary>
public SocketGuild Guild { get; private set; }
public Lazy<SocketGuild> Guild { get; private set; }

/// <inheritdoc/>
public DateTimeOffset? JoinedAt
=> GuildUser.JoinedAt;
=> GuildUser.Value.JoinedAt;

/// <inheritdoc/>
public string DisplayName
=> GuildUser.Nickname ?? GuildUser.Username;
=> GuildUser.Value.Nickname ?? GuildUser.Value.Username;

/// <inheritdoc/>
public string Nickname
=> GuildUser.Nickname;
=> GuildUser.Value.Nickname;

/// <inheritdoc/>
public DateTimeOffset? PremiumSince
=> GuildUser.PremiumSince;
=> GuildUser.Value.PremiumSince;

/// <inheritdoc/>
public DateTimeOffset? TimedOutUntil
=> GuildUser.TimedOutUntil;
=> GuildUser.Value.TimedOutUntil;

/// <inheritdoc/>
public bool? IsPending
=> GuildUser.IsPending;
=> GuildUser.Value.IsPending;

/// <inheritdoc />
public int Hierarchy
=> GuildUser.Hierarchy;
=> GuildUser.Value.Hierarchy;

/// <inheritdoc/>
public override string AvatarId
{
get => GuildUser.AvatarId;
internal set => GuildUser.AvatarId = value;
get => GuildUser.Value.AvatarId;
internal set => GuildUser.Value.AvatarId = value;
}

/// <inheritdoc/>
public string DisplayAvatarId => GuildAvatarId ?? AvatarId;

/// <inheritdoc/>
public string GuildAvatarId
=> GuildUser.GuildAvatarId;
=> GuildUser.Value.GuildAvatarId;

/// <inheritdoc/>
public override ushort DiscriminatorValue
{
get => GuildUser.DiscriminatorValue;
internal set => GuildUser.DiscriminatorValue = value;
get => GuildUser.Value.DiscriminatorValue;
internal set => GuildUser.Value.DiscriminatorValue = value;
}

/// <inheritdoc/>
public override bool IsBot
{
get => GuildUser.IsBot;
internal set => GuildUser.IsBot = value;
get => GuildUser.Value.IsBot;
internal set => GuildUser.Value.IsBot = value;
}

/// <inheritdoc/>
public override bool IsWebhook
=> GuildUser.IsWebhook;
=> GuildUser.Value.IsWebhook;

/// <inheritdoc/>
public override string Username
{
get => GuildUser.Username;
internal set => GuildUser.Username = value;
get => GuildUser.Value.Username;
internal set => GuildUser.Value.Username = value;
}

/// <inheritdoc/>
public bool IsDeafened
=> GuildUser.IsDeafened;
=> GuildUser.Value.IsDeafened;

/// <inheritdoc/>
public bool IsMuted
=> GuildUser.IsMuted;
=> GuildUser.Value.IsMuted;

/// <inheritdoc/>
public bool IsSelfDeafened
=> GuildUser.IsSelfDeafened;
=> GuildUser.Value.IsSelfDeafened;

/// <inheritdoc/>
public bool IsSelfMuted
=> GuildUser.IsSelfMuted;
=> GuildUser.Value.IsSelfMuted;

/// <inheritdoc/>
public bool IsSuppressed
=> GuildUser.IsSuppressed;
=> GuildUser.Value.IsSuppressed;

/// <inheritdoc/>
public IVoiceChannel VoiceChannel
=> GuildUser.VoiceChannel;
=> GuildUser.Value.VoiceChannel;

/// <inheritdoc/>
public string VoiceSessionId
=> GuildUser.VoiceSessionId;
=> GuildUser.Value.VoiceSessionId;

/// <inheritdoc/>
public bool IsStreaming
=> GuildUser.IsStreaming;
=> GuildUser.Value.IsStreaming;

/// <inheritdoc/>
public bool IsVideoing
=> GuildUser.IsVideoing;
=> GuildUser.Value.IsVideoing;

/// <inheritdoc/>
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)
{
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);
return entity;
}
@@ -150,89 +166,116 @@ namespace Discord.WebSocket
internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, SocketGuildUser owner)
{
// 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;
}

internal void Update(Model model)
{
ThreadJoinedAt = model.JoinTimestamp;
ThreadJoinedAt = model.JoinedAt;
}

/// <inheritdoc/>
public ChannelPermissions GetPermissions(IGuildChannel channel) => GuildUser.GetPermissions(channel);
public ChannelPermissions GetPermissions(IGuildChannel channel) => GuildUser.Value.GetPermissions(channel);

/// <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/>
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/>
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/>
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/>
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/>
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/>
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/>
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/>
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/>
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/>
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/>
public Task RemoveTimeOutAsync(RequestOptions options = null) => GuildUser.RemoveTimeOutAsync(options);
public Task RemoveTimeOutAsync(RequestOptions options = null) => GuildUser.Value.RemoveTimeOutAsync(options);

/// <inheritdoc/>
IThreadChannel IThreadUser.Thread => Thread;
IThreadChannel IThreadUser.Thread => Thread.Value;

/// <inheritdoc/>
IGuild IThreadUser.Guild => Guild;
IGuild IThreadUser.Guild => Guild.Value;

/// <inheritdoc/>
IGuild IGuildUser.Guild => Guild;
IGuild IGuildUser.Guild => Guild.Value;

/// <inheritdoc/>
ulong IGuildUser.GuildId => Guild.Id;
ulong IGuildUser.GuildId => Guild.Value.Id;

/// <inheritdoc/>
GuildPermissions IGuildUser.GuildPermissions => GuildUser.GuildPermissions;
GuildPermissions IGuildUser.GuildPermissions => GuildUser.Value.GuildPermissions;

/// <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 />
string IGuildUser.GetDisplayAvatarUrl(ImageFormat format, ushort size) => GuildUser.GetDisplayAvatarUrl(format, size);
string IGuildUser.GetDisplayAvatarUrl(ImageFormat format, ushort size) => GuildUser.Value.GetDisplayAvatarUrl(format, size);

/// <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>
/// Gets the guild user of this thread user.
/// </summary>
/// <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
}
}

+ 6
- 6
src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs View File

@@ -26,22 +26,22 @@ namespace Discord.WebSocket
/// <inheritdoc />
public override bool IsWebhook => false;
/// <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)
: 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);
entity.Update(state, model);
entity.Update(model);
return entity;
}

public override void Dispose() { }

private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Unknown)";
internal new SocketUnknownUser Clone() => MemberwiseClone() as SocketUnknownUser;
}


+ 67
- 33
src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs View File

@@ -6,8 +6,8 @@ using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Discord.Rest;
using Model = Discord.API.User;
using PresenceModel = Discord.API.Presence;
using Model = Discord.IUserModel;
using PresenceModel = Discord.IPresenceModel;

namespace Discord.WebSocket
{
@@ -15,23 +15,23 @@ namespace Discord.WebSocket
/// Represents a WebSocket-based user.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public abstract class SocketUser : SocketEntity<ulong>, IUser
public abstract class SocketUser : SocketEntity<ulong>, IUser, ICached<Model>
{
/// <inheritdoc />
public abstract bool IsBot { get; internal set; }
public virtual bool IsBot { get; internal set; }
/// <inheritdoc />
public abstract string Username { get; internal set; }
public virtual string Username { get; internal set; }
/// <inheritdoc />
public abstract ushort DiscriminatorValue { get; internal set; }
public virtual ushort DiscriminatorValue { get; internal set; }
/// <inheritdoc />
public abstract string AvatarId { get; internal set; }
public virtual string AvatarId { get; internal set; }
/// <inheritdoc />
public abstract bool IsWebhook { get; }
public virtual bool IsWebhook { get; }
/// <inheritdoc />
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 />
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id);
/// <inheritdoc />
@@ -39,11 +39,11 @@ namespace Discord.WebSocket
/// <inheritdoc />
public string Mention => MentionUtils.MentionUser(Id);
/// <inheritdoc />
public UserStatus Status => Presence.Status;
public UserStatus Status => Presence.Value.Status;
/// <inheritdoc />
public IReadOnlyCollection<ClientType> ActiveClients => Presence.ActiveClients ?? ImmutableHashSet<ClientType>.Empty;
public IReadOnlyCollection<ClientType> ActiveClients => Presence.Value?.ActiveClients ?? ImmutableHashSet<ClientType>.Empty;
/// <inheritdoc />
public IReadOnlyCollection<IActivity> Activities => Presence.Activities ?? ImmutableList<IActivity>.Empty;
public IReadOnlyCollection<IActivity> Activities => Presence.Value?.Activities ?? ImmutableList<IActivity>.Empty;
/// <summary>
/// Gets mutual guilds shared with this user.
/// </summary>
@@ -56,48 +56,50 @@ namespace Discord.WebSocket
internal SocketUser(DiscordSocketClient discord, ulong 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;
if (model.Avatar.IsSpecified && model.Avatar.Value != AvatarId)
if (model.Avatar != AvatarId)
{
AvatarId = model.Avatar.Value;
AvatarId = model.Avatar;
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)
{
DiscriminatorValue = ushort.Parse(model.Discriminator.Value, NumberStyles.None, CultureInfo.InvariantCulture);
DiscriminatorValue = ushort.Parse(model.Discriminator, NumberStyles.None, CultureInfo.InvariantCulture);
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;
}
if (model.Username.IsSpecified && model.Username.Value != Username)
if (model.Username != Username)
{
Username = model.Username.Value;
Username = model.Username;
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;
}

internal virtual void Update(PresenceModel model)
{
Presence ??= new SocketPresence();
Presence.Update(model);
}
public abstract void Dispose();

/// <inheritdoc />
public async Task<IDMChannel> CreateDMChannelAsync(RequestOptions options = null)
@@ -120,5 +122,37 @@ namespace Discord.WebSocket
public override string ToString() => Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode);
private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode)} ({Id}{(IsBot ? ", Bot" : "")})";
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
}
}

+ 6
- 5
src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs View File

@@ -33,8 +33,8 @@ namespace Discord.WebSocket
/// <inheritdoc />
public override bool IsWebhook => true;
/// <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)
: base(guild.Discord, id)
@@ -42,16 +42,17 @@ namespace Discord.WebSocket
Guild = guild;
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);
entity.Update(state, model);
entity.Update(model);
return entity;
}

private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Webhook)";
internal new SocketWebhookUser Clone() => MemberwiseClone() as SocketWebhookUser;
#endregion
public override void Dispose() { }
#endregion

#region IGuildUser
/// <inheritdoc />


+ 70
- 0
src/Discord.Net.WebSocket/Extensions/CacheModelExtensions.cs View File

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

+ 16
- 0
src/Discord.Net.WebSocket/Extensions/EntityCacheExtensions.cs View File

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

+ 59
- 36
src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs View File

@@ -7,86 +7,108 @@ namespace Discord.WebSocket
{
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
if (model.Id.IsSpecified && model.Id.Value == "custom")
if (model.Id != null && model.Id == "custom")
{
return new CustomStatusGame()
{
Type = ActivityType.CustomStatus,
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

#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
{
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,
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,
Type = ActivityType.Listening,
Flags = model.Flags.GetValueOrDefault(),
Flags = model.Flags,
AlbumArt = model.LargeImage,
};
}
#endregion

#region Rich Game
if (model.ApplicationId.IsSpecified)
if (model.ApplicationId.HasValue)
{
ulong appId = model.ApplicationId.Value;
var assets = model.Assets.GetValueOrDefault()?.ToEntity(appId);
return new RichGame
{
ApplicationId = appId,
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

#region Stream Game
if (model.StreamUrl.IsSpecified)
if (model.Url != null)
{
return new StreamingGame(
model.Name,
model.StreamUrl.Value)
model.Url)
{
Flags = model.Flags.GetValueOrDefault(),
Details = model.Details.GetValueOrDefault()
Flags = model.Flags,
Details = model.Details
};
}
#endregion

#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
}

@@ -136,5 +158,6 @@ namespace Discord.WebSocket
{
return new GameTimestamps(model.Start.ToNullable(), model.End.ToNullable());
}
#endregion
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save