diff --git a/Flandre.sln b/Flandre.sln index f08c748..e24b411 100644 --- a/Flandre.sln +++ b/Flandre.sln @@ -2,8 +2,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flandre.Core", "src\Flandre.Core\Flandre.Core.csproj", "{347EE5CE-CAB6-4FFA-AAF1-D7AA7BC6AFF7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flandre.ExampleBot", "src\Flandre.ExampleBot\Flandre.ExampleBot.csproj", "{49D31127-B126-4734-B6E4-C1A6A6A41A5E}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flandre.Adapters.Konata", "src\Flandre.Adapters.Konata\Flandre.Adapters.Konata.csproj", "{723E8DD2-55F1-4095-93D7-E6AD600A63EE}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flandre.Core.Tests", "tests\Flandre.Core.Tests\Flandre.Core.Tests.csproj", "{22726A9B-8E54-4513-AB04-61B4A34DDC2A}" @@ -22,10 +20,6 @@ Global {347EE5CE-CAB6-4FFA-AAF1-D7AA7BC6AFF7}.Debug|Any CPU.Build.0 = Debug|Any CPU {347EE5CE-CAB6-4FFA-AAF1-D7AA7BC6AFF7}.Release|Any CPU.ActiveCfg = Release|Any CPU {347EE5CE-CAB6-4FFA-AAF1-D7AA7BC6AFF7}.Release|Any CPU.Build.0 = Release|Any CPU - {49D31127-B126-4734-B6E4-C1A6A6A41A5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {49D31127-B126-4734-B6E4-C1A6A6A41A5E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {49D31127-B126-4734-B6E4-C1A6A6A41A5E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {49D31127-B126-4734-B6E4-C1A6A6A41A5E}.Release|Any CPU.Build.0 = Release|Any CPU {723E8DD2-55F1-4095-93D7-E6AD600A63EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {723E8DD2-55F1-4095-93D7-E6AD600A63EE}.Debug|Any CPU.Build.0 = Debug|Any CPU {723E8DD2-55F1-4095-93D7-E6AD600A63EE}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/src/Flandre.Adapters.Konata/KonataBot.cs b/src/Flandre.Adapters.Konata/KonataBot.cs index 8c0eac1..2c8d6e1 100644 --- a/src/Flandre.Adapters.Konata/KonataBot.cs +++ b/src/Flandre.Adapters.Konata/KonataBot.cs @@ -23,6 +23,9 @@ public sealed class KonataBot : FlandreBot /// public override string Platform => _config.PlatformOverride ?? "konata"; + /// + public override string SelfId { get; } + /// /// Konata 内部 bot /// @@ -52,6 +55,10 @@ internal KonataBot(KonataBotConfig config, Logger logger) Internal = BotFather.Create(config.Konata, config.Device, config.KeyStore); _config = config; + SelfId = string.IsNullOrWhiteSpace(_config.SelfId) + ? Internal.Uin.ToString() + : _config.SelfId; + Internal.OnFriendMessage += InnerOnFriendMessage; Internal.OnGroupMessage += InnerOnGroupMessage; Internal.OnGroupInvite += InnerOnGroupInvite; diff --git a/src/Flandre.Adapters.Mock/MockBot.cs b/src/Flandre.Adapters.Mock/MockBot.cs index 86218d9..d7e2f00 100644 --- a/src/Flandre.Adapters.Mock/MockBot.cs +++ b/src/Flandre.Adapters.Mock/MockBot.cs @@ -15,6 +15,8 @@ public class MockBot : Bot /// public override string Platform => "mock"; + public override string SelfId => _selfId; + private readonly Logger _logger; private readonly string _selfId = Guid.NewGuid().ToString(); @@ -83,4 +85,4 @@ internal void ReceiveMessage(Message message, TaskCompletionSource? OnGuildInvited; public override event BotEventHandler? OnGuildJoinRequested; public override event BotEventHandler? OnFriendRequested; -} \ No newline at end of file +} diff --git a/src/Flandre.Adapters.OneBot/CqCodeParser.cs b/src/Flandre.Adapters.OneBot/CqCodeParser.cs index df06639..23a925d 100644 --- a/src/Flandre.Adapters.OneBot/CqCodeParser.cs +++ b/src/Flandre.Adapters.OneBot/CqCodeParser.cs @@ -30,6 +30,7 @@ public static MessageContent ParseCqMessage(string message) "face" => ParseFace(sections[1..]), "record" => ParseRecord(sections[1..]), "image" => ParseImage(sections[1..]), + "at" => ParseAt(sections[1..]), _ => new TextSegment(code) }); } @@ -145,4 +146,9 @@ private static OneBotImageSegment ParseImage(string[] data) return segment; } + + private static AtSegment ParseAt(string[] data) + { + return new AtSegment(data[0][3..]); // qq=xxx + } } \ No newline at end of file diff --git a/src/Flandre.Adapters.OneBot/GuildBot.cs b/src/Flandre.Adapters.OneBot/GuildBot.cs index ee14d0c..621d0c4 100644 --- a/src/Flandre.Adapters.OneBot/GuildBot.cs +++ b/src/Flandre.Adapters.OneBot/GuildBot.cs @@ -16,6 +16,11 @@ public class OneBotGuildBot : Bot /// public override string Platform => "qqguild"; + public override string SelfId => _selfId; + + private string _selfId = ""; + private bool _isSelfIdSet; + public OneBotGuildInternalBot Internal { get; } private readonly OneBotBot _mainBot; @@ -35,6 +40,12 @@ internal OneBotGuildBot(OneBotBot mainBot) internal void InvokeMessageEvent(OneBotApiGuildMessageEvent e) { + if (!_isSelfIdSet) + { + _selfId = e.SelfId.ToString(); + _isSelfIdSet = true; + } + OnMessageReceived?.Invoke(this, new BotMessageReceivedEvent(new Message { Time = DateTimeOffset.FromUnixTimeSeconds(e.Time).DateTime, diff --git a/src/Flandre.Adapters.OneBot/OneBotBot.cs b/src/Flandre.Adapters.OneBot/OneBotBot.cs index ed95931..7d2b342 100644 --- a/src/Flandre.Adapters.OneBot/OneBotBot.cs +++ b/src/Flandre.Adapters.OneBot/OneBotBot.cs @@ -14,6 +14,9 @@ public abstract class OneBotBot : Bot /// public override string Platform => "onebot"; + /// + public override string SelfId { get; } + internal readonly OneBotGuildBot GuildBot; internal readonly Logger Logger; @@ -24,8 +27,9 @@ public abstract class OneBotBot : Bot protected override Logger GetLogger() => Logger; - internal OneBotBot(Logger logger) + internal OneBotBot(string selfId, Logger logger) { + SelfId = selfId; Internal = new OneBotInternalBot(this); GuildBot = new OneBotGuildBot(this); Logger = logger; diff --git a/src/Flandre.Adapters.OneBot/WebSocketBot.cs b/src/Flandre.Adapters.OneBot/WebSocketBot.cs index 494c1f9..c6287e9 100644 --- a/src/Flandre.Adapters.OneBot/WebSocketBot.cs +++ b/src/Flandre.Adapters.OneBot/WebSocketBot.cs @@ -25,7 +25,7 @@ public class OneBotWebSocketBot : OneBotBot public override event BotEventHandler? OnGuildJoinRequested; public override event BotEventHandler? OnFriendRequested; - internal OneBotWebSocketBot(OneBotBotConfig config, Logger logger) : base(logger) + internal OneBotWebSocketBot(OneBotBotConfig config, Logger logger) : base(config.SelfId, logger) { _config = config; diff --git a/src/Flandre.Core/Attributes/CommandAttribute.cs b/src/Flandre.Core/Attributes/CommandAttribute.cs index 6f5a394..0690a4b 100644 --- a/src/Flandre.Core/Attributes/CommandAttribute.cs +++ b/src/Flandre.Core/Attributes/CommandAttribute.cs @@ -58,8 +58,7 @@ public CommandAttribute(string pattern) } Parameters.Add(info); - - FlandreApp.Logger.Debug(Parameters.Count.ToString()); + parser.SkipSpaces(); } } diff --git a/src/Flandre.Core/Common/Bot.cs b/src/Flandre.Core/Common/Bot.cs index ee941c0..1028e7b 100644 --- a/src/Flandre.Core/Common/Bot.cs +++ b/src/Flandre.Core/Common/Bot.cs @@ -15,6 +15,11 @@ public abstract class Bot /// public abstract string Platform { get; } + /// + /// Bot 自身 ID + /// + public abstract string SelfId { get; } + /// /// 获取 Logger /// diff --git a/src/Flandre.Core/Common/Command.cs b/src/Flandre.Core/Common/Command.cs index 8d6b6d5..b194eaa 100644 --- a/src/Flandre.Core/Common/Command.cs +++ b/src/Flandre.Core/Common/Command.cs @@ -59,9 +59,8 @@ internal Command(Plugin plugin, CommandAttribute info, MethodInfo innerMethod, { var peek = parser.SkipSpaces().Peek(' '); - if (peek.StartsWith("--")) + if (peek.StartsWith("--")) // option (full) { - // option (full) var optName = parser.Read(' ').TrimStart('-'); var optNo = false; @@ -96,9 +95,8 @@ internal Command(Plugin plugin, CommandAttribute info, MethodInfo innerMethod, break; } } - else if (peek.StartsWith('-')) + else if (peek.StartsWith('-')) // option (short) { - // option (short) var opts = parser.Read(' ').TrimStart('-'); parser.SkipSpaces(); @@ -128,25 +126,31 @@ internal Command(Plugin plugin, CommandAttribute info, MethodInfo innerMethod, } } } - else + else // argument { - // argument if (argIndex >= CommandInfo.Parameters.Count) return (args, "参数过多,请检查指令格式。"); var param = CommandInfo.Parameters[argIndex]; - if (param.Type == "string") + switch (param.Type) { - args.Arguments.ArgumentList.Add( - new KeyValuePair(param.Name, parser.ReadQuoted())); - } - else - { - if (CommandUtils.TryParseType(parser.Read(' '), - param.Type, out var result, false)) - args.Arguments.ArgumentList.Add(new KeyValuePair(param.Name, result)); - else return (args, $"参数 {param.Name} 类型错误,应为 {param.Type}。"); + case "string": + args.Arguments.ArgumentList.Add( + new KeyValuePair(param.Name, parser.ReadQuoted())); + break; + + case "text": + args.Arguments.ArgumentList.Add( + new KeyValuePair(param.Name, parser.ReadToEnd())); + break; + + default: + if (CommandUtils.TryParseType(parser.Read(' '), + param.Type, out var result, false)) + args.Arguments.ArgumentList.Add(new KeyValuePair(param.Name, result)); + else return (args, $"参数 {param.Name} 类型错误,应为 {param.Type}。"); + break; } providedArgs.Add(param.Name); diff --git a/src/Flandre.Core/Common/Context.cs b/src/Flandre.Core/Common/Context.cs index 6eb5961..23aae57 100644 --- a/src/Flandre.Core/Common/Context.cs +++ b/src/Flandre.Core/Common/Context.cs @@ -30,4 +30,9 @@ public Context(FlandreApp app, Bot bot) /// Bot 所在平台,等同于 Bot.Platform。 /// public string Platform => Bot.Platform; + + /// + /// Bot 自身 ID,等同于 Bot.SelfId。 + /// + public string SelfId => Bot.SelfId; } \ No newline at end of file diff --git a/src/Flandre.Core/Extensions/AppExtensions.cs b/src/Flandre.Core/Extensions/AppExtensions.cs new file mode 100644 index 0000000..6b72460 --- /dev/null +++ b/src/Flandre.Core/Extensions/AppExtensions.cs @@ -0,0 +1,30 @@ +namespace Flandre.Core.Extensions; + +/// +/// 的扩展方法 +/// +public static class AppExtensions +{ + /// + /// 设置群组代理 Bot(主 Bot) + /// + /// FlandreApp 实例 + /// 平台 + /// 群组 ID + /// Bot.SelfId + public static void SetGuildAssignee(this FlandreApp app, string platform, string guildId, string botId) + { + app.GuildAssignees.AddOrUpdate($"{platform}:{guildId}", botId, (_, _) => botId); + } + + /// + /// 检查群组是否已被代理(已设置主 bot) + /// + /// FlandreApp 实例 + /// 平台 + /// Bot.SelfId + public static bool IsGuildAssigned(this FlandreApp app, string platform, string botId) + { + return app.GuildAssignees.ContainsKey($"{platform}:{botId}"); + } +} \ No newline at end of file diff --git a/src/Flandre.Core/Extensions/ContextExtensions.cs b/src/Flandre.Core/Extensions/ContextExtensions.cs new file mode 100644 index 0000000..5c8f4ba --- /dev/null +++ b/src/Flandre.Core/Extensions/ContextExtensions.cs @@ -0,0 +1,19 @@ +using Flandre.Core.Messaging; + +namespace Flandre.Core.Extensions; + +/// +/// 上下文扩展方法 +/// +public static class ContextExtensions +{ + /// + /// 将 Bot 设置为群组主 Bot + /// + /// 消息上下文 + public static void SetBotAsGuildAssignee(this MessageContext ctx) + { + if (ctx.GuildId is null) return; + ctx.App.SetGuildAssignee(ctx.Platform, ctx.GuildId, ctx.SelfId); + } +} \ No newline at end of file diff --git a/src/Flandre.Core/Flandre.Core.csproj b/src/Flandre.Core/Flandre.Core.csproj index c3514af..4653000 100644 --- a/src/Flandre.Core/Flandre.Core.csproj +++ b/src/Flandre.Core/Flandre.Core.csproj @@ -3,7 +3,7 @@ Flandre.Core $(Title) - 0.5.0 + 0.6.0 $(PackageVersion) b1acksoil 跨平台,低耦合的聊天机器人框架,一次编写,多处运行。 diff --git a/src/Flandre.Core/FlandreApp.cs b/src/Flandre.Core/FlandreApp.cs index f3a8937..1338bdd 100644 --- a/src/Flandre.Core/FlandreApp.cs +++ b/src/Flandre.Core/FlandreApp.cs @@ -1,4 +1,5 @@ -using System.Runtime.CompilerServices; +using System.Collections.Concurrent; +using System.Runtime.CompilerServices; using Flandre.Core.Common; using Flandre.Core.Events.App; using Flandre.Core.Events.Plugin; @@ -17,7 +18,7 @@ namespace Flandre.Core; /// /// 应用基本框架 /// -public class FlandreApp +public partial class FlandreApp { internal readonly List Adapters = new(); internal readonly List> Middlewares = new(); @@ -32,6 +33,8 @@ public class FlandreApp internal Dictionary ShortcutMap { get; } = new(); + internal ConcurrentDictionary GuildAssignees { get; } = new(); + /// /// App 配置 /// @@ -253,22 +256,14 @@ void CatchAndLog(Action action) => Task.Run(() => } } + // 群组 assignee 检查 + Use(CheckAssigneeMiddleware); + // 插件 OnMessageReceived - Use((ctx, next) => - { - foreach (var plugin in Plugins) - plugin.OnMessageReceived(ctx); - next(); - }); + Use(PluginMessageReceivedMiddleware); // 指令解析中间件 - Use((ctx, next) => - { - var content = ParseCommand(ctx); - if (content is not null) - ctx.Bot.SendMessage(ctx.Message, content); - next(); - }); + Use(ParseCommandMiddleware); } private void ExecuteMiddlewares(MessageContext ctx, int index) @@ -305,7 +300,8 @@ private void ExecuteMiddlewares(MessageContext ctx, int index) } var commandStr = ctx.Message.GetText().Trim(); - if (commandStr == Config.CommandPrefix) return null; + if (commandStr == Config.CommandPrefix) + return null; var parser = new StringParser(commandStr); @@ -314,7 +310,9 @@ private void ExecuteMiddlewares(MessageContext ctx, int index) if (ShortcutMap.TryGetValue(root, out var command)) return ParseAndInvoke(command, parser); - + if (!string.IsNullOrWhiteSpace(Config.CommandPrefix) + && !root.StartsWith(Config.CommandPrefix)) + return null; root = root.TrimStart(Config.CommandPrefix); parser.SkipSpaces(); diff --git a/src/Flandre.Core/Messaging/MessageContent.cs b/src/Flandre.Core/Messaging/MessageContent.cs index f33bf10..b91f53b 100644 --- a/src/Flandre.Core/Messaging/MessageContent.cs +++ b/src/Flandre.Core/Messaging/MessageContent.cs @@ -8,7 +8,7 @@ namespace Flandre.Core.Messaging; /// public class MessageContent : IEnumerable { - private readonly IEnumerable _segments; + internal IEnumerable Segments { get; } /// /// 使用消息段构造消息内容 @@ -16,7 +16,7 @@ public class MessageContent : IEnumerable /// public MessageContent(IEnumerable segments) { - _segments = segments; + Segments = segments; } /// @@ -25,7 +25,7 @@ public MessageContent(IEnumerable segments) /// 消息段类型 public TSegment? GetSegment() where TSegment : MessageSegment { - return (TSegment?)_segments.FirstOrDefault(segment => segment is TSegment); + return (TSegment?)Segments.FirstOrDefault(segment => segment is TSegment); } /// @@ -34,7 +34,7 @@ public MessageContent(IEnumerable segments) /// 消息段类型 public IEnumerable GetSegments() where TSegment : MessageSegment { - return _segments.Where(segment => segment is TSegment).Cast(); + return Segments.Where(segment => segment is TSegment).Cast(); } /// @@ -83,7 +83,7 @@ public static implicit operator MessageContent(string text) /// public IEnumerator GetEnumerator() { - return _segments.GetEnumerator(); + return Segments.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() diff --git a/src/Flandre.Core/Middlewares.cs b/src/Flandre.Core/Middlewares.cs new file mode 100644 index 0000000..7000dd1 --- /dev/null +++ b/src/Flandre.Core/Middlewares.cs @@ -0,0 +1,38 @@ +using Flandre.Core.Messaging; +using Flandre.Core.Messaging.Segments; + +namespace Flandre.Core; + +public partial class FlandreApp +{ + internal void CheckAssigneeMiddleware(MessageContext ctx, Action next) + { + var segment = ctx.Message.Content.Segments.FirstOrDefault(); + if (segment is AtSegment ats) + { + if (ats.UserId == ctx.SelfId) + next(); + } + // 如果没找到群组的 assignee + else if (!GuildAssignees.TryGetValue($"{ctx.Platform}:{ctx.GuildId}", out var assignee)) + next(); + // 如果找到了群组的 assignee,且是自己 + else if (ctx.SelfId == assignee) + next(); + } + + internal void PluginMessageReceivedMiddleware(MessageContext ctx, Action next) + { + foreach (var plugin in Plugins) + plugin.OnMessageReceived(ctx); + next(); + } + + internal void ParseCommandMiddleware(MessageContext ctx, Action next) + { + var content = ParseCommand(ctx); + if (content is not null) + ctx.Bot.SendMessage(ctx.Message, content); + next(); + } +} \ No newline at end of file diff --git a/tests/Flandre.Core.Tests/FlandreAppTests.cs b/tests/Flandre.Core.Tests/FlandreAppTests.cs index 9508ad7..4dbb16e 100644 --- a/tests/Flandre.Core.Tests/FlandreAppTests.cs +++ b/tests/Flandre.Core.Tests/FlandreAppTests.cs @@ -1,6 +1,8 @@ using Flandre.Adapters.Mock; using Flandre.Core.Utils; +// ReSharper disable StringLiteralTypo + namespace Flandre.Core.Tests; public class FlandreAppTests @@ -32,6 +34,10 @@ public void TestFlandreApp() content = friendClient.SendForReply("test1 -bo 111.444 --no-trueopt false").Result; Assert.Equal("arg1: False opt: 111.444 b: True t: False", content?.GetText()); + + content = friendClient.SendForReply("test2 some text aaa bbb ").Result; + Assert.Equal("some text aaa bbb", + content?.GetText()); app.Stop(); }; @@ -50,7 +56,7 @@ public void TestShortcutAndAlias() app.OnAppReady += (_, _) => { - Assert.Equal(5, app.CommandMap.Count); + Assert.Equal(6, app.CommandMap.Count); Assert.Equal(2, app.ShortcutMap.Count); Assert.NotNull(app.CommandMap.GetValueOrDefault("test1")); diff --git a/tests/Flandre.Core.Tests/TestPlugin.cs b/tests/Flandre.Core.Tests/TestPlugin.cs index 25bba26..ed07b8e 100644 --- a/tests/Flandre.Core.Tests/TestPlugin.cs +++ b/tests/Flandre.Core.Tests/TestPlugin.cs @@ -29,6 +29,13 @@ public static MessageContent OnTest1(MessageContext ctx, ParsedArgs args) var trueOpt = args.GetOption("trueopt"); return $"arg1: {arg1} opt: {opt} b: {boolOpt} t: {trueOpt}"; } + + [Command("test2 ")] + public static MessageContent OnTest2(MessageContext _, ParsedArgs args) + { + var arg1 = args.GetArgument("arg1"); + return arg1; + } [Command("sub.test")] [Alias("sssuuubbb")]