diff --git a/Wolfringo.Commands/CommandContextExtensions.cs b/Wolfringo.Commands/CommandContextExtensions.cs index 686b098..b779867 100644 --- a/Wolfringo.Commands/CommandContextExtensions.cs +++ b/Wolfringo.Commands/CommandContextExtensions.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -104,11 +105,16 @@ public static async Task ReplyTextAsync(this ICommandContext conte IEnumerable urlLinks = options.AutoDetectWebsiteLinks ? UrlLinkDetectionHelper.FindLinks(text) : Enumerable.Empty(); - IEnumerable embeds = options.EnableGroupLinkPreview && groupLinks.Any() - ? new IChatEmbed[] { new GroupPreviewChatEmbed(groupLinks.First().GroupID) } - : Enumerable.Empty(); - ChatMessage message = new ChatMessage(context.Message.IsGroupMessage ? context.Message.RecipientID : context.Message.SenderID.Value, context.Message.IsGroupMessage, ChatMessageTypes.Text, Encoding.UTF8.GetBytes(text), new ChatMessageFormatting(groupLinks, urlLinks), embeds); + IEnumerable embeds = await ChatEmbedBuilder.BuildEmbedsAsync(context.Client, groupLinks, urlLinks, options, cancellationToken).ConfigureAwait(false); + + ChatMessage message = new ChatMessage( + context.Message.IsGroupMessage ? context.Message.RecipientID : context.Message.SenderID.Value, + context.Message.IsGroupMessage, + ChatMessageTypes.Text, + Encoding.UTF8.GetBytes(text), + new ChatMessageFormatting(groupLinks, urlLinks), + embeds); return await context.Client.SendAsync(message, cancellationToken).ConfigureAwait(false); } diff --git a/Wolfringo.Commands/CommandsService.cs b/Wolfringo.Commands/CommandsService.cs index e16462f..8fb6282 100644 --- a/Wolfringo.Commands/CommandsService.cs +++ b/Wolfringo.Commands/CommandsService.cs @@ -103,6 +103,7 @@ public CommandsService(IServiceProvider services, CommandsOptions options) /// Options for commands service. /// A logger to add to the services. If null, logging will be disabled. /// A with default services added. + [Obsolete] protected static IServiceProvider BuildDefaultServiceProvider(IWolfClient client, CommandsOptions options, ILogger log = null) { if (client == null) @@ -234,25 +235,11 @@ private async Task ExecuteAsyncInternal(ICommandContext context, throw new InvalidOperationException($"This {this.GetType().Name} is not started yet"); using (CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, this._cts.Token)) { - IEnumerable> commandsCopy; - // copying might not be the fastest thing to do, but it'll ensure that commands won't be changed out of the lock - // locking only copying to prevent hangs if user is not careful with their commands, while still preventing race conditions with StartAsync - await this._lock.WaitAsync(cts.Token).ConfigureAwait(false); - try - { - // order commands by priority - commandsCopy = this._commands.OrderByDescending(kvp => kvp.Key.GetPriority()); - } - finally - { - this._lock.Release(); - } - using (IServiceScope serviceScope = this._services.CreateScope()) { IServiceProvider services = serviceScope.ServiceProvider; - foreach (KeyValuePair commandKvp in commandsCopy) + foreach (KeyValuePair commandKvp in this._commands.OrderByDescending(kvp => kvp.Key.GetPriority())) { ICommandInstanceDescriptor command = commandKvp.Key; ICommandInstance instance = commandKvp.Value; diff --git a/Wolfringo.Commands/Wolfringo.Commands.csproj b/Wolfringo.Commands/Wolfringo.Commands.csproj index 7b4d01f..68ce5d0 100644 --- a/Wolfringo.Commands/Wolfringo.Commands.csproj +++ b/Wolfringo.Commands/Wolfringo.Commands.csproj @@ -3,7 +3,7 @@ netstandard2.0;net9.0 TehGM.Wolfringo.Commands - 2.1.5 + 2.2.0 TehGM Wolfringo Commands System for Wolfringo library. @@ -15,12 +15,11 @@ git wolf palringo wolfringo pal bot client commands true - 2.0.0.0 - 2.0.0.0 + 2.2.0.0 + 2.2.0.0 -- Add possibility for the caller to decide whether to pre-process group and website links when sending a message; -- System.Threading.Lock will now be used instead of old lock objects on .NET 9; -- Fix group links not generating preview due to WOLF protocol settings requiring embeds as separate list of IDs; + - Add support for Link and Image preview embeds; + - Chat Messages and retrieved chat histories will now have Embeds property properly populated; NUGET_README.md diff --git a/Wolfringo.Core/AssemblyConfiguration.cs b/Wolfringo.Core/AssemblyConfiguration.cs new file mode 100644 index 0000000..6c96872 --- /dev/null +++ b/Wolfringo.Core/AssemblyConfiguration.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Wolfringo.Commands")] +[assembly: InternalsVisibleTo("Wolfringo.Utilities")] \ No newline at end of file diff --git a/Wolfringo.Core/Messages/Embeds/ImagePreviewChatEmbed.cs b/Wolfringo.Core/Messages/Embeds/ImagePreviewChatEmbed.cs new file mode 100644 index 0000000..1607127 --- /dev/null +++ b/Wolfringo.Core/Messages/Embeds/ImagePreviewChatEmbed.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; +using System; + +namespace TehGM.Wolfringo.Messages.Embeds +{ + /// Represent a chat embed for image link. + public class ImagePreviewChatEmbed : IChatEmbed + { + /// + public string EmbedType => "imagePreview"; + + /// ID of the group to embed. + [JsonProperty("url")] + public string URL { get; } + + /// Creates a new link preview embed with an image. + /// Link to preview. + public ImagePreviewChatEmbed(string url) + { + if (string.IsNullOrWhiteSpace(url)) + throw new ArgumentException("Image URL is required", nameof(url)); + + this.URL = url; + } + } +} diff --git a/Wolfringo.Core/Messages/Embeds/LinkPreviewChatEmbed.cs b/Wolfringo.Core/Messages/Embeds/LinkPreviewChatEmbed.cs new file mode 100644 index 0000000..32a5cf9 --- /dev/null +++ b/Wolfringo.Core/Messages/Embeds/LinkPreviewChatEmbed.cs @@ -0,0 +1,33 @@ +using Newtonsoft.Json; +using System; + +namespace TehGM.Wolfringo.Messages.Embeds +{ + /// Represent a chat embed for website link. + public class LinkPreviewChatEmbed : IChatEmbed + { + /// + public string EmbedType => "linkPreview"; + + /// Title of the webpage. + [JsonProperty("title")] + public string Title { get; set; } + /// URL of the webpage. + [JsonProperty("url")] + public string URL { get; } + + /// Creates a new link preview embed. + /// Title of the webpage. + /// Link to preview. + public LinkPreviewChatEmbed(string title, string url) + { + if (string.IsNullOrWhiteSpace(title)) + throw new ArgumentException("Link title is required", nameof(title)); + if (string.IsNullOrWhiteSpace(url)) + throw new ArgumentException("Link URL is required", nameof(url)); + + this.Title = title; + this.URL = url; + } + } +} diff --git a/Wolfringo.Core/Messages/IChatMessage.cs b/Wolfringo.Core/Messages/IChatMessage.cs index fcb3e8c..472d2fc 100644 --- a/Wolfringo.Core/Messages/IChatMessage.cs +++ b/Wolfringo.Core/Messages/IChatMessage.cs @@ -50,4 +50,12 @@ public interface IRawDataMessage [JsonIgnore] IReadOnlyCollection RawData { get; } } + + /// Represents a message containing embeds. + public interface IChatEmbedContainer + { + /// Visual embeds attached to this chat message. + [JsonProperty("embeds", DefaultValueHandling = DefaultValueHandling.Ignore, NullValueHandling = NullValueHandling.Ignore)] + IEnumerable Embeds { get; } + } } diff --git a/Wolfringo.Core/Messages/Serialization/IChatEmbedDeserializer.cs b/Wolfringo.Core/Messages/Serialization/IChatEmbedDeserializer.cs new file mode 100644 index 0000000..26febb7 --- /dev/null +++ b/Wolfringo.Core/Messages/Serialization/IChatEmbedDeserializer.cs @@ -0,0 +1,102 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using TehGM.Wolfringo.Messages.Embeds; +using TehGM.Wolfringo.Messages.Serialization.Internal; + +namespace TehGM.Wolfringo.Messages.Serialization +{ + /// Maps values present in 'type' property of chat embeds and provides means to deserialize them into a . + public interface IChatEmbedDeserializer + { + /// Attempts to retrieve the embed type. + /// Value of 'type' property. + /// Resulting type if mapped. + /// Whether a type for given embed type was found. + bool TryGetChatEmbedType(string type, out Type result); + /// Maps chat embed type to an implementation type. + /// Implementation type of the chat embed. + /// Type of the chat embed. + void MapChatEmbedType(string type) where T : IChatEmbed; + /// deserializes all embeds from message body. + /// Embeds are deserialized if body has 'embeds' array. Otherwise an empty enumerable is returned. + /// Body of the message. + /// Enumerable of chat embeds. + IEnumerable DeserializeEmbeds(JObject messageBody); + /// Populates chat message's embeds. + /// Chat message. + /// Deserialized embeds. + void PopulateMessageEmbeds(ref ChatMessage message, IEnumerable embeds); + } + + /// + public class ChatEmbedDeserializer : IChatEmbedDeserializer + { + internal static ChatEmbedDeserializer Instance { get; } = new ChatEmbedDeserializer(); + + private readonly Dictionary _registeredEmbedTypes = new Dictionary() + { + ["linkPreview"] = typeof(LinkPreviewChatEmbed), + ["imagePreview"] = typeof(ImagePreviewChatEmbed), + ["groupPreview"] = typeof(GroupPreviewChatEmbed) + }; + + /// + public bool TryGetChatEmbedType(string type, out Type result) + { + return this._registeredEmbedTypes.TryGetValue(type, out result); + } + + /// + public void MapChatEmbedType(string type) where T : IChatEmbed + { + this._registeredEmbedTypes[type] = typeof(T); + } + + /// + public IEnumerable DeserializeEmbeds(JObject messageBody) + { + if (messageBody == null || !messageBody.ContainsKey("embeds") || !(messageBody["embeds"] is JArray embeds)) + yield break; + + foreach (JToken embed in embeds) + { + if (!(embed is JObject embedObject) || !embedObject.ContainsKey("type")) + continue; + + string embedType = embedObject["type"].ToObject(); + if (string.IsNullOrWhiteSpace(embedType)) + continue; + + if (this.TryGetChatEmbedType(embedType, out Type type)) + { + yield return (IChatEmbed)embedObject.ToObject(type, SerializationHelper.DefaultSerializer); + } + } + } + + /// + public void PopulateMessageEmbeds(ref ChatMessage message, IEnumerable embeds) + { + if (message == null) + throw new ArgumentNullException(nameof(message)); + if (embeds?.Any() != true) + return; + + if (message.Embeds == null || !(message.Embeds is ICollection embedCollection) || embedCollection.IsReadOnly) + throw new InvalidOperationException($"Cannot populate embeds in {message.GetType().Name} as the collection is read only or null"); + embedCollection.Clear(); + + // if it's a list, we can do it in a more performant way + if (message.Embeds is List embedList) + embedList.AddRange(embeds); + // otherwise do it one by one + else + { + foreach (IChatEmbed e in embeds) + embedCollection.Add(e); + } + } + } +} diff --git a/Wolfringo.Core/Messages/Serialization/MessageSerializerProvider.cs b/Wolfringo.Core/Messages/Serialization/MessageSerializerProvider.cs index 319437e..6ea9223 100644 --- a/Wolfringo.Core/Messages/Serialization/MessageSerializerProvider.cs +++ b/Wolfringo.Core/Messages/Serialization/MessageSerializerProvider.cs @@ -12,11 +12,6 @@ public class MessageSerializerProvider : ISerializerProvider this.Options.FallbackSerializer; /// Instance of options used by this provider. protected MessageSerializerProviderOptions Options { get; } -#if NET9_0_OR_GREATER - private readonly Lock _lock = new Lock(); -#else - private readonly object _lock = new object(); -#endif /// Create a new instance of default provider. /// Options to use with this provider. @@ -31,11 +26,8 @@ public MessageSerializerProvider() : this(new MessageSerializerProviderOptions() /// public IMessageSerializer GetSerializer(string key) { - lock (this._lock) - { - this.Options.Serializers.TryGetValue(key, out IMessageSerializer result); - return result; - } + this.Options.Serializers.TryGetValue(key, out IMessageSerializer result); + return result; } } } diff --git a/Wolfringo.Core/Messages/Serialization/MessageSerializerProviderOptions.cs b/Wolfringo.Core/Messages/Serialization/MessageSerializerProviderOptions.cs index 2381af0..e76b3ff 100644 --- a/Wolfringo.Core/Messages/Serialization/MessageSerializerProviderOptions.cs +++ b/Wolfringo.Core/Messages/Serialization/MessageSerializerProviderOptions.cs @@ -7,14 +7,23 @@ namespace TehGM.Wolfringo.Messages.Serialization /// public class MessageSerializerProviderOptions { + /// Deserializer of chat embeds that will be used by serializers. + public IChatEmbedDeserializer ChatEmbedDeserializer { get; } = Serialization.ChatEmbedDeserializer.Instance; + /// Fallback serializer that can be used if key has no mapped serializer. /// Note that this serializer cannot be used for deserialization, and will be used only for serialization. /// Defaults to , where T is . public IMessageSerializer FallbackSerializer { get; set; } = new DefaultMessageSerializer(); /// Map for event type and assigned message serializer. + // TODO: 3.0: refactor to work on System.Type so it can be used with IServiceProvider instead + public IDictionary Serializers { get; set; } + + /// Initializes a new instance of options using default values. + public MessageSerializerProviderOptions() + { #pragma warning disable CS0618 // Type or member is obsolete - public IDictionary Serializers { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase) + this.Serializers = new Dictionary(StringComparer.OrdinalIgnoreCase) { // default ones { MessageEventNames.Welcome, new DefaultMessageSerializer() }, @@ -61,6 +70,7 @@ public class MessageSerializerProviderOptions { MessageEventNames.TipGroupSubscribe, new DefaultMessageSerializer() }, { MessageEventNames.TipSummary, new DefaultMessageSerializer() }, { MessageEventNames.TipDetail, new DefaultMessageSerializer() }, + { MessageEventNames.MetadataUrl, new DefaultMessageSerializer() }, // group join and leave { MessageEventNames.GroupMemberAdd, new GroupJoinLeaveMessageSerializer() }, { MessageEventNames.GroupMemberDelete, new GroupJoinLeaveMessageSerializer() }, @@ -68,7 +78,7 @@ public class MessageSerializerProviderOptions { MessageEventNames.SubscriberContactAdd, new ContactAddDeleteMessageSerializer() }, { MessageEventNames.SubscriberContactDelete, new ContactAddDeleteMessageSerializer() }, // chat message - { MessageEventNames.MessageSend, new ChatMessageSerializer() }, + { MessageEventNames.MessageSend, new ChatMessageSerializer(ChatEmbedDeserializer) }, // tip add { MessageEventNames.TipAdd, new TipAddMessageSerializer() }, // entity updates @@ -77,6 +87,7 @@ public class MessageSerializerProviderOptions { MessageEventNames.GroupProfileUpdate, new GroupEditMessageSerializer() }, { MessageEventNames.MessageUpdate, new ChatUpdateMessageSerializer() } }; - } #pragma warning restore CS0618 // Type or member is obsolete + } + } } diff --git a/Wolfringo.Core/Messages/Serialization/ResponseSerializerProvider.cs b/Wolfringo.Core/Messages/Serialization/ResponseSerializerProvider.cs index c28537c..02a06b0 100644 --- a/Wolfringo.Core/Messages/Serialization/ResponseSerializerProvider.cs +++ b/Wolfringo.Core/Messages/Serialization/ResponseSerializerProvider.cs @@ -13,11 +13,6 @@ public class ResponseSerializerProvider : ISerializerProvider this.Options.FallbackSerializer; /// Instance of options used by this provider. protected ResponseSerializerProviderOptions Options { get; } -#if NET9_0_OR_GREATER - private readonly Lock _lock = new Lock(); -#else - private readonly object _lock = new object(); -#endif /// Creates default response serializer map. /// Instance of options to use with this provider. @@ -32,11 +27,8 @@ public ResponseSerializerProvider() : this(new ResponseSerializerProviderOptions /// public IResponseSerializer GetSerializer(Type key) { - lock (this._lock) - { - this.Options.Serializers.TryGetValue(key, out IResponseSerializer result); - return result; - } + this.Options.Serializers.TryGetValue(key, out IResponseSerializer result); + return result; } } } diff --git a/Wolfringo.Core/Messages/Serialization/ResponseSerializerProviderOptions.cs b/Wolfringo.Core/Messages/Serialization/ResponseSerializerProviderOptions.cs index ba52d67..d4c53a7 100644 --- a/Wolfringo.Core/Messages/Serialization/ResponseSerializerProviderOptions.cs +++ b/Wolfringo.Core/Messages/Serialization/ResponseSerializerProviderOptions.cs @@ -8,12 +8,15 @@ namespace TehGM.Wolfringo.Messages.Serialization /// public class ResponseSerializerProviderOptions { + /// Deserializer of chat embeds that will be used by serializers. + public static IChatEmbedDeserializer ChatEmbedDeserializer { get; } = Serialization.ChatEmbedDeserializer.Instance; + /// Default serializer. /// This serializer is used for multiple mappings in default . protected static IResponseSerializer DefaultSerializer { get; } = new DefaultResponseSerializer(); /// Default chat history serializer. /// This serializer is used for multiple mappings in default . - protected static IResponseSerializer DefaultHistorySerializer { get; } = new ChatHistoryResponseSerializer(); + protected static IResponseSerializer DefaultHistorySerializer { get; } /// Fallback serializer that can be used if key has no mapped serializer. /// Note that this serializer cannot be used for deserialization, and will be used only for serialization. @@ -21,7 +24,18 @@ public class ResponseSerializerProviderOptions public IResponseSerializer FallbackSerializer { get; set; } = DefaultSerializer; /// Map for response type and assigned response serializer. - public IDictionary Serializers { get; set; } = new Dictionary() + // TODO: 3.0: refactor to work on System.Type so it can be used with IServiceProvider instead + public IDictionary Serializers { get; set; } + + static ResponseSerializerProviderOptions() + { + DefaultHistorySerializer = new ChatHistoryResponseSerializer(ChatEmbedDeserializer); + } + + /// Initializes a new instance of options using default values. + public ResponseSerializerProviderOptions() + { + this.Serializers = new Dictionary() { // default { typeof(WolfResponse), DefaultSerializer }, @@ -42,6 +56,7 @@ public class ResponseSerializerProviderOptions { typeof(UserCharmsListResponse), DefaultSerializer }, { typeof(EntitiesSubscribeResponse), DefaultSerializer }, { typeof(TipSummaryResponse), DefaultSerializer }, + { typeof(UrlMetadataResponse), DefaultSerializer }, // group stats { typeof(GroupStatisticsResponse), new GroupStatisticsResponseSerializer() }, // group profile @@ -56,5 +71,7 @@ public class ResponseSerializerProviderOptions // tips { typeof(TipDetailsResponse), new TipDetailsResponseSerializer() }, }; + + } } } diff --git a/Wolfringo.Core/Messages/Serialization/Serializers/ChatHistoryResponseSerializer.cs b/Wolfringo.Core/Messages/Serialization/Serializers/ChatHistoryResponseSerializer.cs index ad4e231..3ee33d8 100644 --- a/Wolfringo.Core/Messages/Serialization/Serializers/ChatHistoryResponseSerializer.cs +++ b/Wolfringo.Core/Messages/Serialization/Serializers/ChatHistoryResponseSerializer.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json.Linq; using System; +using System.Collections.Generic; using System.Linq; using TehGM.Wolfringo.Messages.Responses; using TehGM.Wolfringo.Messages.Serialization.Internal; @@ -12,16 +13,39 @@ public class ChatHistoryResponseSerializer : DefaultResponseSerializer, IRespons { private static readonly Type _chatHistoryResponseType = typeof(ChatHistoryResponse); + private readonly IChatEmbedDeserializer _chatEmbedDeserializer; + + /// Initializes a new serializer for chat history responses. + /// Deserializer of chat embeds to use. + public ChatHistoryResponseSerializer(IChatEmbedDeserializer chatEmbedDeserializer) + { + this._chatEmbedDeserializer = chatEmbedDeserializer; + } + + /// Initializes a new serializer for chat history responses. + /// This uses default for deserializing embeds. + public ChatHistoryResponseSerializer() + : this(ChatEmbedDeserializer.Instance) { } + /// public override IWolfResponse Deserialize(Type responseType, SerializedMessageData responseData) { - // first deserialize the json message - ChatHistoryResponse result = (ChatHistoryResponse)base.Deserialize(responseType, responseData); - - // then assign binary data to each of the messages JArray responseBody = GetResponseJson(responseData)["body"] as JArray; if (responseBody == null) throw new ArgumentException("Chat history response requires to have a body property that is a JSON array", nameof(responseData)); + + Dictionary> extractedEmbeds = new Dictionary>(responseBody.Count); + foreach (JToken responseChatMessage in responseBody) + { + if (!(responseChatMessage is JObject chatMessageObject)) + continue; + + IEnumerable embeds = this._chatEmbedDeserializer.DeserializeEmbeds(chatMessageObject).ToArray(); + chatMessageObject.Remove("embeds"); + extractedEmbeds.Add(chatMessageObject, embeds); + } + + ChatHistoryResponse result = (ChatHistoryResponse)base.Deserialize(responseType, responseData); foreach (JToken responseChatMessage in responseBody) { WolfTimestamp msgTimestamp = responseChatMessage["timestamp"].ToObject(SerializationHelper.DefaultSerializer); @@ -32,6 +56,10 @@ public override IWolfResponse Deserialize(Type responseType, SerializedMessageDa int binaryIndex = numProp.ToObject(SerializationHelper.DefaultSerializer); SerializationHelper.PopulateMessageRawData(ref msg, responseData.BinaryMessages.ElementAt(binaryIndex)); } + if (msg is ChatMessage chatMsg && extractedEmbeds.TryGetValue(responseChatMessage, out IEnumerable embeds)) + { + this._chatEmbedDeserializer.PopulateMessageEmbeds(ref chatMsg, embeds); + } } return result; diff --git a/Wolfringo.Core/Messages/Serialization/Serializers/ChatMessageSerializer.cs b/Wolfringo.Core/Messages/Serialization/Serializers/ChatMessageSerializer.cs index cb9c4b1..0e0a729 100644 --- a/Wolfringo.Core/Messages/Serialization/Serializers/ChatMessageSerializer.cs +++ b/Wolfringo.Core/Messages/Serialization/Serializers/ChatMessageSerializer.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json.Linq; using System; +using System.Collections.Generic; using System.Linq; using TehGM.Wolfringo.Messages.Serialization.Internal; @@ -10,17 +11,38 @@ namespace TehGM.Wolfringo.Messages.Serialization /// it inserts data placeholders into payload body object, and adds it to serialized message's binary data. public class ChatMessageSerializer : IMessageSerializer { + private readonly IChatEmbedDeserializer _embedDeserializer; + + /// Initializes a new serializer for chat messages. + /// Deserializer of chat embeds to use. + public ChatMessageSerializer(IChatEmbedDeserializer chatEmbedDeserializer) + { + this._embedDeserializer = chatEmbedDeserializer; + } + + /// Initializes a new serializer for chat messages. + /// This uses default for deserializing embeds. + public ChatMessageSerializer() + : this(ChatEmbedDeserializer.Instance) { } + /// public IWolfMessage Deserialize(string eventName, SerializedMessageData messageData) { - // deserialize message - Type msgType = GetMessageType(messageData.Payload["body"]); + JObject body = messageData.Payload["body"] as JObject; + + // extracting separate deserialization of embeds is anything but ideal + // however we do it instead of using a custom converter so when new embed types are added by the (really unstable, may I add) Wolf protocol, it won't start throwing left and right + IEnumerable embeds = this._embedDeserializer.DeserializeEmbeds(body).ToList(); + body.Remove("embeds"); + + Type msgType = GetMessageType(body); IChatMessage result = (IChatMessage)messageData.Payload.ToObject(msgType, SerializationHelper.DefaultSerializer); messageData.Payload.FlattenCommonProperties(result, SerializationHelper.DefaultSerializer); - // parse and populate binary data if (messageData.BinaryMessages?.Any() == true) SerializationHelper.PopulateMessageRawData(ref result, messageData.BinaryMessages.First()); + if (result is ChatMessage container) + this._embedDeserializer.PopulateMessageEmbeds(ref container, embeds); return result; } diff --git a/Wolfringo.Core/Messages/Types/ChatMessage.cs b/Wolfringo.Core/Messages/Types/ChatMessage.cs index 9427ffc..ebef7f2 100644 --- a/Wolfringo.Core/Messages/Types/ChatMessage.cs +++ b/Wolfringo.Core/Messages/Types/ChatMessage.cs @@ -11,7 +11,7 @@ namespace TehGM.Wolfringo.Messages /// Uses as response type. /// [ResponseType(typeof(ChatResponse))] - public class ChatMessage : IChatMessage, IWolfMessage, IRawDataMessage + public class ChatMessage : IChatMessage, IWolfMessage, IRawDataMessage, IChatEmbedContainer { /// /// Equals to . @@ -76,6 +76,7 @@ public class ChatMessage : IChatMessage, IWolfMessage, IRawDataMessage protected ChatMessage() { this.RawData = new List(); + this.Embeds = new List(0); } /// Creates a message instance. diff --git a/Wolfringo.Core/Messages/Types/UrlMetadataMessage.cs b/Wolfringo.Core/Messages/Types/UrlMetadataMessage.cs index 28189cf..f430b51 100644 --- a/Wolfringo.Core/Messages/Types/UrlMetadataMessage.cs +++ b/Wolfringo.Core/Messages/Types/UrlMetadataMessage.cs @@ -1,14 +1,22 @@ using System; +using System.Collections.Generic; using Newtonsoft.Json; using TehGM.Wolfringo.Messages.Responses; namespace TehGM.Wolfringo.Messages { /// A message for requesting metadata about a link as seen by WOLF servers. - /// Uses as response type. + /// Uses as response type. [ResponseType(typeof(UrlMetadataResponse))] - public class UrlMetadataMessage : IWolfMessage + public class UrlMetadataMessage : IWolfMessage, IHeadersWolfMessage { + /// + [JsonIgnore] + public IDictionary Headers { get; } = new Dictionary() + { + { "version", 2 } + }; + /// /// Equals to . [JsonIgnore] diff --git a/Wolfringo.Core/Utilities/ChatMessageSendingOptions.cs b/Wolfringo.Core/Utilities/ChatMessageSendingOptions.cs index e788c1c..d3e09ce 100644 --- a/Wolfringo.Core/Utilities/ChatMessageSendingOptions.cs +++ b/Wolfringo.Core/Utilities/ChatMessageSendingOptions.cs @@ -4,8 +4,14 @@ public sealed class ChatMessageSendingOptions { /// Default options. - /// These options will automatically detect everything - group and website links, and enable group preview embeds. + /// These options will automatically detect everything - group and website links, and enable all preview embeds. public static ChatMessageSendingOptions Default { get; } = new ChatMessageSendingOptions(); + /// Options that enable detection but disable embeds. + /// These options will automatically detect group and website links, but disable all preview embeds. + public static ChatMessageSendingOptions DisableEmbeds { get; } = new ChatMessageSendingOptions() { EnableGroupLinkPreview = false, EnableImageLinkPreview = false, EnableWebsiteLinkPreview = false }; + /// Options that will disable all automatic detection. + /// These options will not detect group and website links. + public static ChatMessageSendingOptions DisableLinkDetection { get; } = new ChatMessageSendingOptions() { AutoDetectGroupLinks = false, AutoDetectWebsiteLinks = false, EnableGroupLinkPreview = false, EnableImageLinkPreview = false, EnableWebsiteLinkPreview = false }; #if NET5_0_OR_GREATER /// Whether group links should be automatically detected. @@ -15,6 +21,12 @@ public sealed class ChatMessageSendingOptions /// Whether group preview should be displayed as embed. /// Doesn't have any effect if is set to false. public bool EnableGroupLinkPreview { get; init; } + /// Whether link preview should be displayed as embed. + /// Doesn't have any effect if is set to false. + public bool EnableWebsiteLinkPreview { get; init; } + /// Whether image preview should be displayed as embed. + /// Doesn't have any effect if is set to false. + public bool EnableImageLinkPreview { get; init; } #else /// Whether group links should be automatically detected. public bool AutoDetectGroupLinks { get; set; } @@ -23,6 +35,12 @@ public sealed class ChatMessageSendingOptions /// Whether group preview should be displayed as embed. /// Doesn't have any effect if is set to false. public bool EnableGroupLinkPreview { get; set; } + /// Whether website preview should be displayed as embed. + /// Doesn't have any effect if is set to false. + public bool EnableWebsiteLinkPreview { get; set; } + /// Whether image preview should be displayed as embed. + /// Doesn't have any effect if is set to false. + public bool EnableImageLinkPreview { get; set; } #endif /// Creates a new instance of options, with all flags set to true. @@ -31,6 +49,8 @@ public ChatMessageSendingOptions() this.AutoDetectGroupLinks = true; this.AutoDetectWebsiteLinks = true; this.EnableGroupLinkPreview = true; + this.EnableWebsiteLinkPreview = true; + this.EnableImageLinkPreview = true; } } } diff --git a/Wolfringo.Core/Utilities/Internal/ChatEmbedBuilder.cs b/Wolfringo.Core/Utilities/Internal/ChatEmbedBuilder.cs new file mode 100644 index 0000000..f884888 --- /dev/null +++ b/Wolfringo.Core/Utilities/Internal/ChatEmbedBuilder.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Threading; +using TehGM.Wolfringo.Messages; +using TehGM.Wolfringo.Messages.Embeds; +using TehGM.Wolfringo.Messages.Responses; + +namespace TehGM.Wolfringo.Utilities.Internal +{ + /// Internal utility class for generating from extracted URL metadata. + /// Class created to avoid duplication between Commands system extensions and Sender utility extensions. + internal static class ChatEmbedBuilder + { + public static async Task> BuildEmbedsAsync(IWolfClient client, IEnumerable groupLinks, IEnumerable urlLinks, ChatMessageSendingOptions options, CancellationToken cancellationToken) + { + if (options.EnableGroupLinkPreview && groupLinks.Any()) + return new IChatEmbed[] { new GroupPreviewChatEmbed(groupLinks.First().GroupID) }; + + if ((options.EnableWebsiteLinkPreview || options.EnableImageLinkPreview) && urlLinks.Any()) + { + try + { + string url = urlLinks.First().URL; + UrlMetadataResponse metadataResponse = await client.SendAsync(new UrlMetadataMessage(url), cancellationToken).ConfigureAwait(false); + if (metadataResponse.IsSuccess() && !metadataResponse.IsBlacklisted) + { + if (metadataResponse.ImageSize != null && options.EnableImageLinkPreview) + return new IChatEmbed[] { new ImagePreviewChatEmbed(metadataResponse.ImageURL) }; + if (options.EnableWebsiteLinkPreview) + { + string linkTitle = string.IsNullOrWhiteSpace(metadataResponse.Title) ? "-" : metadataResponse.Title; + return new IChatEmbed[] { new LinkPreviewChatEmbed(linkTitle, url) }; + } + } + } + catch (MessageSendingException) { } + } + + return Enumerable.Empty(); + } + } +} diff --git a/Wolfringo.Core/WolfClient.cs b/Wolfringo.Core/WolfClient.cs index ee91d00..5b529df 100644 --- a/Wolfringo.Core/WolfClient.cs +++ b/Wolfringo.Core/WolfClient.cs @@ -142,6 +142,7 @@ public WolfClient(ILoggerFactory logFactory) /// Builds default service provider. Used to temporarily support obsolete non-builder constructors. /// A logger to add to the services. If null, logging will be disabled. /// A with default services added. + [Obsolete] protected static IServiceProvider BuildDefaultServiceProvider(ILogger log = null) { // add all required services diff --git a/Wolfringo.Core/Wolfringo.Core.csproj b/Wolfringo.Core/Wolfringo.Core.csproj index 578470f..af92682 100644 --- a/Wolfringo.Core/Wolfringo.Core.csproj +++ b/Wolfringo.Core/Wolfringo.Core.csproj @@ -3,7 +3,7 @@ netstandard2.0;netstandard2.1;net5.0;net9.0 TehGM.Wolfringo - 2.1.5 + 2.2.0 TehGM Wolfringo https://github.com/TehGM/Wolfringo @@ -11,23 +11,25 @@ A .NET Library for WOLF (previously Palringo) https://wolfringo.tehgm.net true - 2.1.0.0 - 2.1.0.0 + 2.2.0.0 + 2.2.0.0 MIT git false wolf palringo wolfringo pal bot client wolfringo_logo.png -- Add ChatMessageSendingOptions class which is used by the supporting packages; -- Fix odd cases of errors when WOLF protocol wrongly sends member kicked notification as "leave" message; -- Support group preview message embeds; -- ChatMessage ID is no longer obsolete; -- SlowModeRate added to chat responses; -- Structured logs will now include binary message values; -- Minor thread safety improvements in socket client implementation; -- Minor performance in socket client on .NET 5 and later; -- System.Threading.Lock will now be used instead of old lock objects on .NET 9; + - Add IChatEmbedDeserializer to allow deserialization of chat embeds; + - Add support for Link and Image preview embeds; + - Chat Messages and retrieved chat histories will now have Embeds property properly populated; + - ChatMessageSendingOptions provide 2 predefined statics: DisableEmbeds and DisableLinkDetection; + - Update UrlMetadataMessage to use headers with version 2; + - Fix an error when a message containing an embed is received; + - Fix link metadata message not having a serializer registered with the provider; + - ChatMessageSerializer now has a constructor that takes a IChatEmbedDeserializer; + - ChatHistoryResponseSerializer now has a constructor that takes a IChatEmbedDeserializer; + - Removed unnecessary locks in CommandsSystem, MessageSerializerProvider and ResponseSerializerProvider; + - BuildDefaultServiceProvider protected methods are marked as obsolete in WolfClient and CommandsSystem; NUGET_README.md diff --git a/Wolfringo.Hosting/Wolfringo.Hosting.csproj b/Wolfringo.Hosting/Wolfringo.Hosting.csproj index 34c259b..fbd8575 100644 --- a/Wolfringo.Hosting/Wolfringo.Hosting.csproj +++ b/Wolfringo.Hosting/Wolfringo.Hosting.csproj @@ -2,7 +2,7 @@ netstandard2.0;netcoreapp3.0;net9.0 - 2.1.5 + 2.2.0 TehGM Copyright (c) 2020 TehGM https://wolfringo.tehgm.net @@ -17,7 +17,7 @@ 2.0.0.0 2.0.0.0 -- System.Threading.Lock will now be used instead of old lock objects on .NET 9; +Compatibility patch for Wolfringo 2.2.0. NUGET_README.md diff --git a/Wolfringo.Utilities/Sender.cs b/Wolfringo.Utilities/Sender.cs index 9cae8c5..284d00a 100644 --- a/Wolfringo.Utilities/Sender.cs +++ b/Wolfringo.Utilities/Sender.cs @@ -1041,9 +1041,8 @@ public static async Task SendPrivateTextMessageAsync(this IWolfCli IEnumerable urlLinks = options.AutoDetectWebsiteLinks ? UrlLinkDetectionHelper.FindLinks(text) : Enumerable.Empty(); - IEnumerable embeds = options.EnableGroupLinkPreview && groupLinks.Any() - ? new IChatEmbed[] { new GroupPreviewChatEmbed(groupLinks.First().GroupID) } - : Enumerable.Empty(); + + IEnumerable embeds = await ChatEmbedBuilder.BuildEmbedsAsync(client, groupLinks, urlLinks, options, cancellationToken).ConfigureAwait(false); ChatMessage message = new ChatMessage(userID, false, ChatMessageTypes.Text, Encoding.UTF8.GetBytes(text), new ChatMessageFormatting(groupLinks, urlLinks), embeds); return await client.SendAsync(message, cancellationToken).ConfigureAwait(false); @@ -1063,14 +1062,15 @@ public static async Task SendGroupTextMessageAsync(this IWolfClien IEnumerable urlLinks = options.AutoDetectWebsiteLinks ? UrlLinkDetectionHelper.FindLinks(text) : Enumerable.Empty(); - IEnumerable embeds = options.EnableGroupLinkPreview && groupLinks.Any() - ? new IChatEmbed[] { new GroupPreviewChatEmbed(groupLinks.First().GroupID) } - : Enumerable.Empty(); + + IEnumerable embeds = await ChatEmbedBuilder.BuildEmbedsAsync(client, groupLinks, urlLinks, options, cancellationToken).ConfigureAwait(false); ChatMessage message = new ChatMessage(groupID, true, ChatMessageTypes.Text, Encoding.UTF8.GetBytes(text), new ChatMessageFormatting(groupLinks, urlLinks), embeds); return await client.SendAsync(message, cancellationToken).ConfigureAwait(false); } + + /// Sends a private image message. /// Client to send the message with. /// ID of user to send the message to. diff --git a/Wolfringo.Utilities/Wolfringo.Utilities.csproj b/Wolfringo.Utilities/Wolfringo.Utilities.csproj index 8c83f7e..840ee8f 100644 --- a/Wolfringo.Utilities/Wolfringo.Utilities.csproj +++ b/Wolfringo.Utilities/Wolfringo.Utilities.csproj @@ -6,10 +6,10 @@ TehGM Copyright (c) 2020 TehGM https://wolfringo.tehgm.net - 2.1.5 + 2.2.0 https://github.com/TehGM/Wolfringo - 2.1.0.0 - 2.1.0.0 + 2.2.0.0 + 2.2.0.0 MIT true git @@ -22,8 +22,8 @@ true -- Add possibility for the caller to decide whether to pre-process group and website links when sending a message; -- Fix group links not generating preview due to WOLF protocol settings requiring embeds as separate list of IDs; + - Add support for Link and Image preview embeds; + - Chat Messages and retrieved chat histories will now have Embeds property properly populated; diff --git a/Wolfringo/Wolfringo.nuspec b/Wolfringo/Wolfringo.nuspec index 4889078..b598a24 100644 --- a/Wolfringo/Wolfringo.nuspec +++ b/Wolfringo/Wolfringo.nuspec @@ -2,7 +2,7 @@ Wolfringo - 2.1.5 + 2.2.0 Wolfringo TehGM TehGM @@ -16,22 +16,24 @@ true Copyright (c) 2020 TehGM -- Add possibility for the caller to decide whether to pre-process group and website links when sending a message; -- Fix odd cases of errors when WOLF protocol wrongly sends member kicked notification as "leave" message; -- Fix group links not generating preview due to WOLF protocol settings requiring embeds as separate list of IDs; -- ChatMessage ID is no longer obsolete; -- SlowModeRate added to chat responses; -- Structured logs will now include binary message values; -- Minor thread safety improvements in socket client implementation; -- Minor performance in socket client on .NET 5 and later; -- System.Threading.Lock will now be used instead of old lock objects on .NET 9; + - Add IChatEmbedDeserializer to allow deserialization of chat embeds; + - Add support for Link and Image preview embeds; + - Chat Messages and retrieved chat histories will now have Embeds property properly populated; + - ChatMessageSendingOptions provide 2 predefined statics: DisableEmbeds and DisableLinkDetection; + - Update UrlMetadataMessage to use headers with version 2; + - Fix an error when a message containing an embed is received; + - Fix link metadata message not having a serializer registered with the provider; + - ChatMessageSerializer now has a constructor that takes a IChatEmbedDeserializer; + - ChatHistoryResponseSerializer now has a constructor that takes a IChatEmbedDeserializer; + - Removed unnecessary locks in CommandsSystem, MessageSerializerProvider and ResponseSerializerProvider; + - BuildDefaultServiceProvider protected methods are marked as obsolete in WolfClient and CommandsSystem; - - + + - +