diff --git a/README.NuGet.md b/README.NuGet.md index e7ebdd1..518cea5 100644 --- a/README.NuGet.md +++ b/README.NuGet.md @@ -27,7 +27,8 @@ Flandre 自设计之初就是为了跨平台,对聊天平台的结构进行抽 目前已经实现的适配器: -- [Flandre.Adapters.Konata](https://github.com/FlandreDevs/Flandre/blob/dev/src/Flandre.Adapters.Konata/README.md) - QQ 协议适配,基于 [Konata.Core](https://github.com/KonataDev/Konata.Core) +- [Flandre.Adapters.Konata](https://github.com/FlandreDevs/Flandre/blob/dev/src/Flandre.Adapters.Konata/README.md) - QQ + 协议适配,基于 [Konata.Core](https://github.com/KonataDev/Konata.Core) ### 指令系统 @@ -100,13 +101,15 @@ class ExamplePlugin2 : Plugin } ``` -这个插件包含一条有两个参数的指令,类型都为 `string`,其中 `foo` 为必选参数,`bar` 为可选参数。如果调用指令时未提供可选参数,参数将被初始化为类型默认值;如果为提供必选参数,bot 将向其发送一条提示信息并停止执行指令。 +这个插件包含一条有两个参数的指令,类型都为 `string`,其中 `foo` 为必选参数,`bar` +为可选参数。如果调用指令时未提供可选参数,参数将被初始化为类型默认值;如果为提供必选参数,bot 将向其发送一条提示信息并停止执行指令。 向 bot 发送 `example qwq ovo`(~~随便什么~~),bot 会将参数的值发送回来。 ### 类型约束 -如果我们不对指令的参数进行类型约束,那么参数的类型将默认为 `string`。如要添加参数,可以在参数名称后添加 `:` 号和类型名称。类型名称支持 C# 中绝大多数的基本类型,如 `int`, `double`, `long`, `bool` 等等,在解析过程中会自动进行类型检查和转换。 +如果我们不对指令的参数进行类型约束,那么参数的类型将默认为 `string`。如要添加参数,可以在参数名称后添加 `:` 号和类型名称。类型名称支持 +C# 中绝大多数的基本类型,如 `int`, `double`, `long`, `bool` 等等,在解析过程中会自动进行类型检查和转换。 举个例子: @@ -132,7 +135,8 @@ public MessageContent? OnExample(MessageContext ctx, ParsedArgs args) [Command("example [foo:int=1145] [bar:bool=true]")] ``` -如果不人为指定默认值,参数将被初始化为 C# 中的类型默认值(即 `default(T)`)。`string` 比较特殊,在参数中它的默认值是空字符串,而不是 `null`。 +如果不人为指定默认值,参数将被初始化为 C# 中的类型默认值(即 `default(T)`)。`string` +比较特殊,在参数中它的默认值是空字符串,而不是 `null`。 ### 灵活的表现形式 diff --git a/src/Flandre.Core/Attributes/CommandAttribute.cs b/src/Flandre.Core/Attributes/CommandAttribute.cs index 55ffd9c..1c7c93b 100644 --- a/src/Flandre.Core/Attributes/CommandAttribute.cs +++ b/src/Flandre.Core/Attributes/CommandAttribute.cs @@ -32,7 +32,7 @@ public CommandAttribute(string pattern) while (!parser.IsEnd()) { - var bracket = parser.Current(); + var bracket = parser.Current; if (!"<[".Contains(bracket)) { parser.Read(' '); diff --git a/src/Flandre.Core/Attributes/OptionAttribute.cs b/src/Flandre.Core/Attributes/OptionAttribute.cs index 50bb38c..1560451 100644 --- a/src/Flandre.Core/Attributes/OptionAttribute.cs +++ b/src/Flandre.Core/Attributes/OptionAttribute.cs @@ -13,6 +13,11 @@ public class OptionAttribute : Attribute /// public string Name { get; } + /// + /// 短名称 + /// + public char? ShortName { get; } + /// /// 选项别名 /// @@ -45,20 +50,27 @@ public OptionAttribute(string name, string? pattern = null) if (parser.SkipSpaces().IsEnd()) return; var first = parser.Read(' '); + ParameterInfo info; - if (first.StartsWith('-')) + if (first.StartsWith("--")) { Alias = first.TrimStart('-'); if (parser.SkipSpaces().IsEnd()) return; - var info = CommandUtils.ParseParameterSection(parser.ReadToEnd(), cmdName: Name); - Type = info.Type; - DefaultValue = info.DefaultValue; + info = CommandUtils.ParseParameterSection(parser.ReadToEnd(), cmdName: Name); + } + else if (first.StartsWith('-')) + { + var trimmed = first.TrimStart('-'); + ShortName = trimmed.Length > 0 ? trimmed[0] : null; + if (parser.SkipSpaces().IsEnd()) return; + info = CommandUtils.ParseParameterSection(parser.ReadToEnd(), cmdName: Name); } else { - var info = CommandUtils.ParseParameterSection(first, cmdName: Name); - Type = info.Type; - DefaultValue = info.DefaultValue; + info = CommandUtils.ParseParameterSection(first, cmdName: Name); } + + Type = info.Type; + DefaultValue = info.DefaultValue; } } \ No newline at end of file diff --git a/src/Flandre.Core/Common/ArgumentManager.cs b/src/Flandre.Core/Common/ArgumentManager.cs index 977b3ad..5e00602 100644 --- a/src/Flandre.Core/Common/ArgumentManager.cs +++ b/src/Flandre.Core/Common/ArgumentManager.cs @@ -47,28 +47,4 @@ public T Get(string name) { return (T)ArgumentList.First(arg => arg.Key == name).Value; } - - /// - /// 根据索引获取参数 - /// - /// 参数索引 - /// 返回类型 - /// 参数值,若索引越界或无法转换则返回 default(T) - public T? GetOrDefault(int index) - { - var result = ArgumentList.ElementAtOrDefault(index).Value; - return result is T casted ? casted : default; - } - - /// - /// 根据名称获取参数 - /// - /// 参数名称 - /// 返回类型 - /// 参数值,若未找到或无法转换则返回 default(T) - public T? GetOrDefault(string name) - { - var result = ArgumentList.FirstOrDefault(arg => arg.Key == name).Value; - return result is T casted ? casted : default; - } } \ No newline at end of file diff --git a/src/Flandre.Core/Common/Command.cs b/src/Flandre.Core/Common/Command.cs index f60d1c2..15219ac 100644 --- a/src/Flandre.Core/Common/Command.cs +++ b/src/Flandre.Core/Common/Command.cs @@ -1,5 +1,7 @@ using System.Reflection; using Flandre.Core.Attributes; +using Flandre.Core.Messaging; +using Flandre.Core.Utils; namespace Flandre.Core.Common; @@ -23,10 +25,153 @@ public class Command /// public List Options { get; } - internal Command(CommandAttribute info, MethodInfo innerMethod, List options) + private readonly Logger _pluginLogger; + + internal Command(CommandAttribute info, MethodInfo innerMethod, List options, Logger pluginLogger) { CommandInfo = info; InnerMethod = innerMethod; Options = options; + _pluginLogger = pluginLogger; + } + + internal MessageContent? ParseCommand(MessageContext ctx, StringParser parser) + { + var args = new ParsedArgs(); + + var argIndex = 0; + var providedArgs = new List(); + + while (!parser.IsEnd()) + { + var peek = parser.SkipSpaces().Peek(' '); + + if (peek.StartsWith("--")) + { + // option (full) + var optName = parser.Read(' ').TrimStart('-'); + var optNo = false; + + if (optName.Length > 3 && optName.StartsWith("no-")) + { + optName = optName[3..]; + optNo = true; + } + + var option = Options.FirstOrDefault(opt => opt.Alias == optName) + ?? Options.FirstOrDefault(opt => opt.Name == optName); + if (option is null) + return $"未知选项:{optName}。"; + + parser.SkipSpaces(); + + switch (option.Type) + { + case "bool": + args.Options.OptionsDict[option.Name] = !optNo; + break; + + case "string": + args.Options.OptionsDict[option.Name] = parser.ReadQuoted(); + break; + + default: + if (CommandUtils.TryParseType(parser.Read(' '), + option.Type, out var result, false)) + args.Options.OptionsDict[option.Name] = result; + else return $"选项 {option.Name} 类型错误,应为 {option.Type}。"; + break; + } + } + else if (peek.StartsWith('-')) + { + // option (short) + var opts = parser.Read(' ').TrimStart('-'); + + parser.SkipSpaces(); + + for (var i = 0; i < opts.Length; i++) + { + var optName = opts[i]; + var option = Options.FirstOrDefault(opt => opt.ShortName == optName); + if (option is null) + return $"未知选项:{optName}。"; + + if (option.Type == "bool") + { + args.Options.OptionsDict[option.Name] = true; + } + else + { + if (i < opts.Length - 1) + return $"选项 {option.Name} 类型错误,应为 {option.Type}。"; + + if (option.Type == "string") + args.Options.OptionsDict[option.Name] = parser.ReadQuoted(); + else if (CommandUtils.TryParseType(parser.Read(' '), + option.Type, out var result, false)) + args.Options.OptionsDict[option.Name] = result; + else return $"选项 {option.Name} 类型错误,应为 {option.Type}。"; + } + } + } + else + { + // argument + if (argIndex >= CommandInfo.Parameters.Count) + return "参数过多,请检查指令格式。"; + + var param = CommandInfo.Parameters[argIndex]; + + if (param.Type == "string") + { + 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 $"参数 {param.Name} 类型错误,应为 {param.Type}。"; + } + + providedArgs.Add(param.Name); + argIndex++; + } + } + + // 默认值 + foreach (var param in CommandInfo.Parameters) + { + var provided = providedArgs.Contains(param.Name); + if (param.IsRequired && !provided) + return $"参数 {param.Name} 缺失。"; + if (param.IsRequired || provided) continue; + args.Arguments.ArgumentList.Add(new KeyValuePair(param.Name, param.DefaultValue)); + } + + foreach (var opt in Options) + if (!args.Options.OptionsDict.ContainsKey(opt.Name)) + args.Options.OptionsDict[opt.Name] = opt.DefaultValue; + + try + { + var cmdResult = InnerMethod.Invoke( + this, new object[] { ctx, args }[..InnerMethod.GetParameters().Length]); + var content = cmdResult as MessageContent ?? (cmdResult as Task)?.Result ?? null; + + return content; + } + catch (TargetInvocationException te) + { + _pluginLogger.Error(te.InnerException ?? te); + } + catch (Exception e) + { + _pluginLogger.Error(e); + } + + return null; } } \ No newline at end of file diff --git a/src/Flandre.Core/Common/OptionManager.cs b/src/Flandre.Core/Common/OptionManager.cs index 269d7c8..35e17e6 100644 --- a/src/Flandre.Core/Common/OptionManager.cs +++ b/src/Flandre.Core/Common/OptionManager.cs @@ -15,17 +15,14 @@ public class OptionManager : IEnumerable> /// 选项名称 /// 返回类型 /// 若未提供该选项,或类型错误则返回类型默认值 - public T? GetOrDefault(string key) + public T Get(string key) { - var value = OptionsDict.GetValueOrDefault(key); - return value is not null ? (T)value : default; + return (T)OptionsDict[key]; } /// /// 获取 Enumerator /// - /// - /// public IEnumerator> GetEnumerator() { return OptionsDict.GetEnumerator(); diff --git a/src/Flandre.Core/Common/ParsedArgs.cs b/src/Flandre.Core/Common/ParsedArgs.cs index 1a9b355..9eab59b 100644 --- a/src/Flandre.Core/Common/ParsedArgs.cs +++ b/src/Flandre.Core/Common/ParsedArgs.cs @@ -36,24 +36,12 @@ public T GetArgument(string name) } /// - /// 根据索引获取参数 - /// - /// 参数索引 - /// 返回类型 - /// 参数值,若索引越界或无法转换则返回 default(T) - public T? GetArgumentOrDefault(int index) - { - return Arguments.GetOrDefault(index); - } - - /// - /// 根据名称获取参数 + /// 根据名称获取选项,值将被强制转换为 T 类型 /// /// 参数名称 /// 返回类型 - /// 参数值,若未找到或无法转换则返回 default(T) - public T? GetArgumentOrDefault(string name) + public T GetOption(string name) { - return Arguments.GetOrDefault(name); + return Options.Get(name); } } \ No newline at end of file diff --git a/src/Flandre.Core/Common/Plugin.cs b/src/Flandre.Core/Common/Plugin.cs index 2103361..d89522f 100644 --- a/src/Flandre.Core/Common/Plugin.cs +++ b/src/Flandre.Core/Common/Plugin.cs @@ -38,141 +38,17 @@ public Plugin() foreach (var method in type.GetMethods()) { var attr = method.GetCustomAttribute(); - if (attr is not null) - { - var options = new List(); + if (attr is null) continue; - foreach (var optionAttr in method.GetCustomAttributes()) - options.Add(optionAttr); + var options = method.GetCustomAttributes().ToList(); - Commands.Add(new Command(attr, method, options)); - } + Commands.Add(new Command(attr, method, options, Logger)); } } - internal MessageContent? OnCommandParsing(MessageContext ctx) + internal MessageContent GetHelp() { - var commandStr = ctx.Message.GetText(); - if (string.IsNullOrWhiteSpace(commandStr)) return null; - - foreach (var command in Commands) - { - var basePattern = ctx.App.Config.CommandPrefix + - (PluginInfo.BaseCommand + ' ' + - command.CommandInfo.Command).TrimStart(); - - var startsWithFlag = commandStr.StartsWith(basePattern); - - if (!startsWithFlag) - continue; - - if (startsWithFlag) - commandStr = commandStr[basePattern.Length..].Trim(); - - var (parsedArgs, errorMsg) = ParseCommand(command, commandStr); - if (errorMsg != null) - return errorMsg; - - var result = command.InnerMethod.Invoke( - this, new object[] { ctx, parsedArgs }[..command.InnerMethod.GetParameters().Length]); - var content = result as MessageContent ?? (result as Task)?.Result ?? null; - return content; - } - - return null; - } - - internal (ParsedArgs, string?) ParseCommand(Command command, string source) - { - var args = new ParsedArgs(); - var parser = new StringParser(source); - - var argIndex = 0; - var providedArgs = new List(); - - while (!parser.IsEnd()) - { - var peek = parser.SkipSpaces().Peek(' '); - - if (peek.StartsWith('-')) - { - // option - var optName = parser.Read(' ').TrimStart('-'); - var optNo = false; - - if (optName.Length > 3 && optName.StartsWith("no-")) - { - optName = optName[3..]; - optNo = true; - } - - var option = command.Options.FirstOrDefault(opt => opt.Alias == optName) - ?? command.Options.FirstOrDefault(opt => opt.Name == optName); - if (option is null) - return (args, $"未知选项:{optName}。"); - - parser.SkipSpaces(); - - switch (option.Type) - { - case "bool": - args.Options.OptionsDict[option.Name] = !optNo; - break; - - case "string": - args.Options.OptionsDict[option.Name] = parser.ReadQuoted(); - break; - - default: - if (CommandUtils.TryParseType(parser.Read(' '), - option.Type, out var result, false)) - args.Options.OptionsDict[option.Name] = result; - else return (args, $"选项 {option.Name} 类型错误,应为 {option.Type}。"); - break; - } - } - else - { - // argument - if (argIndex >= command.CommandInfo.Parameters.Count) - return (args, "参数过多,请检查指令格式。"); - - var param = command.CommandInfo.Parameters[argIndex]; - - if (param.Type == "string") - { - var quote = parser.Peek(1); - 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}。"); - } - - providedArgs.Add(param.Name); - argIndex++; - } - } - - // 默认值 - foreach (var param in command.CommandInfo.Parameters) - { - var provided = providedArgs.Contains(param.Name); - if (param.IsRequired && !provided) - return (args, $"参数 {param.Name} 缺失。"); - if (param.IsRequired || provided) continue; - args.Arguments.ArgumentList.Add(new KeyValuePair(param.Name, param.DefaultValue)); - } - - foreach (var opt in command.Options) - if (!args.Options.OptionsDict.ContainsKey(opt.Name)) - args.Options.OptionsDict[opt.Name] = opt.DefaultValue; - - return (args, null); + throw new NotImplementedException(); } /// diff --git a/src/Flandre.Core/Flandre.Core.csproj b/src/Flandre.Core/Flandre.Core.csproj index adbadc8..298fd85 100644 --- a/src/Flandre.Core/Flandre.Core.csproj +++ b/src/Flandre.Core/Flandre.Core.csproj @@ -23,8 +23,8 @@ - - + + diff --git a/src/Flandre.Core/FlandreApp.cs b/src/Flandre.Core/FlandreApp.cs index 94dbfa3..9c67869 100644 --- a/src/Flandre.Core/FlandreApp.cs +++ b/src/Flandre.Core/FlandreApp.cs @@ -1,8 +1,7 @@ -using System.Reflection; +using System.Runtime.CompilerServices; using Flandre.Core.Common; using Flandre.Core.Events.App; using Flandre.Core.Utils; -using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Flandre.Core.Tests")] [assembly: InternalsVisibleTo("Flandre.TestKit")] @@ -26,6 +25,8 @@ public class FlandreApp private readonly CancellationTokenSource _appStopTokenSource = new(); + private readonly Dictionary _commandMap = new(); + internal static Logger Logger { get; } = new("App"); /// @@ -83,6 +84,22 @@ public FlandreApp Use(IModule module) case Plugin plugin: Plugins.Add(plugin); + + if (plugin.PluginInfo.BaseCommand is not null) + { + _commandMap[plugin.PluginInfo.BaseCommand] = plugin; + foreach (var command in plugin.Commands) + if (plugin.PluginInfo.BaseCommand == command.CommandInfo.Command) + _commandMap[command.CommandInfo.Command] = command; + else + _commandMap[$"{plugin.PluginInfo.BaseCommand}.{command.CommandInfo.Command}"] = command; + } + else + { + foreach (var command in plugin.Commands) + _commandMap[command.CommandInfo.Command] = command; + } + break; } @@ -128,45 +145,89 @@ public void Stop() private void SubscribeEvents() { foreach (var bot in Bots) - foreach (var plugin in Plugins) { + foreach (var plugin in Plugins) + { + var ctx = new Context(this, bot); + + bot.OnMessageReceived += (_, e) => + plugin.OnMessageReceived(new MessageContext(this, bot, e.Message)); + bot.OnGuildInvited += (_, e) => plugin.OnGuildInvited(ctx, e); + bot.OnGuildRequested += (_, e) => plugin.OnGuildRequested(ctx, e); + bot.OnFriendRequested += (_, e) => plugin.OnFriendRequested(ctx, e); + } + bot.OnMessageReceived += (_, e) => + OnCommandParsing(new MessageContext(this, bot, e.Message)); + } + } + + private void OnCommandParsing(MessageContext ctx) + { + var commandStr = ctx.Message.GetText().Trim(); + + if (commandStr.StartsWith(Config.CommandPrefix)) + { + void DealCommand(Command cmd, StringParser p) { - var ctx = new MessageContext(this, bot, e.Message); - try - { - plugin.OnMessageReceived(ctx); - var content = plugin.OnCommandParsing(ctx); - if (content is not null) - bot.SendMessage(e.Message.SourceType, - e.Message.GuildId, e.Message.ChannelId, e.Message.Sender.Id, content); - } - catch (TargetInvocationException exception) - { - plugin.Logger.Error(exception.InnerException ?? exception); - } - catch (Exception ex) - { - plugin.Logger.Error(ex); - } - }; + var content = cmd.ParseCommand(ctx, p); + if (content is null) return; + ctx.Bot.SendMessage(ctx.Message.SourceType, ctx.Message.GuildId, ctx.Message.ChannelId, + ctx.Message.Sender.Id, content); + } + + if (commandStr == Config.CommandPrefix) return; + var parser = new StringParser(commandStr.TrimStart(Config.CommandPrefix)); + var root = parser.Read(' '); + + var obj = _commandMap.GetValueOrDefault(root); + + switch (obj) + { + case null: + if (Config.CommandPrefix == "") return; + ctx.Bot.SendMessage(ctx.Message.SourceType, ctx.Message.GuildId, ctx.Message.ChannelId, + ctx.Message.Sender.Id, $"未找到指令:{root}。"); + return; - var ctx = new Context(this, bot); + case Command command: + DealCommand(command, parser); + return; - bot.OnGuildInvited += (_, e) => plugin.OnGuildInvited(ctx, e); - bot.OnGuildRequested += (_, e) => plugin.OnGuildRequested(ctx, e); - bot.OnFriendRequested += (_, e) => plugin.OnFriendRequested(ctx, e); + case Plugin plugin: + { + if (!parser.IsEnd()) + { + root = $"{root}.{parser.Read(' ')}"; + obj = _commandMap.GetValueOrDefault(root); + switch (obj) + { + case null: + ctx.Bot.SendMessage(ctx.Message.SourceType, ctx.Message.GuildId, ctx.Message.ChannelId, + ctx.Message.Sender.Id, $"未找到指令:{root}。"); + return; + case Command cmd: + DealCommand(cmd, parser); + return; + } + } + + ctx.Bot.SendMessage(ctx.Message.SourceType, ctx.Message.GuildId, ctx.Message.ChannelId, + ctx.Message.Sender.Id, plugin.GetHelp()); + break; + } + } } } -} -/// -/// 应用配置 -/// -public class AppConfig -{ /// - /// 全局指令前缀 + /// 应用配置 /// - public string CommandPrefix { get; set; } = ""; + public class AppConfig + { + /// + /// 全局指令前缀 + /// + public string CommandPrefix { get; set; } = ""; + } } \ No newline at end of file diff --git a/src/Flandre.Core/Models/GuildMember.cs b/src/Flandre.Core/Models/GuildMember.cs index b0ed8e8..9c22eb5 100644 --- a/src/Flandre.Core/Models/GuildMember.cs +++ b/src/Flandre.Core/Models/GuildMember.cs @@ -8,5 +8,5 @@ public class GuildMember : User /// /// 成员角色 /// - public List Roles { get; init; } = new (); + public List Roles { get; init; } = new(); } \ No newline at end of file diff --git a/src/Flandre.Core/Utils/CommandUtils.cs b/src/Flandre.Core/Utils/CommandUtils.cs index 6b53bab..0b19d75 100644 --- a/src/Flandre.Core/Utils/CommandUtils.cs +++ b/src/Flandre.Core/Utils/CommandUtils.cs @@ -9,7 +9,7 @@ internal static ParameterInfo ParseParameterSection(string section, string defau { var info = new ParameterInfo(); section = section.Trim(); - + if (section[0] == '<') info.IsRequired = true; @@ -21,7 +21,7 @@ internal static ParameterInfo ParseParameterSection(string section, string defau info.DefaultValue = GetTypeDefaultValue(info.Type, defaultType); // 默认值 - if (!info.IsRequired && innerRight.Length > 1) + if (innerRight.Length > 1) { if (TryParseType(innerRight[1].Trim(), info.Type, out var result)) info.DefaultValue = result; diff --git a/src/Flandre.Core/Utils/Logger.cs b/src/Flandre.Core/Utils/Logger.cs index 9600583..dbb74b7 100644 --- a/src/Flandre.Core/Utils/Logger.cs +++ b/src/Flandre.Core/Utils/Logger.cs @@ -18,7 +18,7 @@ public Logger(string name) { Name = name; } - + private static void Log(string message) { var logMessage = $"{DateTime.Now:HH:mm:ss} {message}"; diff --git a/src/Flandre.Core/Utils/StringParser.cs b/src/Flandre.Core/Utils/StringParser.cs index 1db36a2..449cfcd 100644 --- a/src/Flandre.Core/Utils/StringParser.cs +++ b/src/Flandre.Core/Utils/StringParser.cs @@ -5,6 +5,8 @@ internal class StringParser private readonly string _str; private int _pos; + internal char Current => _str[_pos]; + internal StringParser(string str) { _str = str; @@ -32,11 +34,6 @@ internal StringParser SkipSpaces() return this; } - internal char Current() - { - return _str[_pos]; - } - internal string Peek(int length) { return _str.Substring(_pos, length); diff --git a/src/Flandre.Core/Utils/TextUtils.cs b/src/Flandre.Core/Utils/TextUtils.cs index 984716b..b0c14f0 100644 --- a/src/Flandre.Core/Utils/TextUtils.cs +++ b/src/Flandre.Core/Utils/TextUtils.cs @@ -6,4 +6,27 @@ internal static string RemoveString(this string text, string remove) { return text.Replace(remove, ""); } + + internal static string TrimStart(this string source, string value, + StringComparison comparison = StringComparison.Ordinal) + { + if (value == "") return source; + var valueLength = value.Length; + var startIndex = 0; + while (source.IndexOf(value, startIndex, comparison) == startIndex) startIndex += valueLength; + + return source[startIndex..]; + } + + internal static string TrimEnd(this string source, string value, + StringComparison comparison = StringComparison.Ordinal) + { + if (value == "") return source; + var sourceLength = source.Length; + var valueLength = value.Length; + var count = sourceLength; + while (source.LastIndexOf(value, count, comparison) == count - valueLength) count -= valueLength; + + return source[..count]; + } } \ No newline at end of file diff --git a/src/Flandre.TestKit/Extensions.cs b/src/Flandre.TestKit/Extensions.cs index ba301e1..fdbdf06 100644 --- a/src/Flandre.TestKit/Extensions.cs +++ b/src/Flandre.TestKit/Extensions.cs @@ -17,8 +17,10 @@ public static FlandreTestClient GenerateChannelClient(this TestAdapter adapter, } public static FlandreTestClient GenerateChannelClient(this TestAdapter adapter) - => GenerateChannelClient(adapter, Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), + { + return GenerateChannelClient(adapter, Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString()); + } public static FlandreTestClient GenerateFriendClient(this TestAdapter adapter, string userId) { @@ -30,5 +32,7 @@ public static FlandreTestClient GenerateFriendClient(this TestAdapter adapter, s } public static FlandreTestClient GenerateFriendClient(this TestAdapter adapter) - => GenerateFriendClient(adapter, Guid.NewGuid().ToString()); + { + return GenerateFriendClient(adapter, Guid.NewGuid().ToString()); + } } \ No newline at end of file diff --git a/src/Flandre.TestKit/Flandre.TestKit.csproj b/src/Flandre.TestKit/Flandre.TestKit.csproj index c4b3739..28d1f85 100644 --- a/src/Flandre.TestKit/Flandre.TestKit.csproj +++ b/src/Flandre.TestKit/Flandre.TestKit.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Flandre.TestKit/TestAdapter.cs b/src/Flandre.TestKit/TestAdapter.cs index fbb2634..fab7c94 100644 --- a/src/Flandre.TestKit/TestAdapter.cs +++ b/src/Flandre.TestKit/TestAdapter.cs @@ -7,7 +7,7 @@ namespace Flandre.TestKit; public class TestAdapter : IAdapter { internal readonly TestBot Bot = new(); - + public async Task Start() { } diff --git a/src/Flandre.TestKit/TestBot.cs b/src/Flandre.TestKit/TestBot.cs index 2e24368..020993c 100644 --- a/src/Flandre.TestKit/TestBot.cs +++ b/src/Flandre.TestKit/TestBot.cs @@ -60,7 +60,7 @@ public async Task GetSelf() { Name = "Test Bot", Nickname = "Test Bot", - Id = _selfId, + Id = _selfId }; } diff --git a/tests/Flandre.Core.Tests/CommonTests/PluginTests.cs b/tests/Flandre.Core.Tests/CommonTests/PluginTests.cs index 9469683..0dd8d81 100644 --- a/tests/Flandre.Core.Tests/CommonTests/PluginTests.cs +++ b/tests/Flandre.Core.Tests/CommonTests/PluginTests.cs @@ -2,5 +2,4 @@ public class PluginTests { - } \ No newline at end of file diff --git a/tests/Flandre.Core.Tests/Flandre.Core.Tests.csproj b/tests/Flandre.Core.Tests/Flandre.Core.Tests.csproj index 0905195..ba6b98b 100644 --- a/tests/Flandre.Core.Tests/Flandre.Core.Tests.csproj +++ b/tests/Flandre.Core.Tests/Flandre.Core.Tests.csproj @@ -9,8 +9,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -22,8 +22,8 @@ - - + + diff --git a/tests/Flandre.Core.Tests/FlandreAppTests.cs b/tests/Flandre.Core.Tests/FlandreAppTests.cs index 66639f8..3441d3b 100644 --- a/tests/Flandre.Core.Tests/FlandreAppTests.cs +++ b/tests/Flandre.Core.Tests/FlandreAppTests.cs @@ -3,6 +3,8 @@ using Flandre.Core.Messaging; using Flandre.TestKit; +// ReSharper disable StringLiteralTypo + namespace Flandre.Core.Tests; public class FlandreAppTests @@ -18,18 +20,24 @@ public void TestFlandreApp() app.OnAppReady += async (_, _) => { - var content = await channelClient.SendForReply("114514"); - Assert.Equal("114514", content?.GetText()); - + var content = await channelClient.SendForReply("OMR:114514"); + Assert.Equal("OMR:114514", content?.GetText()); + content = await friendClient.SendForReply("test1 true --opt 114.514"); - Assert.Equal("arg1: True opt: 114.514", content?.GetText()); - + Assert.Equal("arg1: True opt: 114.514 b: False t: True", + content?.GetText()); + content = await friendClient.SendForReply("test1 -o 1919.810 false"); - Assert.Equal("arg1: False opt: 1919.81", content?.GetText()); + Assert.Equal("arg1: False opt: 1919.81 b: False t: True", + content?.GetText()); + + content = await friendClient.SendForReply("test1 -bo 111.444 --no-trueopt false"); + Assert.Equal("arg1: False opt: 111.444 b: True t: False", + content?.GetText()); app.Stop(); }; - + app.Use(adapter).Use(new TestPlugin()).Start(); } } @@ -39,15 +47,20 @@ public class TestPlugin : Plugin { public override void OnMessageReceived(MessageContext ctx) { - ctx.Bot.SendMessage(ctx.Message); + if (ctx.Message.GetText().StartsWith("OMR:")) + ctx.Bot.SendMessage(ctx.Message); } [Command("test1 ")] [Option("opt", "-o ")] + [Option("boolopt", "-b <:bool>")] + [Option("trueopt", "-t <:bool=true>")] public static MessageContent OnTest1(MessageContext ctx, ParsedArgs args) { var arg1 = args.GetArgument("arg1"); - var opt = args.Options.GetOrDefault("opt"); - return $"arg1: {arg1} opt: {opt}"; + var opt = args.GetOption("opt"); + var boolOpt = args.GetOption("boolopt"); + var trueOpt = args.GetOption("trueopt"); + return $"arg1: {arg1} opt: {opt} b: {boolOpt} t: {trueOpt}"; } } \ No newline at end of file diff --git a/tests/Flandre.Core.Tests/UtilsTests/CommandUtilsTests.cs b/tests/Flandre.Core.Tests/UtilsTests/CommandUtilsTests.cs index c047e2b..c70511e 100644 --- a/tests/Flandre.Core.Tests/UtilsTests/CommandUtilsTests.cs +++ b/tests/Flandre.Core.Tests/UtilsTests/CommandUtilsTests.cs @@ -6,8 +6,8 @@ public class CommandUtilsTests { [Theory] [InlineData("", true, "double", default(double))] - [InlineData("", true, "string", "")] // string arg type defaults to empty, not null - [InlineData("[gamma]", false, "string", "")] + [InlineData("", true, "string", "1234")] // for option, allow required arg have default value + [InlineData("[gamma]", false, "string", "")] // string arg type defaults to empty, not null [InlineData("[]", false, "string", "")] [InlineData(" [epsilon: bool = true] ", false, "bool", true)] [InlineData("[f: bool = 12345678] ", false, "bool", false)] // wrong default value type diff --git a/tests/Flandre.Core.Tests/UtilsTests/TextUtilsTests.cs b/tests/Flandre.Core.Tests/UtilsTests/TextUtilsTests.cs index 544bee3..b8b4055 100644 --- a/tests/Flandre.Core.Tests/UtilsTests/TextUtilsTests.cs +++ b/tests/Flandre.Core.Tests/UtilsTests/TextUtilsTests.cs @@ -11,4 +11,12 @@ public void TestRemoveString(string source, string result, string removal) { Assert.Equal(result, source.RemoveString(removal)); } + + [Theory] + [InlineData("string", "ring", "st")] + [InlineData("string", "string", "")] + public void TestTrimStart(string source, string result, string trim) + { + Assert.Equal(result, source.TrimStart(trim)); + } } \ No newline at end of file