Skip to content

Commit

Permalink
Release v2.2.0
Browse files Browse the repository at this point in the history
Merge pull request #31 from TehGM/dev
  • Loading branch information
TehGM authored Feb 19, 2025
2 parents 990f323 + 1c89f73 commit 8e84a3f
Show file tree
Hide file tree
Showing 24 changed files with 405 additions and 99 deletions.
14 changes: 10 additions & 4 deletions Wolfringo.Commands/CommandContextExtensions.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -104,11 +105,16 @@ public static async Task<ChatResponse> ReplyTextAsync(this ICommandContext conte
IEnumerable<ChatMessageFormatting.LinkData> urlLinks = options.AutoDetectWebsiteLinks
? UrlLinkDetectionHelper.FindLinks(text)
: Enumerable.Empty<ChatMessageFormatting.LinkData>();
IEnumerable<IChatEmbed> embeds = options.EnableGroupLinkPreview && groupLinks.Any()
? new IChatEmbed[] { new GroupPreviewChatEmbed(groupLinks.First().GroupID) }
: Enumerable.Empty<IChatEmbed>();

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<IChatEmbed> 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<ChatResponse>(message, cancellationToken).ConfigureAwait(false);
}

Expand Down
17 changes: 2 additions & 15 deletions Wolfringo.Commands/CommandsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ public CommandsService(IServiceProvider services, CommandsOptions options)
/// <param name="options">Options for commands service.</param>
/// <param name="log">A logger to add to the services. If null, logging will be disabled.</param>
/// <returns>A <see cref="IServiceProvider"/> with default services added.</returns>
[Obsolete]
protected static IServiceProvider BuildDefaultServiceProvider(IWolfClient client, CommandsOptions options, ILogger log = null)
{
if (client == null)
Expand Down Expand Up @@ -234,25 +235,11 @@ private async Task<ICommandResult> ExecuteAsyncInternal(ICommandContext context,
throw new InvalidOperationException($"This {this.GetType().Name} is not started yet");
using (CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, this._cts.Token))
{
IEnumerable<KeyValuePair<ICommandInstanceDescriptor, ICommandInstance>> 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<ICommandInstanceDescriptor, ICommandInstance> commandKvp in commandsCopy)
foreach (KeyValuePair<ICommandInstanceDescriptor, ICommandInstance> commandKvp in this._commands.OrderByDescending(kvp => kvp.Key.GetPriority()))
{
ICommandInstanceDescriptor command = commandKvp.Key;
ICommandInstance instance = commandKvp.Value;
Expand Down
11 changes: 5 additions & 6 deletions Wolfringo.Commands/Wolfringo.Commands.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net9.0</TargetFrameworks>
<RootNamespace>TehGM.Wolfringo.Commands</RootNamespace>
<Version>2.1.5</Version>
<Version>2.2.0</Version>
<Authors>TehGM</Authors>
<Product>Wolfringo</Product>
<Description>Commands System for Wolfringo library.</Description>
Expand All @@ -15,12 +15,11 @@
<RepositoryType>git</RepositoryType>
<PackageTags>wolf palringo wolfringo pal bot client commands</PackageTags>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<AssemblyVersion>2.0.0.0</AssemblyVersion>
<FileVersion>2.0.0.0</FileVersion>
<AssemblyVersion>2.2.0.0</AssemblyVersion>
<FileVersion>2.2.0.0</FileVersion>
<PackageReleaseNotes>
- 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;
</PackageReleaseNotes>
<PackageReadmeFile>NUGET_README.md</PackageReadmeFile>
</PropertyGroup>
Expand Down
4 changes: 4 additions & 0 deletions Wolfringo.Core/AssemblyConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Wolfringo.Commands")]
[assembly: InternalsVisibleTo("Wolfringo.Utilities")]
26 changes: 26 additions & 0 deletions Wolfringo.Core/Messages/Embeds/ImagePreviewChatEmbed.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Newtonsoft.Json;
using System;

namespace TehGM.Wolfringo.Messages.Embeds
{
/// <summary>Represent a chat embed for image link.</summary>
public class ImagePreviewChatEmbed : IChatEmbed
{
/// <inheritdoc/>
public string EmbedType => "imagePreview";

/// <summary>ID of the group to embed.</summary>
[JsonProperty("url")]
public string URL { get; }

/// <summary>Creates a new link preview embed with an image.</summary>
/// <param name="url">Link to preview.</param>
public ImagePreviewChatEmbed(string url)
{
if (string.IsNullOrWhiteSpace(url))
throw new ArgumentException("Image URL is required", nameof(url));

this.URL = url;
}
}
}
33 changes: 33 additions & 0 deletions Wolfringo.Core/Messages/Embeds/LinkPreviewChatEmbed.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Newtonsoft.Json;
using System;

namespace TehGM.Wolfringo.Messages.Embeds
{
/// <summary>Represent a chat embed for website link.</summary>
public class LinkPreviewChatEmbed : IChatEmbed
{
/// <inheritdoc/>
public string EmbedType => "linkPreview";

/// <summary>Title of the webpage.</summary>
[JsonProperty("title")]
public string Title { get; set; }
/// <summary>URL of the webpage.</summary>
[JsonProperty("url")]
public string URL { get; }

/// <summary>Creates a new link preview embed.</summary>
/// <param name="title">Title of the webpage.</param>
/// <param name="url">Link to preview.</param>
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;
}
}
}
8 changes: 8 additions & 0 deletions Wolfringo.Core/Messages/IChatMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,12 @@ public interface IRawDataMessage
[JsonIgnore]
IReadOnlyCollection<byte> RawData { get; }
}

/// <summary>Represents a message containing embeds.</summary>
public interface IChatEmbedContainer
{
/// <summary>Visual embeds attached to this chat message.</summary>
[JsonProperty("embeds", DefaultValueHandling = DefaultValueHandling.Ignore, NullValueHandling = NullValueHandling.Ignore)]
IEnumerable<IChatEmbed> Embeds { get; }
}
}
102 changes: 102 additions & 0 deletions Wolfringo.Core/Messages/Serialization/IChatEmbedDeserializer.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>Maps values present in 'type' property of chat embeds and provides means to deserialize them into a <see cref="ChatMessage"/>.</summary>
public interface IChatEmbedDeserializer
{
/// <summary>Attempts to retrieve the embed type.</summary>
/// <param name="type">Value of 'type' property.</param>
/// <param name="result">Resulting type if mapped.</param>
/// <returns>Whether a type for given embed type was found.</returns>
bool TryGetChatEmbedType(string type, out Type result);
/// <summary>Maps chat embed type to an implementation type.</summary>
/// <typeparam name="T">Implementation type of the chat embed.</typeparam>
/// <param name="type">Type of the chat embed.</param>
void MapChatEmbedType<T>(string type) where T : IChatEmbed;
/// <summary>deserializes all embeds from message body.</summary>
/// <remarks>Embeds are deserialized if body has 'embeds' array. Otherwise an empty enumerable is returned.</remarks>
/// <param name="messageBody">Body of the message.</param>
/// <returns>Enumerable of chat embeds.</returns>
IEnumerable<IChatEmbed> DeserializeEmbeds(JObject messageBody);
/// <summary>Populates chat message's embeds.</summary>
/// <param name="message">Chat message.</param>
/// <param name="embeds">Deserialized embeds.</param>
void PopulateMessageEmbeds(ref ChatMessage message, IEnumerable<IChatEmbed> embeds);
}

/// <inheritdoc/>
public class ChatEmbedDeserializer : IChatEmbedDeserializer
{
internal static ChatEmbedDeserializer Instance { get; } = new ChatEmbedDeserializer();

private readonly Dictionary<string, Type> _registeredEmbedTypes = new Dictionary<string, Type>()
{
["linkPreview"] = typeof(LinkPreviewChatEmbed),
["imagePreview"] = typeof(ImagePreviewChatEmbed),
["groupPreview"] = typeof(GroupPreviewChatEmbed)
};

/// <inheritdoc/>
public bool TryGetChatEmbedType(string type, out Type result)
{
return this._registeredEmbedTypes.TryGetValue(type, out result);
}

/// <inheritdoc/>
public void MapChatEmbedType<T>(string type) where T : IChatEmbed
{
this._registeredEmbedTypes[type] = typeof(T);
}

/// <inheritdoc/>
public IEnumerable<IChatEmbed> 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<string>();
if (string.IsNullOrWhiteSpace(embedType))
continue;

if (this.TryGetChatEmbedType(embedType, out Type type))
{
yield return (IChatEmbed)embedObject.ToObject(type, SerializationHelper.DefaultSerializer);
}
}
}

/// <inheritdoc/>
public void PopulateMessageEmbeds(ref ChatMessage message, IEnumerable<IChatEmbed> embeds)
{
if (message == null)
throw new ArgumentNullException(nameof(message));
if (embeds?.Any() != true)
return;

if (message.Embeds == null || !(message.Embeds is ICollection<IChatEmbed> 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<IChatEmbed> embedList)
embedList.AddRange(embeds);
// otherwise do it one by one
else
{
foreach (IChatEmbed e in embeds)
embedCollection.Add(e);
}
}
}
}
12 changes: 2 additions & 10 deletions Wolfringo.Core/Messages/Serialization/MessageSerializerProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,6 @@ public class MessageSerializerProvider : ISerializerProvider<string, IMessageSer
public IMessageSerializer FallbackSerializer => this.Options.FallbackSerializer;
/// <summary>Instance of options used by this provider.</summary>
protected MessageSerializerProviderOptions Options { get; }
#if NET9_0_OR_GREATER
private readonly Lock _lock = new Lock();
#else
private readonly object _lock = new object();
#endif

/// <summary>Create a new instance of default provider.</summary>
/// <param name="options">Options to use with this provider.</param>
Expand All @@ -31,11 +26,8 @@ public MessageSerializerProvider() : this(new MessageSerializerProviderOptions()
/// <inheritdoc/>
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,23 @@ namespace TehGM.Wolfringo.Messages.Serialization
/// <seealso cref="MessageSerializerProvider"/>
public class MessageSerializerProviderOptions
{
/// <summary>Deserializer of chat embeds that will be used by serializers.</summary>
public IChatEmbedDeserializer ChatEmbedDeserializer { get; } = Serialization.ChatEmbedDeserializer.Instance;

/// <summary>Fallback serializer that can be used if key has no mapped serializer.</summary>
/// <remarks><para>Note that this serializer cannot be used for deserialization, and will be used only for serialization.</para>
/// <para>Defaults to <see cref="DefaultMessageSerializer{T}"/>, where T is <see cref="IWolfMessage"/>.</para></remarks>
public IMessageSerializer FallbackSerializer { get; set; } = new DefaultMessageSerializer<IWolfMessage>();

/// <summary>Map for event type and assigned message serializer.</summary>
// TODO: 3.0: refactor to work on System.Type so it can be used with IServiceProvider instead
public IDictionary<string, IMessageSerializer> Serializers { get; set; }

/// <summary>Initializes a new instance of options using default values.</summary>
public MessageSerializerProviderOptions()
{
#pragma warning disable CS0618 // Type or member is obsolete
public IDictionary<string, IMessageSerializer> Serializers { get; set; } = new Dictionary<string, IMessageSerializer>(StringComparer.OrdinalIgnoreCase)
this.Serializers = new Dictionary<string, IMessageSerializer>(StringComparer.OrdinalIgnoreCase)
{
// default ones
{ MessageEventNames.Welcome, new DefaultMessageSerializer<WelcomeEvent>() },
Expand Down Expand Up @@ -61,14 +70,15 @@ public class MessageSerializerProviderOptions
{ MessageEventNames.TipGroupSubscribe, new DefaultMessageSerializer<SubscribeToGroupTipsMessage>() },
{ MessageEventNames.TipSummary, new DefaultMessageSerializer<TipSummaryMessage>() },
{ MessageEventNames.TipDetail, new DefaultMessageSerializer<TipDetailsMessage>() },
{ MessageEventNames.MetadataUrl, new DefaultMessageSerializer<UrlMetadataMessage>() },
// group join and leave
{ MessageEventNames.GroupMemberAdd, new GroupJoinLeaveMessageSerializer<GroupJoinMessage>() },
{ MessageEventNames.GroupMemberDelete, new GroupJoinLeaveMessageSerializer<GroupLeaveMessage>() },
// contact add and delete
{ MessageEventNames.SubscriberContactAdd, new ContactAddDeleteMessageSerializer<ContactAddMessage>() },
{ MessageEventNames.SubscriberContactDelete, new ContactAddDeleteMessageSerializer<ContactDeleteMessage>() },
// chat message
{ MessageEventNames.MessageSend, new ChatMessageSerializer() },
{ MessageEventNames.MessageSend, new ChatMessageSerializer(ChatEmbedDeserializer) },
// tip add
{ MessageEventNames.TipAdd, new TipAddMessageSerializer() },
// entity updates
Expand All @@ -77,6 +87,7 @@ public class MessageSerializerProviderOptions
{ MessageEventNames.GroupProfileUpdate, new GroupEditMessageSerializer<GroupUpdateMessage>() },
{ MessageEventNames.MessageUpdate, new ChatUpdateMessageSerializer() }
};
}
#pragma warning restore CS0618 // Type or member is obsolete
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,6 @@ public class ResponseSerializerProvider : ISerializerProvider<Type, IResponseSer
public IResponseSerializer FallbackSerializer => this.Options.FallbackSerializer;
/// <summary>Instance of options used by this provider.</summary>
protected ResponseSerializerProviderOptions Options { get; }
#if NET9_0_OR_GREATER
private readonly Lock _lock = new Lock();
#else
private readonly object _lock = new object();
#endif

/// <summary>Creates default response serializer map.</summary>
/// <param name="options">Instance of options to use with this provider.</param>
Expand All @@ -32,11 +27,8 @@ public ResponseSerializerProvider() : this(new ResponseSerializerProviderOptions
/// <inheritdoc/>
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;
}
}
}
Loading

0 comments on commit 8e84a3f

Please sign in to comment.