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
7 changes: 7 additions & 0 deletions HuaJiBot.NET.sln
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HuaJiBot.NET.CLI", "src\Hua
{729D022F-6631-401D-9378-BC28A5014772} = {729D022F-6631-401D-9378-BC28A5014772}
{7773E08C-333A-4005-B2D1-ACA9057B31F2} = {7773E08C-333A-4005-B2D1-ACA9057B31F2}
{81546AAF-F44A-49D9-8AD4-C90497E020F1} = {81546AAF-F44A-49D9-8AD4-C90497E020F1}
{8EDFA1FA-BBC7-40EF-A1EF-47AB459264B3} = {8EDFA1FA-BBC7-40EF-A1EF-47AB459264B3}
{964D0795-71C8-4E8A-B07E-44967BE2A73A} = {964D0795-71C8-4E8A-B07E-44967BE2A73A}
{AF700E47-CD53-4DC4-9602-AE9606B4F6A5} = {AF700E47-CD53-4DC4-9602-AE9606B4F6A5}
{B1AC8288-59AC-42C9-8669-C61C8781C3C4} = {B1AC8288-59AC-42C9-8669-C61C8781C3C4}
Expand All @@ -34,6 +35,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HuaJiBot.NET.Plugin.Scripti
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HuaJiBot.NET.Adapter.Satori", "src\HuaJiBot.NET.Adapter.Satori\HuaJiBot.NET.Adapter.Satori.csproj", "{8EDFA1FA-BBC7-40EF-A1EF-47AB459264B2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HuaJiBot.NET.Adapter.Kook", "src\HuaJiBot.NET.Adapter.Kook\HuaJiBot.NET.Adapter.Kook.csproj", "{8EDFA1FA-BBC7-40EF-A1EF-47AB459264B3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HuaJiBot.NET.Adapter.Lagrange", "src\HuaJiBot.NET.Adapter.Lagrange\HuaJiBot.NET.Adapter.Lagrange.csproj", "{E678F9C3-5643-47D1-BFAE-ABC4181CE4A9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HuaJiBot.NET.Plugin.AutoReply", "src\HuaJiBot.NET.Plugin.AutoReply\HuaJiBot.NET.Plugin.AutoReply.csproj", "{81546AAF-F44A-49D9-8AD4-C90497E020F1}"
Expand Down Expand Up @@ -88,6 +91,10 @@ Global
{8EDFA1FA-BBC7-40EF-A1EF-47AB459264B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8EDFA1FA-BBC7-40EF-A1EF-47AB459264B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8EDFA1FA-BBC7-40EF-A1EF-47AB459264B2}.Release|Any CPU.Build.0 = Release|Any CPU
{8EDFA1FA-BBC7-40EF-A1EF-47AB459264B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8EDFA1FA-BBC7-40EF-A1EF-47AB459264B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8EDFA1FA-BBC7-40EF-A1EF-47AB459264B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8EDFA1FA-BBC7-40EF-A1EF-47AB459264B3}.Release|Any CPU.Build.0 = Release|Any CPU
{E678F9C3-5643-47D1-BFAE-ABC4181CE4A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E678F9C3-5643-47D1-BFAE-ABC4181CE4A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E678F9C3-5643-47D1-BFAE-ABC4181CE4A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down
17 changes: 17 additions & 0 deletions src/HuaJiBot.NET.Adapter.Kook/HuaJiBot.NET.Adapter.Kook.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\HuaJiBot.NET\HuaJiBot.NET.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Kook.Net" Version="0.0.45-alpha" />
</ItemGroup>

</Project>
266 changes: 266 additions & 0 deletions src/HuaJiBot.NET.Adapter.Kook/KookAdapter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
using HuaJiBot.NET.Bot;
using HuaJiBot.NET.Commands;
using HuaJiBot.NET.Events;
using HuaJiBot.NET.Logger;
using Kook;
using Kook.WebSocket;

namespace HuaJiBot.NET.Adapter.Kook;

public class KookAdapter : BotServiceBase
{
private readonly KookSocketClient _client;
private readonly string _token;

public KookAdapter(string token)
{
_token = token;
_client = new KookSocketClient();
}

public override required ILogger Logger { get; init; }

public override void Reconnect()
{
_ = Task.Run(async () =>
{
if (_client.ConnectionState == ConnectionState.Connected)
{
await _client.StopAsync();
}
await _client.LoginAsync(TokenType.Bot, _token);
await _client.StartAsync();
});
}

public override async Task SetupServiceAsync()
{
await _client.LoginAsync(TokenType.Bot, _token);
await _client.StartAsync();

_client.MessageReceived += OnMessageReceived;
_client.Ready += OnReady;
}

private Task OnReady()
{
Events.CallOnBotLogin(this, new BotLoginEventArgs
{
Service = this,
Accounts = _client.CurrentUser is not null ? [_client.CurrentUser.Id.ToString()] : [],
ClientName = "Kook.Net",
ClientVersion = "0.0.45-alpha"
});
Log($"Kook Bot 登录成功!账号:{_client.CurrentUser?.Username}({_client.CurrentUser?.Id})");
return Task.CompletedTask;
}

private Task OnMessageReceived(Cacheable<IMessage, Guid> cachedMessage, ISocketMessageChannel channel)
{
_ = Task.Run(async () =>
{
try
{
var message = cachedMessage.HasValue ? cachedMessage.Value : await cachedMessage.GetOrDownloadAsync();

// Only process messages from guilds (servers)
if (channel is not ITextChannel textChannel || message is not IUserMessage userMessage)
return;

// Ignore bot messages
if (message.Author.IsBot)
return;

var guild = textChannel.Guild;
var author = message.Author;

// Create command reader for the message
var commandReader = new KookCommandReader(this, message);

// Create group message event args
var eventArgs = new GroupMessageEventArgs(
() => commandReader,
() => ValueTask.FromResult(textChannel.Name)
)
{
Service = this,
RobotId = _client.CurrentUser?.Id.ToString(),
MessageId = message.Id.ToString(),
GroupId = textChannel.Id.ToString(),
SenderId = author.Id.ToString(),
SenderMemberCard = author.Username,
TextMessageLazy = new Lazy<string>(() => message.Content)
};

// Call the group message received event
Events.CallOnGroupMessageReceived(eventArgs);
}
catch (Exception ex)
{
LogError("处理消息时出错", ex);
}
});

return Task.CompletedTask;
}

public override string[] AllRobots => _client.CurrentUser is not null
? [_client.CurrentUser.Id.ToString()]
: [];

public override async Task<string[]> SendGroupMessageAsync(
string? robotId,
string targetGroup,
params SendingMessageBase[] messages
)
{
if (!ulong.TryParse(targetGroup, out var channelId))
throw new ArgumentException("Invalid channel ID format", nameof(targetGroup));

var channel = await _client.GetChannelAsync(channelId) as ITextChannel;
if (channel == null)
throw new ArgumentException("Channel not found", nameof(targetGroup));

var messageIds = new List<string>();

foreach (var message in messages)
{
switch (message)
{
case TextMessage textMessage:
var sentMessage = await channel.SendTextAsync(textMessage.Text);
messageIds.Add(sentMessage.Id.ToString());
break;
case AtMessage atMessage:
if (ulong.TryParse(atMessage.Target, out var userId))
{
var sentAtMessage = await channel.SendTextAsync($"(met){userId}(met)");
messageIds.Add(sentAtMessage.Id.ToString());
}
break;
case ImageMessage imageMessage:
// TODO: Implement image sending
LogDebug($"图片消息暂未实现: {imageMessage.ImagePath}");
break;
case ReplyMessage replyMessage:
// TODO: Implement reply message
LogDebug($"回复消息暂未实现: {replyMessage.MessageId}");
break;
default:
LogDebug($"未支持的消息类型: {message.GetType().Name}");
break;
}
}

return messageIds.ToArray();
}

public override void RecallMessage(string? robotId, string targetGroup, string msgId)
{
_ = Task.Run(async () =>
{
try
{
if (!ulong.TryParse(targetGroup, out var channelId) || !Guid.TryParse(msgId, out var messageId))
return;

var channel = await _client.GetChannelAsync(channelId) as ITextChannel;
if (channel == null)
return;

var message = await channel.GetMessageAsync(messageId);
if (message != null)
{
await message.DeleteAsync();
}
}
catch (Exception ex)
{
LogError("撤回消息失败", ex);
}
});
}

public override void SetGroupName(string? robotId, string targetGroup, string groupName)
{
_ = Task.Run(async () =>
{
try
{
if (!ulong.TryParse(targetGroup, out var channelId))
return;

var channel = await _client.GetChannelAsync(channelId) as ITextChannel;
if (channel == null)
return;

await channel.ModifyAsync(x => x.Name = groupName);
}
catch (Exception ex)
{
LogError("修改频道名称失败", ex);
}
});
}

public override MemberType GetMemberType(string robotId, string targetGroup, string userId)
{
// TODO: Implement member type retrieval based on Kook roles
return MemberType.Member;
}

public override async Task<string[]> FeedbackAt(
string? robotId,
string targetGroup,
string msgId,
string text
)
{
if (!ulong.TryParse(targetGroup, out var channelId))
return [];

var channel = await _client.GetChannelAsync(channelId) as ITextChannel;
if (channel == null)
return [];

try
{
// Try to get the original message to find the sender
if (Guid.TryParse(msgId, out var messageId))
{
var originalMessage = await channel.GetMessageAsync(messageId);
if (originalMessage != null)
{
// Create a mention for the original sender
var mention = $"(met){originalMessage.Author.Id}(met)";
var replyText = $"{mention} {text}";
var sentMessage = await channel.SendTextAsync(replyText);
return [sentMessage.Id.ToString()];
}
}

// Fallback: just send the text without mention
var fallbackMessage = await channel.SendTextAsync(text);
return [fallbackMessage.Id.ToString()];
}
catch (Exception ex)
{
LogError("发送回复消息失败", ex);
return [];
}
}

public override string GetNick(string robotId, string userId)
{
// TODO: Implement nickname retrieval
return "";
}

public override string GetPluginDataPath()
{
var path = Path.GetFullPath(Path.Combine("plugins", "data"));
if (!Directory.Exists(path))
Directory.CreateDirectory(path);
return path;
}
}
72 changes: 72 additions & 0 deletions src/HuaJiBot.NET.Adapter.Kook/KookCommandReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using HuaJiBot.NET.Bot;
using HuaJiBot.NET.Commands;
using Kook;
using static HuaJiBot.NET.Commands.CommonCommandReader;

namespace HuaJiBot.NET.Adapter.Kook;

/// <summary>
/// Kook 指令读取器
/// </summary>
internal class KookCommandReader(BotService service, IMessage message) : CommonCommandReader
{
public override IEnumerable<ReaderEntity> Msg
{
get
{
return Parse();

IEnumerable<ReaderEntity> Parse()
{
// Parse the message content - Kook uses KMarkdown format
var content = message.Content;

if (string.IsNullOrEmpty(content))
yield break;

// Parse mentions in the format (met)userId(met)
var mentionPattern = @"\(met\)(\d+)\(met\)";
var matches = System.Text.RegularExpressions.Regex.Matches(content, mentionPattern);

var processedContent = content;
foreach (System.Text.RegularExpressions.Match match in matches)
{
var userId = match.Groups[1].Value;
processedContent = processedContent.Replace(match.Value, "");

// Get the username if possible
var username = "";
if (message is IUserMessage userMessage && userMessage.MentionedUsers.Any())
{
var mentionedUser = userMessage.MentionedUsers.FirstOrDefault(u => u.Id.ToString() == userId);
username = mentionedUser?.Username ?? "";
}

yield return new ReaderAt(userId, username);
}

// Return the text content without mentions
var trimmedContent = processedContent.Trim();
if (!string.IsNullOrEmpty(trimmedContent))
{
yield return trimmedContent;
}

// Handle quote/reply if reference exists
if (message.Reference.HasValue && message.Reference.Value.MessageId.HasValue)
{
var refMessageId = message.Reference.Value.MessageId.Value.ToString();
yield return new ReaderReply(new CommandReader.ReplyInfo(
messageId: refMessageId,
seqId: null,
senderId: null,
content: null
));
}

// TODO: Handle other Kook-specific message types like cards, embeds, attachments, etc.
// Kook supports rich KMarkdown content that could be parsed here
}
}
}
}
Loading