Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Kook.Net.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<Project Path="src\Kook.Net.WebSocket\Kook.Net.WebSocket.csproj" />
</Folder>
<Folder Name="/Samples/">
<Project Path="samples/Kook.Net.Samples.NativeAOT/Kook.Net.Samples.NativeAOT.csproj" />
<Project Path="samples\Kook.Net.Samples.Audio\Kook.Net.Samples.Audio.csproj" />
<Project Path="samples\Kook.Net.Samples.CardMarkup\Kook.Net.Samples.CardMarkup.csproj" />
<Project Path="samples\Kook.Net.Samples.Docker\Kook.Net.Samples.Docker.csproj" />
Expand Down
7 changes: 5 additions & 2 deletions props/common.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
<VersionPrefix>0.10.4</VersionPrefix>
<!-- <VersionSuffix>beta2</VersionSuffix>-->
<IsPackable>false</IsPackable>
<IsTrimmable>false</IsTrimmable>
<IsAotCompatible>false</IsAotCompatible>
<LangVersion>latest</LangVersion>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
Expand All @@ -31,6 +29,11 @@
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<DebugType>pdbonly</DebugType>
</PropertyGroup>
<!-- Enable trimming and AOT for .NET 8 and later -->
<PropertyGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">
<IsTrimmable>true</IsTrimmable>
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
<PropertyGroup>
<Version Condition=" '$(VersionSuffix)' == '' ">$(VersionPrefix)</Version>
<Version Condition=" '$(VersionSuffix)' != '' ">$(VersionPrefix)-$(VersionSuffix)</Version>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<Import Project="../../props/samples.props"/>

<PropertyGroup>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PublishAot>true</PublishAot>
<InvariantGlobalization>false</InvariantGlobalization>
<SelfContained>true</SelfContained>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>full</TrimMode>
<IlcOptimizationPreference>Speed</IlcOptimizationPreference>
<IlcGenerateStackTraceData>false</IlcGenerateStackTraceData>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Kook.Net.WebSocket\Kook.Net.WebSocket.csproj" />
</ItemGroup>

</Project>
86 changes: 86 additions & 0 deletions samples/Kook.Net.Samples.NativeAOT/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using Kook;
using Kook.WebSocket;

// Kook.Net NativeAOT 示例
// 此示例演示如何在 NativeAOT 编译模式下使用 Kook.Net

Console.WriteLine("Kook.Net NativeAOT Sample");
Console.WriteLine("========================");
Console.WriteLine();

// 从环境变量或配置文件读取 Token
string? token = Environment.GetEnvironmentVariable("KOOK_TOKEN");
if (string.IsNullOrEmpty(token))
{
Console.WriteLine("Error: KOOK_TOKEN environment variable is not set.");
Console.WriteLine("Please set your bot token:");
Console.WriteLine(" export KOOK_TOKEN=\"your-bot-token-here\"");
return 1;
}

// 创建客户端配置
// 注意:NativeAOT 不支持 Kook.Net.Commands 框架,因为它依赖反射
KookSocketConfig config = new()
{
AlwaysDownloadUsers = false,
MessageCacheSize = 100,
LogLevel = LogSeverity.Info,
StartupCacheFetchMode = StartupCacheFetchMode.Synchronous
};

// 创建客户端
using KookSocketClient client = new(config);

// 设置事件处理器
client.Log += LogAsync;
client.Ready += ReadyAsync;
client.MessageReceived += MessageReceivedAsync;

// 登录并启动
try
{
await client.LoginAsync(TokenType.Bot, token);
await client.StartAsync();

Console.WriteLine("Bot is running. Press Ctrl+C to exit.");

// 保持程序运行
await Task.Delay(Timeout.Infinite);
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
return 1;
}

return 0;

static Task LogAsync(LogMessage msg)
{
Console.WriteLine($"[{msg.Severity}] {msg.Source}: {msg.Message}");
if (msg.Exception != null)
Console.WriteLine($" Exception: {msg.Exception}");
return Task.CompletedTask;
}

static Task ReadyAsync()
{
Console.WriteLine("Bot is ready!");
return Task.CompletedTask;
}

static Task MessageReceivedAsync(SocketMessage message, SocketGuildUser user, SocketTextChannel channel)
{
// 忽略系统消息和 Bot 自己的消息
if (message.Author.IsBot == true || message.Author.IsSystemUser)
return Task.CompletedTask;

// 简单的 ping 命令
if (message.Content.Equals("!ping", StringComparison.OrdinalIgnoreCase))
{
// 在 NativeAOT 模式下,直接回复消息
_ = message.Channel.SendTextAsync("Pong! 🏓");
}

return Task.CompletedTask;
}
59 changes: 59 additions & 0 deletions samples/Kook.Net.Samples.NativeAOT/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Kook.Net NativeAOT 示例

此示例演示如何在 NativeAOT 编译模式下使用 Kook.Net。

## 功能特性

- ✅ REST API 调用(使用 JSON 源生成)
- ✅ WebSocket 连接
- ✅ 事件处理
- ✅ 消息发送和接收
- ❌ Commands 框架(不支持,因为依赖反射)

## 运行示例

### 开发模式运行

```bash
export KOOK_TOKEN="your-bot-token-here"
dotnet run
```

### 发布为 NativeAOT

```bash
dotnet publish -c Release

# 运行已编译的原生可执行文件
export KOOK_TOKEN="your-bot-token-here"
./bin/Release/net8.0/linux-x64/publish/Kook.Net.Samples.NativeAOT
```

## NativeAOT 限制

1. **Commands 框架不可用**: `Kook.Net.Commands` 使用反射进行命令发现和参数绑定,这与 NativeAOT 不兼容。
- 解决方案:手动处理消息并实现命令逻辑

2. **JSON 序列化**: Kook.Net 使用源生成的 `KookJsonSerializerContext` 来支持 NativeAOT。
- 所有 API 模型都已注册用于源生成
- 自定义类型可能需要额外的序列化配置

3. **程序集大小**: NativeAOT 编译的可执行文件会比常规 .NET 应用程序大,但启动速度更快,内存占用更低。

## 测试 Bot

运行 Bot 后,在 Kook 频道中发送 `!ping` 命令,Bot 会回复 "Pong! 🏓"。

## 性能优势

NativeAOT 编译的应用程序具有以下优势:

- **快速启动**: 无需 JIT 编译
- **较低内存占用**: 无需加载整个 .NET 运行时
- **单文件部署**: 可执行文件包含所有依赖项
- **更好的代码保护**: 原生代码更难反编译

## 相关资源

- [.NET NativeAOT 文档](https://learn.microsoft.com/dotnet/core/deploying/native-aot/)
- [Kook.Net 文档](https://kooknet.dev/)
1 change: 0 additions & 1 deletion samples/Kook.Net.Samples.SimpleBot/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Diagnostics;
using System.Text.Json;
using Kook;
using Kook.Rest;
using Kook.WebSocket;
Expand Down
3 changes: 3 additions & 0 deletions src/Kook.Net.Commands/Kook.Net.Commands.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
<IsPackable>true</IsPackable>
<PackageId>Kook.Net.Commands</PackageId>
<Description>The text message based command framework extension for Kook.Net.</Description>
<!-- Commands framework uses extensive reflection and is not AOT compatible -->
<IsAotCompatible>false</IsAotCompatible>
<IsTrimmable>false</IsTrimmable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Kook.Net.Core\Kook.Net.Core.csproj" />
Expand Down
36 changes: 35 additions & 1 deletion src/Kook.Net.Commands/Readers/NullableTypeReader.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,50 @@
using System.Collections.Frozen;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;

namespace Kook.Commands;

internal static class NullableTypeReader
{
private static readonly FrozenDictionary<Type, Func<TypeReader, TypeReader>> PrimitiveNullableReaders =
new Dictionary<Type, Func<TypeReader, TypeReader>>
{
[typeof(bool)] = x => new NullableTypeReader<bool>(x),
[typeof(byte)] = x => new NullableTypeReader<byte>(x),
[typeof(sbyte)] = x => new NullableTypeReader<sbyte>(x),
[typeof(short)] = x => new NullableTypeReader<short>(x),
[typeof(ushort)] = x => new NullableTypeReader<ushort>(x),
[typeof(int)] = x => new NullableTypeReader<int>(x),
[typeof(uint)] = x => new NullableTypeReader<uint>(x),
[typeof(long)] = x => new NullableTypeReader<long>(x),
[typeof(ulong)] = x => new NullableTypeReader<ulong>(x),
[typeof(float)] = x => new NullableTypeReader<float>(x),
[typeof(double)] = x => new NullableTypeReader<double>(x),
[typeof(decimal)] = x => new NullableTypeReader<decimal>(x),
[typeof(DateTime)] = x => new NullableTypeReader<DateTime>(x),
[typeof(DateTimeOffset)] = x => new NullableTypeReader<DateTimeOffset>(x),
[typeof(Guid)] = x => new NullableTypeReader<Guid>(x),
[typeof(TimeSpan)] = x => new NullableTypeReader<TimeSpan>(x),
#if NET6_0_OR_GREATER
[typeof(DateOnly)] = x => new NullableTypeReader<DateOnly>(x),
[typeof(TimeOnly)] = x => new NullableTypeReader<TimeOnly>(x),
#endif
[typeof(char)] = x => new NullableTypeReader<char>(x),
}
.ToFrozenDictionary();

public static TypeReader Create(Type type, TypeReader reader)
{
if (PrimitiveNullableReaders.TryGetValue(type, out Func<TypeReader, TypeReader>? factory))
return factory(reader);
ConstructorInfo constructor = typeof(NullableTypeReader<>).MakeGenericType(type).GetTypeInfo().DeclaredConstructors.First();
return (TypeReader)constructor.Invoke([reader]);
}
}

#if NET6_0_OR_GREATER
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
#endif
internal class NullableTypeReader<T> : TypeReader
where T : struct
{
Expand All @@ -26,7 +60,7 @@ public override async Task<TypeReaderResult> ReadAsync(ICommandContext context,
{
if (string.Equals(input, "null", StringComparison.OrdinalIgnoreCase)
|| string.Equals(input, "nothing", StringComparison.OrdinalIgnoreCase))
return TypeReaderResult.FromSuccess(new T());
return TypeReaderResult.FromSuccess(new T( ));
return await _baseTypeReader.ReadAsync(context, input, services).ConfigureAwait(false);
}
}
34 changes: 34 additions & 0 deletions src/Kook.Net.Commands/Readers/PrimitiveTypeReader.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,48 @@
using System.Collections.Frozen;
using System.Diagnostics.CodeAnalysis;

namespace Kook.Commands;

internal static class PrimitiveTypeReader
{
private static readonly FrozenDictionary<Type, Func<TypeReader>> PrimitiveReaders =
new Dictionary<Type, Func<TypeReader>>
{
[typeof(bool)] = () => new PrimitiveTypeReader<bool>(),
[typeof(byte)] = () => new PrimitiveTypeReader<byte>(),
[typeof(sbyte)] = () => new PrimitiveTypeReader<sbyte>(),
[typeof(short)] = () => new PrimitiveTypeReader<short>(),
[typeof(ushort)] = () => new PrimitiveTypeReader<ushort>(),
[typeof(int)] = () => new PrimitiveTypeReader<int>(),
[typeof(uint)] = () => new PrimitiveTypeReader<uint>(),
[typeof(long)] = () => new PrimitiveTypeReader<long>(),
[typeof(ulong)] = () => new PrimitiveTypeReader<ulong>(),
[typeof(float)] = () => new PrimitiveTypeReader<float>(),
[typeof(double)] = () => new PrimitiveTypeReader<double>(),
[typeof(decimal)] = () => new PrimitiveTypeReader<decimal>(),
[typeof(DateTime)] = () => new PrimitiveTypeReader<DateTime>(),
[typeof(DateTimeOffset)] = () => new PrimitiveTypeReader<DateTimeOffset>(),
[typeof(Guid)] = () => new PrimitiveTypeReader<Guid>(),
#if NET6_0_OR_GREATER
[typeof(DateOnly)] = () => new PrimitiveTypeReader<DateOnly>(),
[typeof(TimeOnly)] = () => new PrimitiveTypeReader<TimeOnly>(),
#endif
[typeof(char)] = () => new PrimitiveTypeReader<char>(),
}
.ToFrozenDictionary();

public static TypeReader? Create(Type type)
{
if (PrimitiveReaders.TryGetValue(type, out Func<TypeReader>? factory))
return factory();
type = typeof(PrimitiveTypeReader<>).MakeGenericType(type);
return Activator.CreateInstance(type) as TypeReader;
}
}

#if NET6_0_OR_GREATER
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
#endif
internal class PrimitiveTypeReader<T> : TypeReader
{
private readonly TryParseDelegate<T> _tryParse;
Expand Down
4 changes: 4 additions & 0 deletions src/Kook.Net.Core/Entities/Guilds/GuildFeatures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ internal void EnsureFeature(GuildFeature feature)
{
if (HasFeature(feature)) return;
GuildFeatures features = this;
#if NET8_0_OR_GREATER
IEnumerable<GuildFeature> values = Enum.GetValues<GuildFeature>();
#else
IEnumerable<GuildFeature> values = Enum.GetValues(typeof(GuildFeature)).Cast<GuildFeature>();
#endif
IEnumerable<GuildFeature> missingValues = values.Where(x => feature.HasFlag(x) && !features.Value.HasFlag(x)).ToList();
throw new InvalidOperationException($"Missing required guild feature{(missingValues.Count() > 1 ? "s" : "")} {string.Join(", ", missingValues.Select(x => x.ToString()))} in order to execute this operation.");
}
Expand Down
34 changes: 34 additions & 0 deletions src/Kook.Net.Core/Entities/Messages/Embeds/NotImplementedEmbed.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;

namespace Kook;

Expand Down Expand Up @@ -35,6 +37,10 @@ internal NotImplementedEmbed(string rawType, JsonNode jsonNode)
/// <param name="options"> 用于反序列化操作的选项。 </param>
/// <typeparam name="T"> 要解析为的具体类型。 </typeparam>
/// <returns> 解析后的嵌入式内容。 </returns>
#if NET5_0_OR_GREATER
[RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a resolving function for AOT applications.")]
[RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use the overload that takes a resolving function for AOT applications.")]
#endif
public T? Resolve<T>(JsonSerializerOptions? options = null)
where T : IEmbed
{
Expand All @@ -47,6 +53,34 @@ internal NotImplementedEmbed(string rawType, JsonNode jsonNode)
return embed;
}

/// <summary>
/// 通过 JSON 反序列化将嵌入式内容解析为具体类型。
/// </summary>
/// <param name="jsonTypeInfo"> 用于反序列化操作的类型信息。 </param>
/// <typeparam name="T"> 要解析为的具体类型。 </typeparam>
/// <returns> 解析后的嵌入式内容。 </returns>
public T? Resolve<T>(JsonTypeInfo jsonTypeInfo)
where T : IEmbed
{
if (jsonTypeInfo is not JsonTypeInfo<T> typedJsonTypeInfo)
throw new ArgumentException($"The provided JsonTypeInfo is not of the expected type {typeof(T).FullName}.", nameof(jsonTypeInfo));
T? embed = JsonNode.Deserialize(typedJsonTypeInfo);
return embed;
}

/// <summary>
/// 通过 JSON 反序列化将嵌入式内容解析为具体类型。
/// </summary>
/// <param name="jsonTypeInfo"> 用于反序列化操作的类型信息。 </param>
/// <typeparam name="T"> 要解析为的具体类型。 </typeparam>
/// <returns> 解析后的嵌入式内容。 </returns>
public T? Resolve<T>(JsonTypeInfo<T> jsonTypeInfo)
where T : IEmbed
{
T? embed = JsonNode.Deserialize(jsonTypeInfo);
return embed;
}

/// <summary>
/// 通过指定的解析函数将嵌入式内容解析为具体类型。
/// </summary>
Expand Down
Loading