├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── docs.yml │ └── feature-request.yml └── workflows │ └── test-publish.yml ├── .gitignore ├── Flandre.sln ├── Flandre.sln.DotSettings ├── LICENSE ├── README.NuGet.md ├── README.md ├── assets └── avatar.jpg ├── scripts └── release.ps1 ├── src ├── Flandre.Adapters.Discord │ ├── DiscordAdapter.cs │ ├── DiscordBot.cs │ ├── DiscordBotConfig.cs │ ├── DiscordException.cs │ ├── Flandre.Adapters.Discord.csproj │ ├── InternalEventHandlers.cs │ └── InternalUtils.cs ├── Flandre.Adapters.Konata.Extensions │ ├── AdapterCollectionExtensions.cs │ └── Flandre.Adapters.Konata.Extensions.csproj ├── Flandre.Adapters.Konata │ ├── Flandre.Adapters.Konata.csproj │ ├── KonataAdapter.cs │ ├── KonataBot.cs │ ├── LICENSE │ ├── README.md │ └── Utils.cs ├── Flandre.Adapters.Mock │ ├── Extensions.cs │ ├── Flandre.Adapters.Mock.csproj │ ├── MockAdapter.cs │ ├── MockBot.cs │ ├── MockClient.cs │ └── MockClientExtensions.cs ├── Flandre.Adapters.OneBot.Extensions │ ├── AdapterCollectionExtensions.cs │ └── Flandre.Adapters.OneBot.Extensions.csproj ├── Flandre.Adapters.OneBot │ ├── CqCodeParser.cs │ ├── Flandre.Adapters.OneBot.csproj │ ├── GuildBot.cs │ ├── GuildInternalBot.cs │ ├── InternalBot.cs │ ├── Models │ │ ├── OneBotApiEvent.cs │ │ ├── OneBotApiGuildMessageEvent.cs │ │ ├── OneBotApiMessageEvent.cs │ │ ├── OneBotApiRequestEvent.cs │ │ ├── OneBotApiResponse.cs │ │ ├── OneBotFriend.cs │ │ ├── OneBotGroup.cs │ │ ├── OneBotGroupMember.cs │ │ ├── OneBotGuild.cs │ │ ├── OneBotGuildChannel.cs │ │ ├── OneBotGuildMember.cs │ │ ├── OneBotGuildMemberProfile.cs │ │ ├── OneBotGuildMeta.cs │ │ ├── OneBotGuildServiceProfile.cs │ │ ├── OneBotLoginInfo.cs │ │ ├── OneBotMessage.cs │ │ ├── OneBotMessageSender.cs │ │ └── OneBotUser.cs │ ├── OneBotAdapter.cs │ ├── OneBotBot.cs │ ├── OneBotBotConfig.cs │ ├── OneBotUtils.cs │ ├── README.md │ ├── Segments │ │ ├── OneBotImageSegment.cs │ │ └── OneBotRecordSegment.cs │ └── WebSocketBot.cs ├── Flandre.Core.Reactive │ ├── CoreReactiveExtensions.cs │ ├── Flandre.Core.Reactive.csproj │ ├── MessageExtensions.cs │ └── MessageReceivedExtensions.cs ├── Flandre.Core │ ├── AssemblyInfo.cs │ ├── Common │ │ ├── Bot.cs │ │ ├── BotContext.cs │ │ ├── BotEvents.cs │ │ ├── BotExtensions.cs │ │ ├── BotLogLevel.cs │ │ └── IAdapter.cs │ ├── Events │ │ ├── BotFriendRequestedEvent.cs │ │ ├── BotGuildInvitedEvent.cs │ │ ├── BotGuildJoinRequestedEvent.cs │ │ ├── BotLoggingEvent.cs │ │ ├── BotMessageReceivedEvent.cs │ │ └── FlandreEvent.cs │ ├── Flandre.Core.csproj │ ├── Messaging │ │ ├── Message.cs │ │ ├── MessageBuilder.cs │ │ ├── MessageContent.cs │ │ ├── MessageContext.cs │ │ ├── MessageSegment.cs │ │ └── Segments │ │ │ ├── AtSegment.cs │ │ │ ├── AudioSegment.cs │ │ │ ├── FaceSegment.cs │ │ │ ├── ImageSegment.cs │ │ │ ├── QuoteSegment.cs │ │ │ └── TextSegment.cs │ ├── Models │ │ ├── Channel.cs │ │ ├── Guild.cs │ │ ├── GuildMember.cs │ │ ├── User.cs │ │ └── UserRole.cs │ └── Utils │ │ ├── FlandreCoreUtils.cs │ │ ├── StringParser.cs │ │ └── TextUtils.cs ├── Flandre.Framework.Reactive │ ├── AppReactiveExtensions.cs │ └── Flandre.Framework.Reactive.csproj └── Flandre.Framework │ ├── AdapterCollection.cs │ ├── AssemblyInfo.cs │ ├── Common │ ├── Command.cs │ ├── CommandContext.cs │ ├── CommandExceptions.cs │ ├── CommandOption.cs │ ├── CommandParameter.cs │ ├── CommandParseResult.cs │ ├── CommandShortcut.cs │ ├── ICommandParser.cs │ ├── MiddlewareContext.cs │ ├── OptionAttribute.cs │ ├── Plugin.cs │ ├── ShortcutAttribute.cs │ └── TypeResolverDelegate.cs │ ├── Events │ ├── AppReadyEvent.cs │ ├── AppStartingEvent.cs │ ├── AppStoppedEvent.cs │ ├── CommandInvokedEvent.cs │ └── CommandInvokingEvent.cs │ ├── Extensions │ ├── FlandreAppExtensions.cs │ ├── ServiceCollectionExtensions.cs │ └── SessionExtensions.cs │ ├── Flandre.Framework.csproj │ ├── FlandreApp.cs │ ├── FlandreAppBuilder.cs │ ├── FlandreAppEvents.cs │ ├── FlandreAppOptions.cs │ ├── Internal │ ├── DefaultCommandParser.cs │ └── PluginCommandLoader.cs │ ├── InternalMiddlewares.cs │ ├── PluginCollection.cs │ ├── Routing │ ├── CommandAttribute.cs │ ├── CommandNode.cs │ ├── CommandNodeExtensions.cs │ ├── CommandRouteBuilderExtensions.cs │ └── ICommandRouteBuilder.cs │ ├── Services │ ├── CommandService.cs │ └── MiddlewareService.cs │ └── Utils │ ├── LogUtils.cs │ └── MessageUtils.cs └── tests ├── Flandre.Core.Reactive.Tests ├── CoreReactiveExtensionsTests.cs ├── Flandre.Core.Reactive.Tests.csproj └── Usings.cs ├── Flandre.Core.Tests ├── Flandre.Core.Tests.csproj ├── Usings.cs └── UtilsTests │ ├── StringParserTests.cs │ └── TextUtilsTests.cs └── Flandre.Framework.Tests ├── AppEventsTests.cs ├── CommandNodeTests.cs ├── CommandTests.cs ├── Flandre.Framework.Tests.csproj ├── FlandreAppTests.cs ├── MiddlewareTests.cs ├── SessionTests.cs ├── Usings.cs └── Utils.cs /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: 问题报告 3 | title: "Bug: " 4 | 5 | labels: 6 | - bug 7 | 8 | body: 9 | - type: textarea 10 | attributes: 11 | label: Describe the bug 12 | description: 请简明地描述 bug 的基本信息。 13 | validations: 14 | required: true 15 | 16 | - type: textarea 17 | attributes: 18 | label: Steps to reproduce 19 | description: 请描述如何重现这个行为。 20 | validations: 21 | required: true 22 | 23 | - type: textarea 24 | attributes: 25 | label: Expected behavior 26 | description: 请描述正常情况下期望的行为。 27 | validations: 28 | required: true 29 | 30 | - type: textarea 31 | attributes: 32 | label: Screenshots or logs 33 | description: 请尽量详细提供相关截图或日志等信息。 34 | 35 | - type: input 36 | attributes: 37 | label: Platform 38 | description: 项目运行的聊天平台。 39 | placeholder: e.g. QQ (Konata) 40 | validations: 41 | required: true 42 | 43 | - type: textarea 44 | attributes: 45 | label: Versions 46 | description: 使用的环境版本。 47 | placeholder: | 48 | e.g. 49 | .NET 6.0.301 50 | Flandre.Framework v1.0.0 51 | Flandre.Adapters.Konata v1.0.0-alpha 52 | validations: 53 | required: true 54 | 55 | - type: textarea 56 | attributes: 57 | label: Additional context 58 | description: 请描述其他想要补充的信息。 59 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | description: 文档改进 3 | title: "Docs: " 4 | 5 | labels: 6 | - docs 7 | 8 | body: 9 | - type: textarea 10 | attributes: 11 | label: Describe which part of the docs is problematic 12 | description: 请说明文档的哪一部分存在问题。 13 | validations: 14 | required: true 15 | 16 | - type: textarea 17 | attributes: 18 | label: Describe the problem related to this part of the docs 19 | description: 请说明文档的这一部分出现了什么问题。 20 | validations: 21 | required: true 22 | 23 | - type: textarea 24 | attributes: 25 | label: Additional context 26 | description: 请描述其他想要补充的信息。 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: 功能建议 3 | title: "Feature: " 4 | 5 | labels: 6 | - feature 7 | 8 | body: 9 | - type: textarea 10 | attributes: 11 | label: Describe the problem related to the feature request 12 | description: 请简要地说明是什么问题导致你想要一个新特性。 13 | validations: 14 | required: true 15 | 16 | - type: textarea 17 | attributes: 18 | label: Describe the solution you'd like 19 | description: 请说明你希望使用什么样的方法,或增加什么功能来解决上述问题。 20 | validations: 21 | required: true 22 | 23 | - type: textarea 24 | attributes: 25 | label: Describe alternatives you've considered 26 | description: 除了上述方法以外,你还考虑过的其他的实现方式。 27 | 28 | - type: textarea 29 | attributes: 30 | label: Additional context 31 | description: 请描述其他想要补充的信息。 32 | -------------------------------------------------------------------------------- /.github/workflows/test-publish.yml: -------------------------------------------------------------------------------- 1 | name: Test and Publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | 8 | jobs: 9 | test: 10 | name: Unit Test 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Setup .NET 17 | uses: actions/setup-dotnet@v3 18 | with: 19 | dotnet-version: 6.x.x 20 | 21 | - name: Test Projects 22 | run: | 23 | dotnet test tests/Flandre.Core.Tests/ --collect "XPlat Code Coverage" --results-directory code-coverage/ 24 | dotnet test tests/Flandre.Framework.Tests/ --collect "XPlat Code Coverage" --results-directory code-coverage/ 25 | 26 | - name: Upload code coverage to Codecov 27 | uses: codecov/codecov-action@v3 28 | with: 29 | directory: ./code-coverage/ 30 | 31 | 32 | publish: 33 | name: Publish NuGet Packages 34 | runs-on: ubuntu-latest 35 | needs: test 36 | 37 | steps: 38 | - uses: actions/checkout@v2 39 | 40 | - name: Setup .NET 41 | uses: actions/setup-dotnet@v1 42 | with: 43 | dotnet-version: 6.x.x 44 | 45 | - name: Publish Core 46 | id: publish-core 47 | uses: alirezanet/publish-nuget@v3.0.4 48 | with: 49 | PROJECT_FILE_PATH: src/Flandre.Core/Flandre.Core.csproj 50 | PACKAGE_NAME: Flandre.Core 51 | VERSION_FILE_PATH: src/Flandre.Core/Flandre.Core.csproj 52 | VERSION_REGEX: ^\s*(.*)<\/PackageVersion>\s*$ 53 | TAG_COMMIT: false 54 | NUGET_KEY: ${{secrets.NUGET_API_KEY}} 55 | 56 | - name: Publish Framework 57 | id: publish-fx 58 | uses: alirezanet/publish-nuget@v3.0.4 59 | with: 60 | PROJECT_FILE_PATH: src/Flandre.Framework/Flandre.Framework.csproj 61 | PACKAGE_NAME: Flandre.Framework 62 | VERSION_FILE_PATH: src/Flandre.Framework/Flandre.Framework.csproj 63 | VERSION_REGEX: ^\s*(.*)<\/PackageVersion>\s*$ 64 | TAG_COMMIT: true 65 | TAG_FORMAT: v* 66 | NUGET_KEY: ${{secrets.NUGET_API_KEY}} 67 | 68 | # ===== Adapters ===== 69 | 70 | - name: Publish Adapters.Konata 71 | id: publish-adapter-konata 72 | uses: alirezanet/publish-nuget@v3.0.4 73 | with: 74 | PROJECT_FILE_PATH: src/Flandre.Adapters.Konata/Flandre.Adapters.Konata.csproj 75 | PACKAGE_NAME: Flandre.Adapters.Konata 76 | VERSION_FILE_PATH: src/Flandre.Adapters.Konata/Flandre.Adapters.Konata.csproj 77 | VERSION_REGEX: ^\s*(.*)<\/PackageVersion>\s*$ 78 | TAG_COMMIT: false 79 | NUGET_KEY: ${{secrets.NUGET_API_KEY}} 80 | 81 | - name: Publish Adapters.OneBot 82 | id: publish-adapter-onebot 83 | uses: alirezanet/publish-nuget@v3.0.4 84 | with: 85 | PROJECT_FILE_PATH: src/Flandre.Adapters.OneBot/Flandre.Adapters.OneBot.csproj 86 | PACKAGE_NAME: Flandre.Adapters.OneBot 87 | VERSION_FILE_PATH: src/Flandre.Adapters.OneBot/Flandre.Adapters.OneBot.csproj 88 | VERSION_REGEX: ^\s*(.*)<\/PackageVersion>\s*$ 89 | TAG_COMMIT: false 90 | NUGET_KEY: ${{secrets.NUGET_API_KEY}} 91 | 92 | - name: Publish Adapters.Discord 93 | id: publish-adapter-discord 94 | uses: alirezanet/publish-nuget@v3.0.4 95 | with: 96 | PROJECT_FILE_PATH: src/Flandre.Adapters.Discord/Flandre.Adapters.Discord.csproj 97 | PACKAGE_NAME: Flandre.Adapters.Discord 98 | VERSION_FILE_PATH: src/Flandre.Adapters.Discord/Flandre.Adapters.Discord.csproj 99 | VERSION_REGEX: ^\s*(.*)<\/PackageVersion>\s*$ 100 | TAG_COMMIT: false 101 | NUGET_KEY: ${{secrets.NUGET_API_KEY}} 102 | 103 | - name: Publish Adapters.Mock 104 | id: publish-adapter-mock 105 | uses: alirezanet/publish-nuget@v3.0.4 106 | with: 107 | PROJECT_FILE_PATH: src/Flandre.Adapters.Mock/Flandre.Adapters.Mock.csproj 108 | PACKAGE_NAME: Flandre.Adapters.Mock 109 | VERSION_FILE_PATH: src/Flandre.Adapters.Mock/Flandre.Adapters.Mock.csproj 110 | VERSION_REGEX: ^\s*(.*)<\/PackageVersion>\s*$ 111 | TAG_COMMIT: false 112 | NUGET_KEY: ${{secrets.NUGET_API_KEY}} 113 | 114 | # ===== Adapter Extensions ===== 115 | 116 | - name: Publish Adapters.Konata.Extensions 117 | id: publish-adapter-konata-extensions 118 | uses: alirezanet/publish-nuget@v3.0.4 119 | with: 120 | PROJECT_FILE_PATH: src/Flandre.Adapters.Konata.Extensions/Flandre.Adapters.Konata.Extensions.csproj 121 | PACKAGE_NAME: Flandre.Adapters.Konata.Extensions 122 | VERSION_FILE_PATH: src/Flandre.Adapters.Konata.Extensions/Flandre.Adapters.Konata.Extensions.csproj 123 | VERSION_REGEX: ^\s*(.*)<\/PackageVersion>\s*$ 124 | TAG_COMMIT: false 125 | NUGET_KEY: ${{secrets.NUGET_API_KEY}} 126 | 127 | - name: Publish Adapters.OneBot.Extensions 128 | id: publish-adapter-onebot-extensions 129 | uses: alirezanet/publish-nuget@v3.0.4 130 | with: 131 | PROJECT_FILE_PATH: src/Flandre.Adapters.OneBot.Extensions/Flandre.Adapters.OneBot.Extensions.csproj 132 | PACKAGE_NAME: Flandre.Adapters.OneBot.Extensions 133 | VERSION_FILE_PATH: src/Flandre.Adapters.OneBot.Extensions/Flandre.Adapters.OneBot.Extensions.csproj 134 | VERSION_REGEX: ^\s*(.*)<\/PackageVersion>\s*$ 135 | TAG_COMMIT: false 136 | NUGET_KEY: ${{secrets.NUGET_API_KEY}} 137 | 138 | # ===== Reactive Extensions ===== 139 | 140 | - name: Publish Core.Reactive 141 | id: publish-core-rx 142 | uses: alirezanet/publish-nuget@v3.0.4 143 | with: 144 | PROJECT_FILE_PATH: src/Flandre.Core.Reactive/Flandre.Core.Reactive.csproj 145 | PACKAGE_NAME: Flandre.Core.Reactive 146 | VERSION_FILE_PATH: src/Flandre.Core.Reactive/Flandre.Core.Reactive.csproj 147 | VERSION_REGEX: ^\s*(.*)<\/PackageVersion>\s*$ 148 | TAG_COMMIT: false 149 | NUGET_KEY: ${{secrets.NUGET_API_KEY}} 150 | 151 | - name: Publish Framework.Reactive 152 | id: publish-fx-rx 153 | uses: alirezanet/publish-nuget@v3.0.4 154 | with: 155 | PROJECT_FILE_PATH: src/Flandre.Framework.Reactive/Flandre.Framework.Reactive.csproj 156 | PACKAGE_NAME: Flandre.Framework.Reactive 157 | VERSION_FILE_PATH: src/Flandre.Framework.Reactive/Flandre.Framework.Reactive.csproj 158 | VERSION_REGEX: ^\s*(.*)<\/PackageVersion>\s*$ 159 | TAG_COMMIT: false 160 | NUGET_KEY: ${{secrets.NUGET_API_KEY}} 161 | -------------------------------------------------------------------------------- /Flandre.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True 3 | True -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2023 b1acksoil 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.NuGet.md: -------------------------------------------------------------------------------- 1 | # Flandre 2 | 3 | .NET 6 实现的跨平台,现代化聊天机器人框架 4 | 一套代码,多平台服务 5 | 6 | [![License](https://img.shields.io/github/license/FlandreDevs/Flandre?label=License&style=flat&color=42a5f5)](https://github.com/FlandreDevs/Flandre/blob/main/LICENSE) 7 | [![Stars](https://img.shields.io/github/stars/FlandreDevs/Flandre?label=Stars&style=flat&color=1976d2)](https://github.com/FlandreDevs/Flandre/stargazers) 8 | [![Contributors](https://img.shields.io/github/contributors/FlandreDevs/Flandre?label=Contributors&style=flat&color=9866ca)](https://github.com/FlandreDevs/Flandre/graphs/contributors) 9 | [![.NET Version](https://img.shields.io/badge/.NET-6-ffe57f?style=flat)](https://www.nuget.org/packages/Flandre.Core/) 10 | [![Codecov](https://img.shields.io/codecov/c/gh/FlandreDevs/Flandre/dev?style=flat&color=a5d6a7&label=Coverage)](https://app.codecov.io/gh/FlandreDevs/Flandre) 11 | 12 | \- **[使用文档](https://flandredevs.github.io/)** - 13 | 14 | 本项目的名称来源于东方 Project 中的角色芙兰朵露 · 斯卡雷特 (Flandre Scarlet) ~~(番茄炒蛋)~~ 15 | 16 | --- 17 | 18 | **项目的完整 README 可[在 GitHub 上查看](https://github.com/FlandreDevs/Flandre/)。** 19 | 20 | --- 21 | 22 | **项目仍在早期开发阶段,功能尚未完善,且处于快速迭代过程中。** 23 | **如果您对项目的开发感兴趣,诚挚欢迎您的改进建议或 PR 贡献。** 24 | -------------------------------------------------------------------------------- /assets/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlandreBot/Flandre/d7889de2fa6450f005540dbea22c7b339c9ac676/assets/avatar.jpg -------------------------------------------------------------------------------- /scripts/release.ps1: -------------------------------------------------------------------------------- 1 | git switch release 2 | git merge dev 3 | git push origin release 4 | git switch dev 5 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.Discord/DiscordAdapter.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Common; 2 | 3 | namespace Flandre.Adapters.Discord; 4 | 5 | /// 6 | /// Discord 适配器 7 | /// 8 | public sealed class DiscordAdapter : IAdapter 9 | { 10 | /// 11 | public IEnumerable Bots => _bots.AsReadOnly(); 12 | 13 | private readonly List _bots = new(); 14 | 15 | /// 16 | /// 构造 Discord 适配器 17 | /// 18 | /// 19 | public DiscordAdapter(DiscordAdapterConfig config) 20 | { 21 | foreach (var bot in config.Bots) 22 | { 23 | _bots.Add(new DiscordBot(bot, config.Proxy)); 24 | } 25 | } 26 | 27 | /// 28 | public Task StartAsync() 29 | { 30 | return Task.WhenAll(_bots.Select(bot => bot.StartAsync())); 31 | } 32 | 33 | /// 34 | public async Task StopAsync() 35 | { 36 | } 37 | } 38 | 39 | /// 40 | /// Discord 适配器配置 41 | /// 42 | public class DiscordAdapterConfig 43 | { 44 | /// 45 | /// bot 配置列表 46 | /// 47 | public List Bots { get; set; } = new(); 48 | 49 | /// 50 | /// 代理地址,为空则不使用代理 51 | /// 52 | public string? Proxy { get; set; } = null; 53 | } 54 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.Discord/DiscordBot.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using Discord; 3 | using Discord.Net.Rest; 4 | using Discord.Net.WebSockets; 5 | using Discord.WebSocket; 6 | using Flandre.Core.Common; 7 | using Flandre.Core.Events; 8 | using Flandre.Core.Messaging; 9 | 10 | namespace Flandre.Adapters.Discord; 11 | 12 | /// 13 | /// Discord Bot 14 | /// 15 | public sealed partial class DiscordBot : Bot 16 | { 17 | /// 18 | public override string Platform => "discord"; 19 | 20 | /// 21 | public override string SelfId { get; } 22 | 23 | /// 24 | /// Discord Client 25 | /// 26 | /// 27 | public DiscordSocketClient Internal { get; } 28 | 29 | private readonly DiscordBotConfig _config; 30 | 31 | /// 32 | public override event BotEventHandler? MessageReceived; 33 | 34 | /// 35 | public override event BotEventHandler? GuildInvited; 36 | 37 | /// 38 | public override event BotEventHandler? GuildJoinRequested; 39 | 40 | /// 41 | public override event BotEventHandler? FriendRequested; 42 | 43 | internal DiscordBot(DiscordBotConfig config, string? proxy) 44 | { 45 | _config = config; 46 | Internal = new DiscordSocketClient(new DiscordSocketConfig 47 | { 48 | GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.MessageContent, 49 | RestClientProvider = DefaultRestClientProvider.Create(proxy is not null), 50 | WebSocketProvider = DefaultWebSocketProvider.Create(proxy is null ? null : new WebProxy(proxy)) 51 | }); 52 | SelfId = config.SelfId; 53 | 54 | // See InternalEventHandlers.cs 55 | Internal.Log += InternalOnLog; 56 | Internal.MessageReceived += InternalOnMessageReceived; 57 | } 58 | 59 | /// 60 | public override async Task StartAsync() 61 | { 62 | if (_config.Token is null) 63 | throw new DiscordException("Bot token cannot be null."); 64 | 65 | await Internal.LoginAsync(TokenType.Bot, _config.Token); 66 | await Internal.StartAsync(); 67 | } 68 | 69 | /// 70 | public override async Task SendChannelMessageAsync(string channelId, MessageContent content, 71 | string? guildId = null) 72 | { 73 | if (!InternalUtils.CheckIsValidId(channelId, out var parsed, this, "channel")) 74 | return null; 75 | 76 | var channel = await Internal.GetChannelAsync(parsed); 77 | if (channel is not SocketTextChannel textChannel) 78 | return null; 79 | 80 | var msg = await textChannel.SendMessageAsync(content.GetText()); 81 | return msg?.Id.ToString(); 82 | } 83 | 84 | /// 85 | public override async Task SendPrivateMessageAsync(string userId, MessageContent content) 86 | { 87 | if (!InternalUtils.CheckIsValidId(userId, out var parsed, this, "user")) 88 | return null; 89 | 90 | var user = await Internal.GetUserAsync(parsed); 91 | var msg = await user.SendMessageAsync(content.GetText()); 92 | return msg?.Id.ToString(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.Discord/DiscordBotConfig.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Common; 2 | 3 | namespace Flandre.Adapters.Discord; 4 | 5 | /// 6 | /// Discord Bot 配置 7 | /// 8 | public class DiscordBotConfig : BotConfig 9 | { 10 | /// 11 | /// Bot Token 12 | /// 13 | public string? Token { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.Discord/DiscordException.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Adapters.Discord; 2 | 3 | /// 4 | /// Discord 异常 5 | /// 6 | public sealed class DiscordException : Exception 7 | { 8 | internal DiscordException(string message) : base(message) { } 9 | } 10 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.Discord/Flandre.Adapters.Discord.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Flandre.Adapters.Discord 5 | 0.1.1 6 | FlandreDevs,bsdayo 7 | Discord adapter for Flandre project, based on Discord.Net. 8 | bot;chatbot;flandre;adapter;discord 9 | MIT 10 | avatar.jpg 11 | 12 | net6.0 13 | enable 14 | enable 15 | Library 16 | true 17 | 18 | https://github.com/FlandreDevs/Flandre 19 | https://github.com/FlandreDevs/Flandre.git 20 | git 21 | FlandreDevs (C) 2023 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.Discord/InternalEventHandlers.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | using Discord.WebSocket; 3 | using Flandre.Core.Common; 4 | using Flandre.Core.Events; 5 | using Flandre.Core.Messaging; 6 | using Flandre.Core.Models; 7 | 8 | namespace Flandre.Adapters.Discord; 9 | 10 | public sealed partial class DiscordBot 11 | { 12 | private Task InternalOnLog(LogMessage log) 13 | { 14 | var logLevel = log.Severity switch 15 | { 16 | LogSeverity.Critical => BotLogLevel.Critical, 17 | LogSeverity.Error => BotLogLevel.Error, 18 | LogSeverity.Warning => BotLogLevel.Warning, 19 | LogSeverity.Info => BotLogLevel.Information, 20 | LogSeverity.Verbose => BotLogLevel.Trace, 21 | LogSeverity.Debug => BotLogLevel.Debug, 22 | 23 | _ => BotLogLevel.Debug 24 | }; 25 | 26 | Log(logLevel, log.Message); 27 | if (log.Exception is { } ex) 28 | Log(logLevel, ex.ToString()); 29 | 30 | return Task.CompletedTask; 31 | } 32 | 33 | private Task InternalOnMessageReceived(SocketMessage message) 34 | { 35 | MessageReceived?.Invoke(this, new BotMessageReceivedEvent(new Message 36 | { 37 | Time = message.Timestamp.DateTime, 38 | Platform = Platform, 39 | Environment = message.Channel is SocketDMChannel ? MessageEnvironment.Private : MessageEnvironment.Channel, 40 | MessageId = message.Id.ToString(), 41 | GuildId = message.Author is IGuildUser gu ? gu.Guild.Id.ToString() : null, 42 | ChannelId = message.Channel.Id.ToString(), 43 | Sender = new User 44 | { 45 | Name = message.Author.Username, 46 | Nickname = message.Author.Username, 47 | UserId = message.Author.Id.ToString(), 48 | AvatarUrl = message.Author.GetAvatarUrl() 49 | }, 50 | Content = message.Content 51 | })); 52 | 53 | return Task.CompletedTask; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.Discord/InternalUtils.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Common; 2 | 3 | namespace Flandre.Adapters.Discord; 4 | 5 | internal static class InternalUtils 6 | { 7 | internal static bool CheckIsValidId(string id, out ulong parsed, Bot bot, string idType) 8 | { 9 | var result = ulong.TryParse(id, out parsed); 10 | if (!result) 11 | bot.Log(BotLogLevel.Warning, 12 | $"Invalid {idType} id passed to Discord API. Must be parsable to uint64. Ignoring."); 13 | return result; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.Konata.Extensions/AdapterCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Framework; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace Flandre.Adapters.Konata.Extensions; 6 | 7 | /// 8 | /// Konata 适配器扩展 9 | /// 10 | public static class AdapterCollectionExtensions 11 | { 12 | /// 13 | /// 添加 Konata 适配器,自动从配置根中的 Adapters:Konata 项读取配置。 14 | /// 15 | public static void AddKonata(this IAdapterCollection adapters) 16 | { 17 | var config = adapters.Services 18 | .BuildServiceProvider() 19 | .GetRequiredService() 20 | .GetSection("Adapters:Konata") 21 | .Get(); 22 | adapters.Add(new KonataAdapter(config ?? new KonataAdapterConfig())); 23 | } 24 | 25 | /// 26 | /// 添加 Konata 适配器。 27 | /// 28 | public static void AddKonata(this IAdapterCollection adapters, IConfiguration configuration) 29 | { 30 | var config = configuration.Get(); 31 | adapters.Add(new KonataAdapter(config ?? new KonataAdapterConfig())); 32 | } 33 | 34 | /// 35 | /// 添加 Konata 适配器。 36 | /// 37 | public static void AddKonata(this IAdapterCollection adapters, Action action) 38 | { 39 | var config = new KonataAdapterConfig(); 40 | action(config); 41 | adapters.Add(new KonataAdapter(config)); 42 | } 43 | 44 | /// 45 | /// 添加 Konata 适配器。 46 | /// 47 | public static void AddKonata(this IAdapterCollection adapters, KonataAdapterConfig config) 48 | { 49 | adapters.Add(new KonataAdapter(config)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.Konata.Extensions/Flandre.Adapters.Konata.Extensions.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Flandre.Adapters.Konata.Extensions 5 | 2.0.0-rc.3 6 | FlandreDevs,bsdayo 7 | Flandre.Framework extensions for Flandre.Adapters.Konata. 8 | bot;chatbot;flandre;adapter;konata;extensions 9 | GPL-3.0-only 10 | avatar.jpg 11 | 12 | net6.0 13 | enable 14 | enable 15 | Library 16 | true 17 | 18 | https://github.com/FlandreDevs/Flandre 19 | https://github.com/FlandreDevs/Flandre.git 20 | git 21 | FlandreDevs (C) 2022-2023 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.Konata/Flandre.Adapters.Konata.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Flandre.Adapters.Konata 5 | 2.0.0-rc.3 6 | FlandreDevs,bsdayo 7 | Konata.Core (QQ Protocol) adapter for Flandre project. 8 | bot;chatbot;flandre;adapter;konata 9 | GPL-3.0-only 10 | avatar.jpg 11 | README.md 12 | 13 | net6.0 14 | enable 15 | enable 16 | Library 17 | true 18 | 19 | https://github.com/FlandreDevs/Flandre 20 | https://github.com/FlandreDevs/Flandre.git 21 | git 22 | FlandreDevs (C) 2022-2023 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.Konata/KonataAdapter.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Common; 2 | 3 | namespace Flandre.Adapters.Konata; 4 | 5 | /// 6 | /// Konata 适配器 7 | /// 8 | public class KonataAdapter : IAdapter 9 | { 10 | /// 11 | public IEnumerable Bots => _bots.AsReadOnly(); 12 | 13 | private readonly List _bots = new(); 14 | private readonly KonataAdapterConfig _config; 15 | 16 | /// 17 | /// 构造适配器实例 18 | /// 19 | /// 适配器配置 20 | public KonataAdapter(KonataAdapterConfig config) 21 | { 22 | _config = config; 23 | 24 | _config.Bots.ForEach(bot => 25 | _bots.Add(new KonataBot(bot))); 26 | } 27 | 28 | /// 29 | /// 启动适配器 30 | /// 31 | public Task StartAsync() => Task.WhenAll(_bots.ConvertAll(bot => bot.StartAsync())); 32 | 33 | /// 34 | /// 停止适配器 35 | /// 36 | public Task StopAsync() => Task.WhenAll(_bots.ConvertAll(bot => bot.StopAsync())); 37 | } 38 | 39 | /// 40 | /// Konata 适配器配置 41 | /// 42 | public sealed class KonataAdapterConfig 43 | { 44 | /// 45 | /// 构造 Konata 适配器配置 46 | /// 47 | public KonataAdapterConfig() 48 | { 49 | Bots = new List(); 50 | } 51 | 52 | /// 53 | /// 构造 Konata 适配器配置,并使用已有的 bot 配置列表 54 | /// 55 | /// 56 | public KonataAdapterConfig(List bots) 57 | { 58 | Bots = bots; 59 | } 60 | 61 | /// 62 | /// bot 配置列表 63 | /// 64 | public List Bots { get; init; } 65 | } 66 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.Konata/README.md: -------------------------------------------------------------------------------- 1 | # Flandre.Adapters.Konata 2 | 3 | 基于 [Konata.Core](https://github.com/KonataDev/Konata.Core) 实现的 QQ 协议适配器 4 | 5 | [![NuGet](https://img.shields.io/nuget/vpre/Flandre.Adapters.Konata?label=NuGet&color=blue)](https://www.nuget.org/packages/Flandre.Adapters.Konata/) 6 | [![NuGet Downloads](https://img.shields.io/nuget/dt/Flandre.Adapters.Konata?label=Downloads&color=f06292)](https://www.nuget.org/packages/Flandre.Adapters.Konata/) 7 | 8 | ## 小贴士 9 | 10 | - 由于 Konata 机制,在接收到的消息中,图片消息段 (`ImageSegment`) 将固定只包含 `Url` 属性,为图片的链接,需要自行下载。 11 | - ~~想到什么再补~~ 12 | 13 | ## 开源许可 14 | 由于 GPL v3 协议的传染性,不同于 Flandre 项目其他部分,Flandre.Adapters.Konata 采用 [GPL v3](./LICENSE) 协议开源。 15 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.Konata/Utils.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Messaging; 2 | using Flandre.Core.Messaging.Segments; 3 | using Flandre.Core.Models; 4 | using Konata.Core.Message; 5 | using Konata.Core.Message.Model; 6 | using FlandreMessageBuilder = Flandre.Core.Messaging.MessageBuilder; 7 | using KonataMessageBuilder = Konata.Core.Message.MessageBuilder; 8 | 9 | namespace Flandre.Adapters.Konata; 10 | 11 | internal static class CommonUtils 12 | { 13 | public static string GetAvatarUrl(uint userId) 14 | { 15 | return $"http://q.qlogo.cn/headimg_dl?dst_uin={userId}&spec=640"; 16 | } 17 | } 18 | 19 | internal static class MessageUtils 20 | { 21 | internal static Message ToFlandreMessage(this MessageStruct message, string platform) 22 | { 23 | var mb = new FlandreMessageBuilder(); 24 | 25 | foreach (var chain in message.Chain) 26 | switch (chain) 27 | { 28 | case TextChain textChain: 29 | mb.Text(textChain.Content); 30 | break; 31 | 32 | case ImageChain imageChain: 33 | mb.Image(ImageSegment.FromUrl(imageChain.ImageUrl)); 34 | break; 35 | } 36 | 37 | var groupId = message.Type == MessageStruct.SourceType.Group 38 | ? message.Receiver.Uin.ToString() 39 | : null; 40 | 41 | return new Message 42 | { 43 | Time = DateTimeOffset.FromUnixTimeSeconds(message.Time).LocalDateTime, 44 | Platform = platform, 45 | Environment = message.Type == MessageStruct.SourceType.Group 46 | ? MessageEnvironment.Channel 47 | : MessageEnvironment.Private, 48 | MessageId = message.Uuid.ToString(), 49 | GuildId = groupId, 50 | ChannelId = groupId, 51 | Sender = new User 52 | { 53 | Name = message.Sender.Name, 54 | // Nickname = message.Sender.Name, (can't get user's nickname) 55 | UserId = message.Sender.Uin.ToString(), 56 | AvatarUrl = CommonUtils.GetAvatarUrl(message.Sender.Uin) 57 | }, 58 | Content = mb.Build() 59 | }; 60 | } 61 | 62 | internal static MessageChain ToKonataMessageChain(this MessageContent content) 63 | { 64 | var mb = new KonataMessageBuilder(); 65 | 66 | var prefixChecked = false; 67 | foreach (var segment in content) 68 | switch (segment) 69 | { 70 | case QuoteSegment quoteSegment: 71 | if (!prefixChecked) 72 | { 73 | var messageStruct = new MessageStruct( 74 | uint.Parse(quoteSegment.QuotedMessage.Sender.UserId), 75 | quoteSegment.QuotedMessage.Sender.Nickname, 76 | quoteSegment.QuotedMessage.Content.ToKonataMessageChain(), 77 | quoteSegment.QuotedMessage.Environment == 78 | MessageEnvironment.Channel 79 | ? MessageStruct.SourceType.Group 80 | : MessageStruct.SourceType.Friend); 81 | mb.Add(ReplyChain.Create(messageStruct)); 82 | prefixChecked = true; 83 | } 84 | 85 | break; 86 | 87 | case TextSegment textSegment: 88 | mb.Text(textSegment.Text); 89 | break; 90 | 91 | case ImageSegment imageSegment: 92 | if (imageSegment.Path is not null) 93 | mb.Image(imageSegment.Path); 94 | else if (imageSegment.Data is not null) 95 | mb.Image(imageSegment.Data); 96 | else if (imageSegment.Url is not null) 97 | mb.Add(ImageChain.CreateFromUrl(imageSegment.Url)); 98 | break; 99 | } 100 | 101 | return mb.Build(); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.Mock/Extensions.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Messaging; 2 | 3 | namespace Flandre.Adapters.Mock; 4 | 5 | public static class MockAdapterExtensions 6 | { 7 | public static MockClient GetChannelClient(this MockAdapter adapter, string guildId, string channelId, 8 | string userId) 9 | { 10 | return new MockClient(adapter) 11 | { 12 | EnvironmentType = MessageEnvironment.Channel, 13 | GuildId = guildId, 14 | ChannelId = channelId, 15 | UserId = userId 16 | }; 17 | } 18 | 19 | public static MockClient GetChannelClient(this MockAdapter adapter) 20 | { 21 | return GetChannelClient(adapter, Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), 22 | Guid.NewGuid().ToString()); 23 | } 24 | 25 | public static MockClient GetFriendClient(this MockAdapter adapter, string userId) 26 | { 27 | return new MockClient(adapter) 28 | { 29 | EnvironmentType = MessageEnvironment.Private, 30 | UserId = userId 31 | }; 32 | } 33 | 34 | public static MockClient GetFriendClient(this MockAdapter adapter) 35 | { 36 | return GetFriendClient(adapter, Guid.NewGuid().ToString()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.Mock/Flandre.Adapters.Mock.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Flandre.Adapters.Mock 5 | 1.0.0-rc.4 6 | FlandreDevs,bsdayo 7 | Mock client for Project Flandre. 8 | bot;chatbot;flandre;adapter 9 | MIT 10 | avatar.jpg 11 | 12 | net6.0 13 | enable 14 | enable 15 | Library 16 | true 17 | CS1591 18 | 19 | https://github.com/FlandreDevs/Flandre 20 | https://github.com/FlandreDevs/Flandre.git 21 | git 22 | FlandreDevs (C) 2022-2023 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.Mock/MockAdapter.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Common; 2 | 3 | #pragma warning disable CS1998 4 | 5 | namespace Flandre.Adapters.Mock; 6 | 7 | public class MockAdapter : IAdapter 8 | { 9 | public IEnumerable Bots => new[] { Bot }; 10 | 11 | internal readonly MockBot Bot = new(); 12 | 13 | public Task StartAsync() => Task.CompletedTask; 14 | 15 | public Task StopAsync() => Task.CompletedTask; 16 | } 17 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.Mock/MockBot.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Common; 2 | using Flandre.Core.Events; 3 | using Flandre.Core.Messaging; 4 | using Flandre.Core.Models; 5 | 6 | #pragma warning disable CS1998 7 | 8 | namespace Flandre.Adapters.Mock; 9 | 10 | public class MockBot : Bot 11 | { 12 | /// 13 | /// Bot 平台名称,值为 mock 14 | /// 15 | public override string Platform => "mock"; 16 | 17 | public override string SelfId => _selfId; 18 | 19 | private readonly string _selfId = Guid.NewGuid().ToString(); 20 | 21 | internal (string MessageId, TaskCompletionSource Tcs)? ReplyTarget { get; set; } 22 | 23 | internal void ReceiveMessage(Message message) 24 | { 25 | MessageReceived?.Invoke(this, new BotMessageReceivedEvent(message)); 26 | } 27 | 28 | private void Send(MessageContent? content) 29 | { 30 | ReplyTarget?.Tcs.TrySetResult(content); 31 | } 32 | 33 | public override async Task SendChannelMessageAsync(string channelId, MessageContent content, 34 | string? guildId = null) 35 | { 36 | Send(content); 37 | return null; 38 | } 39 | 40 | public override async Task SendPrivateMessageAsync(string userId, MessageContent content) 41 | { 42 | Send(content); 43 | return null; 44 | } 45 | 46 | public override async Task GetSelfAsync() 47 | { 48 | return new User 49 | { 50 | Name = "Test Bot", 51 | Nickname = "Test Bot", 52 | UserId = _selfId 53 | }; 54 | } 55 | 56 | public override event BotEventHandler? MessageReceived; 57 | public override event BotEventHandler? GuildInvited; 58 | public override event BotEventHandler? GuildJoinRequested; 59 | public override event BotEventHandler? FriendRequested; 60 | } 61 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.Mock/MockClient.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Messaging; 2 | using Flandre.Core.Models; 3 | 4 | namespace Flandre.Adapters.Mock; 5 | 6 | public class MockClient 7 | { 8 | private readonly MockAdapter _adapter; 9 | 10 | public string GuildId { get; internal init; } = string.Empty; 11 | public string ChannelId { get; internal init; } = string.Empty; 12 | public string UserId { get; internal init; } = string.Empty; 13 | 14 | public MessageEnvironment EnvironmentType { get; internal init; } 15 | 16 | internal MockClient(MockAdapter adapter) 17 | { 18 | _adapter = adapter; 19 | } 20 | 21 | private Message ConstructMessage(string message) 22 | { 23 | return new Message 24 | { 25 | Time = DateTime.Now, 26 | Environment = EnvironmentType, 27 | MessageId = Guid.NewGuid().ToString(), 28 | GuildId = GuildId, 29 | ChannelId = ChannelId, 30 | Sender = new GuildMember 31 | { 32 | Name = "Test Client", 33 | Nickname = "Test Client", 34 | UserId = UserId, 35 | AvatarUrl = null, 36 | Roles = new List() 37 | }, 38 | Content = message 39 | }; 40 | } 41 | 42 | public void SendMessage(string message) 43 | { 44 | var msg = ConstructMessage(message); 45 | _adapter.Bot.ReceiveMessage(msg); 46 | } 47 | 48 | public Task SendMessageForReplyAsync(string message) 49 | { 50 | return SendMessageForReplyAsync(message, TimeSpan.FromSeconds(10)); 51 | } 52 | 53 | public Task SendMessageForReplyAsync(string message, TimeSpan timeout) 54 | { 55 | var tcs = new TaskCompletionSource(); 56 | 57 | var msg = ConstructMessage(message); 58 | 59 | _adapter.Bot.ReplyTarget = (msg.MessageId, tcs); 60 | _adapter.Bot.ReceiveMessage(msg); 61 | 62 | Task.Run(async () => 63 | { 64 | await Task.Delay(timeout); 65 | if (_adapter.Bot.ReplyTarget is { } target 66 | && target.MessageId == msg.MessageId 67 | && !target.Tcs.Task.IsCompleted) 68 | { 69 | _adapter.Bot.ReplyTarget = null; 70 | tcs.TrySetResult(null); 71 | } 72 | }); 73 | 74 | return tcs.Task; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.Mock/MockClientExtensions.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Messaging; 2 | 3 | namespace Flandre.Adapters.Mock; 4 | 5 | public static class MockClientExtensions 6 | { 7 | public static MessageContent? SendMessageForReply(this MockClient client, string message) 8 | { 9 | return client.SendMessageForReplyAsync(message).GetAwaiter().GetResult(); 10 | } 11 | 12 | public static MessageContent? SendMessageForReply(this MockClient client, string message, TimeSpan timeout) 13 | { 14 | return client.SendMessageForReplyAsync(message, timeout).GetAwaiter().GetResult(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.OneBot.Extensions/AdapterCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Framework; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace Flandre.Adapters.OneBot.Extensions; 6 | 7 | /// 8 | /// OneBot 适配器扩展 9 | /// 10 | public static class AdapterCollectionExtensions 11 | { 12 | /// 13 | /// 添加 OneBot 适配器,自动从配置根中的 Adapters:OneBot 项读取配置。 14 | /// 15 | public static void AddOneBot(this IAdapterCollection adapters) 16 | { 17 | var config = adapters.Services 18 | .BuildServiceProvider() 19 | .GetRequiredService() 20 | .GetSection("Adapters:OneBot") 21 | .Get(); 22 | adapters.Add(new OneBotAdapter(config ?? new OneBotAdapterConfig())); 23 | } 24 | 25 | /// 26 | /// 添加 OneBot 适配器。 27 | /// 28 | public static void AddOneBot(this IAdapterCollection adapters, IConfiguration configuration) 29 | { 30 | var config = configuration.Get(); 31 | adapters.Add(new OneBotAdapter(config ?? new OneBotAdapterConfig())); 32 | } 33 | 34 | /// 35 | /// 添加 OneBot 适配器。 36 | /// 37 | public static void AddOneBot(this IAdapterCollection adapters, Action action) 38 | { 39 | var config = new OneBotAdapterConfig(); 40 | action(config); 41 | adapters.Add(new OneBotAdapter(config)); 42 | } 43 | 44 | /// 45 | /// 添加 OneBot 适配器。 46 | /// 47 | public static void AddOneBot(this IAdapterCollection adapters, OneBotAdapterConfig config) 48 | { 49 | adapters.Add(new OneBotAdapter(config)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.OneBot.Extensions/Flandre.Adapters.OneBot.Extensions.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Flandre.Adapters.OneBot.Extensions 5 | 2.0.0-rc.3 6 | FlandreDevs,bsdayo 7 | Flandre.Framework extensions for Flandre.Adapters.OneBot. 8 | bot;chatbot;flandre;adapter;onebot;extensions 9 | MIT 10 | avatar.jpg 11 | 12 | net6.0 13 | enable 14 | enable 15 | Library 16 | true 17 | 18 | https://github.com/FlandreDevs/Flandre 19 | https://github.com/FlandreDevs/Flandre.git 20 | git 21 | FlandreDevs (C) 2022-2023 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.OneBot/CqCodeParser.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Flandre.Adapters.OneBot.Segments; 3 | using Flandre.Core.Messaging; 4 | using Flandre.Core.Messaging.Segments; 5 | using Flandre.Core.Utils; 6 | 7 | namespace Flandre.Adapters.OneBot; 8 | 9 | public static class CqCodeParser 10 | { 11 | /// 12 | /// 将含有 CQ 码的消息解析为 。 13 | /// 14 | /// 含有 CQ 码的消息 15 | public static MessageContent ParseCqMessage(string message) 16 | { 17 | if (string.IsNullOrEmpty(message)) 18 | return ""; 19 | 20 | var parser = new StringParser(message); 21 | var segments = new List(); 22 | 23 | while (!parser.IsEnd) 24 | if (parser.Current == '[') 25 | { 26 | // CQCode 27 | var code = parser.Read(']', true); 28 | var sections = code[4..^1].Split(','); 29 | segments.Add(sections[0] switch 30 | { 31 | "face" => ParseFace(sections[1..]), 32 | "record" => ParseRecord(sections[1..]), 33 | "image" => ParseImage(sections[1..]), 34 | "at" => ParseAt(sections[1..]), 35 | _ => new TextSegment(code) 36 | }); 37 | } 38 | else 39 | { 40 | var text = parser.Read('['); 41 | segments.Add(new TextSegment(OneBotUtils.UnescapeCqCode(text))); 42 | } 43 | 44 | return new MessageContent(segments); 45 | } 46 | 47 | public static string ToCqMessage(this MessageContent content) 48 | { 49 | var sb = new StringBuilder(); 50 | foreach (var segment in content) 51 | sb.Append(segment.ToCqCode()); 52 | return sb.ToString(); 53 | } 54 | 55 | public static string ToCqCode(this MessageSegment segment) 56 | { 57 | switch (segment) 58 | { 59 | case TextSegment ts: 60 | return OneBotUtils.EscapeCqCode(ts.Text); 61 | 62 | case FaceSegment fs: 63 | return $"[CQ:face,id={fs.FaceId}]"; 64 | 65 | case AudioSegment aus: 66 | if (aus.Data is not null) 67 | return $"[CQ:record,file=base64://{Convert.ToBase64String(aus.Data)}]"; 68 | if (aus.Path is not null) 69 | return $"[CQ:record,file={aus.Path}]"; 70 | if (aus.Url is not null) 71 | return $"[CQ:record,file={aus.Url}]"; 72 | break; 73 | 74 | case ImageSegment ims: 75 | var type = ims.Type is null ? "" : $",type={ims.Type}"; 76 | if (ims.Data is not null) 77 | return $"[CQ:image,file=base64://{Convert.ToBase64String(ims.Data)}{type}]"; 78 | if (ims.Path is not null) 79 | return $"[CQ:image,file={ims.Path}{type}]"; 80 | if (ims.Url is not null) 81 | return $"[CQ:image,file={ims.Url}{type}]"; 82 | break; 83 | 84 | case QuoteSegment qs: 85 | return $"[CQ:reply,id={qs.QuotedMessage.MessageId}]"; 86 | 87 | case AtSegment ats: 88 | return $"[CQ:at,qq={(ats.Scope == AtSegmentScope.All ? "all" : ats.UserId)}]"; 89 | } 90 | 91 | return ""; 92 | } 93 | 94 | private static FaceSegment ParseFace(string[] data) 95 | { 96 | return new FaceSegment(data[0][3..]); // id=xxx 97 | } 98 | 99 | private static OneBotRecordSegment ParseRecord(string[] data) 100 | { 101 | var segment = new OneBotRecordSegment(); 102 | foreach (var d in data) 103 | { 104 | var kv = d.Split('='); 105 | switch (kv[0]) 106 | { 107 | case "file": 108 | segment.Filename = kv[1]; 109 | break; 110 | case "url": 111 | segment.Url = string.Join('=', kv[1..]); 112 | break; 113 | case "magic": 114 | segment.Magic = int.Parse(kv[1]); 115 | break; 116 | } 117 | } 118 | 119 | return segment; 120 | } 121 | 122 | private static OneBotImageSegment ParseImage(string[] data) 123 | { 124 | var segment = new OneBotImageSegment(); 125 | foreach (var d in data) 126 | { 127 | var kv = d.Split('='); 128 | switch (kv[0]) 129 | { 130 | case "file": 131 | segment.Filename = kv[1]; 132 | break; 133 | case "type": 134 | segment.Type = kv[1]; 135 | break; 136 | case "subType": 137 | segment.SubType = kv[1]; 138 | break; 139 | case "url": 140 | segment.Url = string.Join('=', kv[1..]); 141 | break; 142 | case "id": 143 | segment.Id = int.Parse(kv[1]); 144 | break; 145 | } 146 | } 147 | 148 | return segment; 149 | } 150 | 151 | private static AtSegment ParseAt(string[] data) 152 | { 153 | return new AtSegment(data[0][3..]); // qq=xxx 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.OneBot/Flandre.Adapters.OneBot.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Flandre.Adapters.OneBot 5 | 2.0.0-rc.4 6 | FlandreDevs,bsdayo 7 | OneBot protocol adapter for Flandre project. 8 | bot;chatbot;flandre;adapter;onebot 9 | MIT 10 | avatar.jpg 11 | README.md 12 | 13 | net6.0 14 | enable 15 | enable 16 | Library 17 | true 18 | 1591 19 | 20 | https://github.com/FlandreDevs/Flandre 21 | https://github.com/FlandreDevs/Flandre.git 22 | git 23 | FlandreDevs (C) 2022-2023 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.OneBot/GuildBot.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Adapters.OneBot.Models; 2 | using Flandre.Core.Common; 3 | using Flandre.Core.Events; 4 | using Flandre.Core.Messaging; 5 | using Flandre.Core.Models; 6 | 7 | #pragma warning disable CS0067 8 | 9 | namespace Flandre.Adapters.OneBot; 10 | 11 | public class OneBotGuildBot : Bot 12 | { 13 | /// 14 | /// Bot 平台名称,值为 qqguild 15 | /// 16 | public override string Platform => "qqguild"; 17 | 18 | public override string SelfId => _selfId; 19 | 20 | private string _selfId = string.Empty; 21 | private bool _isSelfIdSet; 22 | 23 | public OneBotGuildInternalBot Internal { get; } 24 | 25 | public override event BotEventHandler? MessageReceived; 26 | public override event BotEventHandler? GuildInvited; 27 | public override event BotEventHandler? GuildJoinRequested; 28 | public override event BotEventHandler? FriendRequested; 29 | 30 | internal OneBotGuildBot(OneBotBot mainBot) 31 | { 32 | Internal = new OneBotGuildInternalBot(mainBot); 33 | } 34 | 35 | internal void InvokeMessageEvent(OneBotApiGuildMessageEvent e) 36 | { 37 | if (!_isSelfIdSet) 38 | { 39 | _selfId = e.SelfId.ToString(); 40 | _isSelfIdSet = true; 41 | } 42 | 43 | MessageReceived?.Invoke(this, 44 | new BotMessageReceivedEvent(new Message 45 | { 46 | Time = DateTimeOffset.FromUnixTimeSeconds(e.Time).DateTime, 47 | Platform = Platform, 48 | Environment = MessageEnvironment.Channel, 49 | MessageId = e.MessageId!, 50 | GuildId = e.GuildId, 51 | ChannelId = e.ChannelId, 52 | Sender = new User 53 | { 54 | Name = e.Sender.Nickname, 55 | UserId = e.Sender.TinyId! 56 | }, 57 | Content = CqCodeParser.ParseCqMessage(e.Message!) 58 | })); 59 | } 60 | 61 | public override async Task SendChannelMessageAsync(string channelId, MessageContent content, 62 | string? guildId = null) 63 | { 64 | return await Internal.SendGuildChannelMessage(guildId!, channelId, content); 65 | } 66 | 67 | public override async Task GetSelfAsync() 68 | { 69 | var self = await Internal.GetGuildServiceProfile(); 70 | return new User 71 | { 72 | Name = self.Nickname!, 73 | UserId = self.TinyId!, 74 | AvatarUrl = self.AvatarUrl 75 | }; 76 | } 77 | 78 | public override Task GetUserAsync(string userId, string? guildId = null) 79 | { 80 | Log(BotLogLevel.Warning, 81 | $"Platform qqguild does not support method {nameof(GetUserAsync)}. If you need to get the information of guild member, please use method {nameof(GetGuildMemberAsync)} instead."); 82 | return Task.FromResult(null); 83 | } 84 | 85 | public override Task> GetFriendListAsync() 86 | { 87 | return Task.FromResult>(Array.Empty()); 88 | } 89 | 90 | public override async Task GetGuildAsync(string guildId) 91 | { 92 | try 93 | { 94 | var guild = await Internal.GetGuildMetaByGuest(guildId); 95 | return new Guild 96 | { 97 | Id = guild.GuildId!, 98 | Name = guild.GuildName! 99 | }; 100 | } 101 | catch 102 | { 103 | return null; 104 | } 105 | } 106 | 107 | public override async Task> GetGuildListAsync() 108 | { 109 | return (await Internal.GetGuildList()).Select(g => new Guild 110 | { 111 | Id = g.GuildId!, 112 | Name = g.GuildName! 113 | }); 114 | } 115 | 116 | public override async Task GetGuildMemberAsync(string guildId, string userId) 117 | { 118 | try 119 | { 120 | var user = await Internal.GetGuildMemberProfile(guildId, userId); 121 | return new GuildMember 122 | { 123 | Name = user.Nickname!, 124 | UserId = user.TinyId!, 125 | AvatarUrl = user.AvatarUrl, 126 | Roles = user.Roles?.Select(r => r.RoleName!).ToList() ?? new List() 127 | }; 128 | } 129 | catch 130 | { 131 | return null; 132 | } 133 | } 134 | 135 | public override async Task> GetGuildMemberListAsync(string guildId) 136 | { 137 | var list = new List(); 138 | OneBotGuildMemberListResponse resp; 139 | var nextToken = ""; 140 | 141 | do 142 | { 143 | resp = await Internal.GetGuildMemberList(guildId, nextToken); 144 | nextToken = resp.NextToken!; 145 | list.AddRange(resp.Members!); 146 | } while (!resp.Finished); 147 | 148 | return list.Select(m => new GuildMember 149 | { 150 | Name = m.Nickname!, 151 | UserId = m.TinyId!, 152 | Roles = new List { m.RoleName! } 153 | }); 154 | } 155 | 156 | public override async Task GetChannelAsync(string channelId, string? guildId = null) 157 | { 158 | return (await GetChannelListAsync(guildId!)).FirstOrDefault(c => c.Id == channelId); 159 | } 160 | 161 | public override async Task> GetChannelListAsync(string guildId) 162 | { 163 | return (await Internal.GetGuildChannelList(guildId)).Select(c => new Channel 164 | { 165 | Id = c.ChannelId!, 166 | Name = c.ChannelName! 167 | }); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.OneBot/GuildInternalBot.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using Flandre.Adapters.OneBot.Models; 3 | using Flandre.Core.Messaging; 4 | 5 | namespace Flandre.Adapters.OneBot; 6 | 7 | public class OneBotGuildInternalBot 8 | { 9 | private readonly OneBotBot _mainBot; 10 | 11 | internal OneBotGuildInternalBot(OneBotBot mainBot) 12 | { 13 | _mainBot = mainBot; 14 | } 15 | 16 | public async Task GetGuildServiceProfile() 17 | { 18 | return (await _mainBot.SendApiRequest("get_guild_service_profile")) 19 | .Deserialize()!; 20 | } 21 | 22 | public async Task GetGuildList() 23 | { 24 | var list = await _mainBot.SendApiRequest("get_guild_list"); 25 | return list.ValueKind == JsonValueKind.Null 26 | ? Array.Empty() 27 | : list.Deserialize()!; 28 | } 29 | 30 | public async Task GetGuildMetaByGuest(string guildId) 31 | { 32 | return (await _mainBot.SendApiRequest("get_guild_service_profile", 33 | new { guild_id = guildId })) 34 | .Deserialize()!; 35 | } 36 | 37 | public async Task GetGuildChannelList(string guildId, bool noCache = false) 38 | { 39 | return (await _mainBot.SendApiRequest("get_guild_channel_list", new 40 | { 41 | guild_id = guildId, 42 | no_cache = noCache 43 | })) 44 | .Deserialize()!; 45 | } 46 | 47 | public async Task GetGuildMemberList(string guildId, string nextToken = "") 48 | { 49 | return (await _mainBot.SendApiRequest("get_guild_member_list", new 50 | { 51 | guild_id = guildId, 52 | next_token = nextToken 53 | })) 54 | .Deserialize()!; 55 | } 56 | 57 | public async Task GetGuildMemberProfile(string guildId, string userId) 58 | { 59 | return (await _mainBot.SendApiRequest("get_guild_member_profile", new 60 | { 61 | guild_id = guildId, 62 | user_id = userId 63 | })) 64 | .Deserialize()!; 65 | } 66 | 67 | public async Task SendGuildChannelMessage(string guildId, string channelId, string message) 68 | { 69 | return (await _mainBot.SendApiRequest("send_guild_channel_msg", new 70 | { 71 | guild_id = guildId, 72 | channel_id = channelId, 73 | message 74 | })) 75 | .GetProperty("message_id").ToString(); 76 | } 77 | 78 | public Task SendGuildChannelMessage(string guildId, string channelId, MessageContent content) 79 | { 80 | return SendGuildChannelMessage(guildId, channelId, content.ToCqMessage()); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.OneBot/Models/OneBotApiEvent.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Flandre.Adapters.OneBot.Models; 4 | 5 | internal class OneBotApiEvent 6 | { 7 | [JsonPropertyName("time")] 8 | public long Time { get; set; } 9 | 10 | [JsonPropertyName("self_id")] 11 | public long SelfId { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.OneBot/Models/OneBotApiGuildMessageEvent.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Flandre.Adapters.OneBot.Models; 4 | 5 | // 频道专有 6 | internal class OneBotApiGuildMessageEvent : OneBotApiMessageEvent 7 | { 8 | [JsonPropertyName("message_id")] 9 | public new string? MessageId { get; set; } 10 | 11 | [JsonPropertyName("user_id")] 12 | public new string? UserId { get; set; } 13 | 14 | [JsonPropertyName("message")] 15 | public string? Message { get; set; } 16 | 17 | [JsonPropertyName("guild_id")] 18 | public string? GuildId { get; set; } 19 | 20 | [JsonPropertyName("channel_id")] 21 | public string? ChannelId { get; set; } 22 | } 23 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.OneBot/Models/OneBotApiMessageEvent.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | #pragma warning disable CS8618 4 | 5 | namespace Flandre.Adapters.OneBot.Models; 6 | 7 | internal class OneBotApiMessageEvent : OneBotApiEvent 8 | { 9 | [JsonPropertyName("sub_type")] 10 | public string SubType { get; set; } 11 | 12 | [JsonPropertyName("message_id")] 13 | public int MessageId { get; set; } 14 | 15 | [JsonPropertyName("user_id")] 16 | public long UserId { get; set; } 17 | 18 | // [JsonPropertyName("message")] 19 | // public string Message { get; set; } 20 | 21 | [JsonPropertyName("raw_message")] 22 | public string RawMessage { get; set; } 23 | 24 | // [JsonPropertyName("font")] 25 | // public string Font { get; set; } 26 | 27 | [JsonPropertyName("sender")] 28 | public OneBotMessageSender Sender { get; set; } 29 | 30 | [JsonPropertyName("message_type")] 31 | public string MessageType { get; set; } 32 | 33 | // 私聊消息专有 34 | // [JsonPropertyName("temp_source")] 35 | // public int? TempSource { get; set; } 36 | 37 | // 以下为群消息专有 38 | [JsonPropertyName("group_id")] 39 | public long? GroupId { get; set; } 40 | 41 | [JsonPropertyName("anonymous")] 42 | public object? Anonymous { get; set; } 43 | } 44 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.OneBot/Models/OneBotApiRequestEvent.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | #pragma warning disable CS8618 4 | 5 | namespace Flandre.Adapters.OneBot.Models; 6 | 7 | internal class OneBotApiRequestEvent : OneBotApiEvent 8 | { 9 | [JsonPropertyName("request_type")] 10 | public string RequestType { get; set; } 11 | 12 | [JsonPropertyName("flag")] 13 | public string Flag { get; set; } 14 | 15 | [JsonPropertyName("comment")] 16 | public string Comment { get; set; } 17 | 18 | [JsonPropertyName("user_id")] 19 | public long UserId { get; set; } 20 | 21 | [JsonPropertyName("sub_type")] 22 | public string? SubType { get; set; } 23 | 24 | [JsonPropertyName("group_id")] 25 | public long? GroupId { get; set; } 26 | } 27 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.OneBot/Models/OneBotApiResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | #pragma warning disable CS8618 5 | 6 | namespace Flandre.Adapters.OneBot.Models; 7 | 8 | internal class OneBotApiResponse 9 | { 10 | [JsonPropertyName("status")] 11 | public string Status { get; set; } 12 | 13 | [JsonPropertyName("retcode")] 14 | public int RetCode { get; set; } 15 | 16 | [JsonPropertyName("msg")] 17 | public string? Msg { get; set; } 18 | 19 | [JsonPropertyName("wording")] 20 | public string? Wording { get; set; } 21 | 22 | [JsonPropertyName("data")] 23 | public JsonElement Data { get; set; } 24 | 25 | [JsonPropertyName("echo")] 26 | public string Echo { get; set; } 27 | } 28 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.OneBot/Models/OneBotFriend.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | #pragma warning disable CS8618 4 | 5 | namespace Flandre.Adapters.OneBot.Models; 6 | 7 | public class OneBotFriend 8 | { 9 | [JsonPropertyName("user_id")] 10 | public long UserId { get; set; } 11 | 12 | [JsonPropertyName("nickname")] 13 | public string Nickname { get; set; } 14 | 15 | [JsonPropertyName("remark")] 16 | public string Remark { get; set; } 17 | } 18 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.OneBot/Models/OneBotGroup.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | #pragma warning disable CS8618 4 | 5 | namespace Flandre.Adapters.OneBot.Models; 6 | 7 | public class OneBotGroup 8 | { 9 | [JsonPropertyName("group_id")] 10 | public long GroupId { get; set; } 11 | 12 | [JsonPropertyName("group_name")] 13 | public string GroupName { get; set; } 14 | 15 | [JsonPropertyName("group_memo")] 16 | public string GroupMemo { get; set; } 17 | 18 | /// 若 Bot 未加入群,该项将会为 0。 19 | [JsonPropertyName("group_create_time")] 20 | public uint GroupCreateTime { get; set; } 21 | 22 | /// 若 Bot 未加入群,该项将会为 0。 23 | [JsonPropertyName("group_level")] 24 | public uint GroupLevel { get; set; } 25 | 26 | /// 若 Bot 未加入群,该项将会为 0。 27 | [JsonPropertyName("member_count")] 28 | public int MemberCount { get; set; } 29 | 30 | /// 若 Bot 未加入群,该项将会为 0。 31 | [JsonPropertyName("max_member_count")] 32 | public int MaxMemberCount { get; set; } 33 | } 34 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.OneBot/Models/OneBotGroupMember.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | #pragma warning disable CS8618 4 | 5 | namespace Flandre.Adapters.OneBot.Models; 6 | 7 | public class OneBotGroupMember 8 | { 9 | [JsonPropertyName("group_id")] 10 | public long GroupId { get; set; } 11 | 12 | [JsonPropertyName("user_id")] 13 | public long UserId { get; set; } 14 | 15 | [JsonPropertyName("nickname")] 16 | public string Nickname { get; set; } 17 | 18 | [JsonPropertyName("card")] 19 | public string Card { get; set; } 20 | 21 | [JsonPropertyName("sex")] 22 | public string Sex { get; set; } 23 | 24 | [JsonPropertyName("age")] 25 | public int Age { get; set; } 26 | 27 | [JsonPropertyName("area")] 28 | public string Area { get; set; } = ""; 29 | 30 | [JsonPropertyName("join_time")] 31 | public int JoinTime { get; set; } 32 | 33 | [JsonPropertyName("last_sent_time")] 34 | public int LastSentTime { get; set; } 35 | 36 | [JsonPropertyName("level")] 37 | public string Level { get; set; } = ""; 38 | 39 | [JsonPropertyName("role")] 40 | public string Role { get; set; } = ""; 41 | 42 | [JsonPropertyName("unfriendly")] 43 | public bool Unfriendly { get; set; } 44 | 45 | [JsonPropertyName("title")] 46 | public string Title { get; set; } = ""; 47 | 48 | [JsonPropertyName("title_expire_time")] 49 | public long TitleExpireTime { get; set; } 50 | 51 | [JsonPropertyName("card_changeable")] 52 | public bool CardChangeable { get; set; } 53 | 54 | [JsonPropertyName("shut_up_timestamp")] 55 | public long ShutUpTimestamp { get; set; } 56 | } 57 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.OneBot/Models/OneBotGuild.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Flandre.Adapters.OneBot.Models; 4 | 5 | public class OneBotGuild 6 | { 7 | [JsonPropertyName("guild_id")] 8 | public string? GuildId { get; set; } 9 | 10 | [JsonPropertyName("guild_name")] 11 | public string? GuildName { get; set; } 12 | 13 | [JsonPropertyName("guild_display_id")] 14 | public string? GuildDisplayId { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.OneBot/Models/OneBotGuildChannel.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Flandre.Adapters.OneBot.Models; 4 | 5 | public class OneBotGuildChannel 6 | { 7 | [JsonPropertyName("owner_guild_id")] 8 | public string? OwnerGuildId { get; set; } 9 | 10 | [JsonPropertyName("channel_id")] 11 | public string? ChannelId { get; set; } 12 | 13 | [JsonPropertyName("channel_type")] 14 | public int ChannelType { get; set; } 15 | 16 | [JsonPropertyName("channel_name")] 17 | public string? ChannelName { get; set; } 18 | 19 | [JsonPropertyName("create_time")] 20 | public long CreateTime { get; set; } 21 | 22 | [JsonPropertyName("creator_tiny_id")] 23 | public string? CreatorTinyId { get; set; } 24 | 25 | [JsonPropertyName("talk_permission")] 26 | public int TalkPermission { get; set; } 27 | 28 | [JsonPropertyName("visible_type")] 29 | public int VisibleType { get; set; } 30 | 31 | [JsonPropertyName("current_slow_mode")] 32 | public int CurrentSlowMode { get; set; } 33 | 34 | [JsonPropertyName("slow_modes")] 35 | public SlowModeInfo[]? SlowModes { get; set; } 36 | 37 | public class SlowModeInfo 38 | { 39 | [JsonPropertyName("slow_mode_key")] 40 | public int SlowModeKey { get; set; } 41 | 42 | [JsonPropertyName("slow_mode_text")] 43 | public string? SlowModeText { get; set; } 44 | 45 | [JsonPropertyName("speak_frequency")] 46 | public int SpeekFrequency { get; set; } 47 | 48 | [JsonPropertyName("slow_mode_circle")] 49 | public int SlowModeCircle { get; set; } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.OneBot/Models/OneBotGuildMember.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Flandre.Adapters.OneBot.Models; 4 | 5 | public class OneBotGuildMemberListResponse 6 | { 7 | [JsonPropertyName("members")] 8 | public OneBotGuildMember[]? Members { get; set; } 9 | 10 | [JsonPropertyName("finished")] 11 | public bool Finished { get; set; } 12 | 13 | [JsonPropertyName("next_token")] 14 | public string? NextToken { get; set; } 15 | } 16 | 17 | public class OneBotGuildMember 18 | { 19 | [JsonPropertyName("tiny_id")] 20 | public string? TinyId { get; set; } 21 | 22 | [JsonPropertyName("title")] 23 | public string? Title { get; set; } 24 | 25 | [JsonPropertyName("nickname")] 26 | public string? Nickname { get; set; } 27 | 28 | [JsonPropertyName("role_id")] 29 | public string? RoleId { get; set; } 30 | 31 | [JsonPropertyName("role_name")] 32 | public string? RoleName { get; set; } 33 | } 34 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.OneBot/Models/OneBotGuildMemberProfile.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Flandre.Adapters.OneBot.Models; 4 | 5 | public class OneBotGuildMemberProfile : OneBotGuildServiceProfile 6 | { 7 | [JsonPropertyName("join_time")] 8 | public long JoinTime { get; set; } 9 | 10 | [JsonPropertyName("roles")] 11 | public RoleInfo[]? Roles { get; set; } 12 | 13 | public class RoleInfo 14 | { 15 | [JsonPropertyName("role_id")] 16 | public string? RoleId { get; set; } 17 | 18 | [JsonPropertyName("role_name")] 19 | public string? RoleName { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.OneBot/Models/OneBotGuildMeta.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Flandre.Adapters.OneBot.Models; 4 | 5 | public class OneBotGuildMeta 6 | { 7 | [JsonPropertyName("guild_id")] 8 | public string? GuildId { get; set; } 9 | 10 | [JsonPropertyName("guild_name")] 11 | public string? GuildName { get; set; } 12 | 13 | [JsonPropertyName("guild_profile")] 14 | public string? GuildProfile { get; set; } 15 | 16 | [JsonPropertyName("create_time")] 17 | public long CreateTime { get; set; } 18 | 19 | [JsonPropertyName("max_member_count")] 20 | public long MaxMemberCount { get; set; } 21 | 22 | [JsonPropertyName("max_robot_count")] 23 | public long MaxRobotCount { get; set; } 24 | 25 | [JsonPropertyName("max_admin_count")] 26 | public long MaxAdminCount { get; set; } 27 | 28 | [JsonPropertyName("member_count")] 29 | public long MemberCount { get; set; } 30 | 31 | [JsonPropertyName("owner_id")] 32 | public string? OwnerId { get; set; } 33 | } 34 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.OneBot/Models/OneBotGuildServiceProfile.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Flandre.Adapters.OneBot.Models; 4 | 5 | public class OneBotGuildServiceProfile 6 | { 7 | [JsonPropertyName("nickname")] 8 | public string? Nickname { get; set; } 9 | 10 | [JsonPropertyName("tiny_id")] 11 | public string? TinyId { get; set; } 12 | 13 | [JsonPropertyName("avatar_url")] 14 | public string? AvatarUrl { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.OneBot/Models/OneBotLoginInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | #pragma warning disable CS8618 4 | 5 | namespace Flandre.Adapters.OneBot.Models; 6 | 7 | public class OneBotLoginInfo 8 | { 9 | [JsonPropertyName("user_id")] 10 | public long UserId { get; set; } 11 | 12 | [JsonPropertyName("nickname")] 13 | public string Nickname { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.OneBot/Models/OneBotMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | #pragma warning disable CS8618 4 | 5 | namespace Flandre.Adapters.OneBot.Models; 6 | 7 | public class OneBotMessage 8 | { 9 | [JsonPropertyName("message_id")] 10 | public int MessageId { get; set; } 11 | 12 | [JsonPropertyName("real_id")] 13 | public int RealId { get; set; } 14 | 15 | [JsonPropertyName("sender")] 16 | public OneBotMessageSender Sender { get; set; } 17 | 18 | [JsonPropertyName("time")] 19 | public int Time { get; set; } 20 | 21 | [JsonPropertyName("message")] 22 | public object Message { get; set; } 23 | 24 | [JsonPropertyName("raw_message")] 25 | public string RawMessage { get; set; } 26 | } 27 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.OneBot/Models/OneBotMessageSender.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | #pragma warning disable CS8618 4 | 5 | namespace Flandre.Adapters.OneBot.Models; 6 | 7 | public class OneBotMessageSender 8 | { 9 | [JsonPropertyName("user_id")] 10 | public long UserId { get; set; } 11 | 12 | [JsonPropertyName("nickname")] 13 | public string Nickname { get; set; } 14 | 15 | [JsonPropertyName("sex")] 16 | public string Sex { get; set; } 17 | 18 | [JsonPropertyName("age")] 19 | public int Age { get; set; } 20 | 21 | // 以下为群聊专属 22 | 23 | [JsonPropertyName("card")] 24 | public string? Card { get; set; } 25 | 26 | [JsonPropertyName("area")] 27 | public string? Area { get; set; } 28 | 29 | [JsonPropertyName("level")] 30 | public string? Level { get; set; } 31 | 32 | [JsonPropertyName("role")] 33 | public string? Role { get; set; } 34 | 35 | [JsonPropertyName("title")] 36 | public string? Title { get; set; } 37 | 38 | // 以下为频道专属 39 | [JsonPropertyName("tiny_id")] 40 | public string? TinyId { get; set; } 41 | } 42 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.OneBot/Models/OneBotUser.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Flandre.Adapters.OneBot.Models; 4 | 5 | #pragma warning disable CS8618 6 | 7 | public class OneBotUser 8 | { 9 | [JsonPropertyName("user_id")] 10 | public long UserId { get; set; } 11 | 12 | [JsonPropertyName("nickname")] 13 | public string Nickname { get; set; } 14 | 15 | [JsonPropertyName("sex")] 16 | public string Sex { get; set; } 17 | 18 | [JsonPropertyName("age")] 19 | public int Age { get; set; } 20 | 21 | [JsonPropertyName("qid")] 22 | public string Qid { get; set; } 23 | 24 | [JsonPropertyName("level")] 25 | public int Level { get; set; } 26 | 27 | [JsonPropertyName("login_days")] 28 | public int LoginDays { get; set; } 29 | } 30 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.OneBot/OneBotAdapter.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Common; 2 | 3 | namespace Flandre.Adapters.OneBot; 4 | 5 | public class OneBotAdapter : IAdapter 6 | { 7 | public IEnumerable Bots => _bots.AsReadOnly(); 8 | 9 | private readonly List _bots = new(); 10 | 11 | private readonly OneBotAdapterConfig _config; 12 | 13 | public OneBotAdapter(OneBotAdapterConfig config) 14 | { 15 | _config = config; 16 | 17 | foreach (var bot in _config.Bots) 18 | switch (bot.Protocol) 19 | { 20 | case OneBotProtocol.WebSocket: 21 | var obb = new OneBotWebSocketBot(bot); 22 | _bots.Add(obb); 23 | _bots.Add(obb.GuildBot); 24 | break; 25 | 26 | default: 27 | throw new NotSupportedException( 28 | $"For now, the OneBot adapter only supports WebSocket protocol. Skipped initialization of bot {bot.SelfId}."); 29 | } 30 | } 31 | 32 | public async Task StartAsync() 33 | { 34 | await Task.WhenAll(_bots.Select(bot => bot.StartAsync())); 35 | } 36 | 37 | public async Task StopAsync() 38 | { 39 | await Task.WhenAll(_bots.Select(bot => bot.StopAsync())); 40 | } 41 | } 42 | 43 | public class OneBotAdapterConfig 44 | { 45 | /// 46 | /// 构造 OneBot 适配器配置 47 | /// 48 | public OneBotAdapterConfig() 49 | { 50 | Bots = new List(); 51 | } 52 | 53 | /// 54 | /// 构造 OneBot 适配器配置,并使用已有的 bot 配置列表 55 | /// 56 | /// 57 | public OneBotAdapterConfig(List bots) 58 | { 59 | Bots = bots; 60 | } 61 | 62 | /// 63 | /// bot 配置列表 64 | /// 65 | public List Bots { get; init; } 66 | } 67 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.OneBot/OneBotBotConfig.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Common; 2 | 3 | namespace Flandre.Adapters.OneBot; 4 | 5 | /// 6 | /// OneBot 通信协议 7 | /// 8 | public enum OneBotProtocol 9 | { 10 | /// 11 | /// 正向 WebSocket 12 | /// 13 | WebSocket 14 | } 15 | 16 | /// 17 | /// OneBot 平台 Bot 配置 18 | /// 19 | public class OneBotBotConfig : BotConfig 20 | { 21 | /// 22 | /// 连接 OneBot 服务端使用的协议。 23 | /// 24 | public OneBotProtocol Protocol { get; set; } = OneBotProtocol.WebSocket; 25 | 26 | /// 27 | /// 和 OneBot 服务端通信时使用的终结点。 28 | /// 29 | public string? Endpoint { get; set; } = null; 30 | 31 | /// 32 | /// WebSocket 服务端重连等待事件,单位为秒。 33 | /// 34 | public int WebSocketReconnectTimeout { get; set; } = 10; 35 | } 36 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.OneBot/OneBotUtils.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Adapters.OneBot; 2 | 3 | public static class OneBotUtils 4 | { 5 | public static string EscapeCqCode(string source, bool escapeComma = false) 6 | { 7 | if (escapeComma) 8 | source = source.Replace(",", ","); 9 | return source 10 | .Replace("&", "&") 11 | .Replace("[", "[") 12 | .Replace("]", "]"); 13 | } 14 | 15 | public static string UnescapeCqCode(string source, bool unescapeComma = false) 16 | { 17 | if (unescapeComma) 18 | source = source.Replace(",", ","); 19 | return source 20 | .Replace("&", "&") 21 | .Replace("[", "[") 22 | .Replace("]", "]"); 23 | } 24 | 25 | public static string GetUserAvatar(string userId) 26 | { 27 | return $"http://q.qlogo.cn/headimg_dl?dst_uin={userId}&spec=640"; 28 | } 29 | 30 | public static string GetUserAvatar(long userId) 31 | { 32 | return $"http://q.qlogo.cn/headimg_dl?dst_uin={userId}&spec=640"; 33 | } 34 | } 35 | 36 | public class OneBotApiException : Exception 37 | { 38 | public OneBotApiException(string message) : base(message) 39 | { 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.OneBot/README.md: -------------------------------------------------------------------------------- 1 | # Flandre.Adapters.OneBot 2 | 3 | 基于 [OneBot](https://github.com/botuniverse/onebot) 协议实现的 QQ 4 | 协议适配器,主要对 [go-cqhttp](https://github.com/Mrs4s/go-cqhttp) 提供支持。同时基于 go-cqhttp 对 QQ 频道也进行了一定的支持。 5 | 6 | [![NuGet](https://img.shields.io/nuget/vpre/Flandre.Adapters.OneBot?label=NuGet&color=blue)](https://www.nuget.org/packages/Flandre.Adapters.OneBot/) 7 | [![NuGet Downloads](https://img.shields.io/nuget/dt/Flandre.Adapters.OneBot?label=Downloads&color=f06292)](https://www.nuget.org/packages/Flandre.Adapters.OneBot/) 8 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.OneBot/Segments/OneBotImageSegment.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Messaging.Segments; 2 | 3 | namespace Flandre.Adapters.OneBot.Segments; 4 | 5 | public class OneBotImageSegment : ImageSegment 6 | { 7 | public string? Filename { get; set; } 8 | 9 | public string? SubType { get; set; } 10 | 11 | public int? Id { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /src/Flandre.Adapters.OneBot/Segments/OneBotRecordSegment.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Messaging.Segments; 2 | 3 | namespace Flandre.Adapters.OneBot.Segments; 4 | 5 | public class OneBotRecordSegment : AudioSegment 6 | { 7 | public string? Filename { get; set; } 8 | 9 | public int? Magic { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /src/Flandre.Core.Reactive/CoreReactiveExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive.Linq; 2 | using Flandre.Core.Common; 3 | using Flandre.Core.Events; 4 | 5 | namespace Flandre.Core.Reactive; 6 | 7 | public static class CoreReactiveExtensions 8 | { 9 | private static IObservable OnBotEvent( 10 | Action> add, Action> remove) where TEvent : FlandreEvent 11 | { 12 | return Observable.FromEventPattern, TEvent>(add, remove) 13 | .Select(pattern => pattern.EventArgs); 14 | } 15 | 16 | public static IObservable OnLogging(this Bot bot) 17 | { 18 | return OnBotEvent( 19 | add => bot.Logging += add, 20 | remove => bot.Logging -= remove); 21 | } 22 | 23 | public static IObservable OnMessageReceived(this Bot bot) 24 | { 25 | return OnBotEvent( 26 | add => bot.MessageReceived += add, 27 | remove => bot.MessageReceived -= remove); 28 | } 29 | 30 | public static IObservable OnGuildInvited(this Bot bot) 31 | { 32 | return OnBotEvent( 33 | add => bot.GuildInvited += add, 34 | remove => bot.GuildInvited -= remove); 35 | } 36 | 37 | public static IObservable OnGuildJoinRequested(this Bot bot) 38 | { 39 | return OnBotEvent( 40 | add => bot.GuildJoinRequested += add, 41 | remove => bot.GuildJoinRequested -= remove); 42 | } 43 | 44 | public static IObservable OnFriendRequested(this Bot bot) 45 | { 46 | return OnBotEvent( 47 | add => bot.FriendRequested += add, 48 | remove => bot.FriendRequested -= remove); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Flandre.Core.Reactive/Flandre.Core.Reactive.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Flandre.Core.Reactive 5 | 1.0.0-rc.3 6 | FlandreDevs,bsdayo 7 | Reactive Extensions (Rx.NET) support for Flandre.Core. 8 | flandre;rx.net;reactive 9 | MIT 10 | avatar.jpg 11 | 12 | net6.0 13 | enable 14 | enable 15 | Library 16 | true 17 | CS1591 18 | 19 | https://github.com/FlandreDevs/Flandre 20 | https://github.com/FlandreDevs/Flandre.git 21 | git 22 | FlandreDevs (C) 2022-2023 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/Flandre.Core.Reactive/MessageExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive.Linq; 2 | using Flandre.Core.Messaging; 3 | 4 | namespace Flandre.Core.Reactive; 5 | 6 | public static class ObservableMessageExtensions 7 | { 8 | public static IObservable OfPlatform( 9 | this IObservable observable, 10 | params string[] platforms) 11 | { 12 | return observable.Where(msg => 13 | platforms.Any(p => msg.Platform.Equals(p, StringComparison.OrdinalIgnoreCase))); 14 | } 15 | 16 | public static IObservable OfUser( 17 | this IObservable observable, 18 | params string[] userIds) 19 | { 20 | return observable.Where(msg => userIds.Contains(msg.Sender.UserId)); 21 | } 22 | 23 | public static IObservable OfGuild( 24 | this IObservable observable, 25 | params string[] guildIds) 26 | { 27 | return observable.Where(msg => guildIds.Contains(msg.GuildId)); 28 | } 29 | 30 | public static IObservable OfChannel( 31 | this IObservable observable, 32 | params string[] channelIds) 33 | { 34 | return observable.Where(msg => channelIds.Contains(msg.ChannelId)); 35 | } 36 | 37 | public static IObservable InPrivate(this IObservable observable) 38 | { 39 | return observable.Where(msg => msg.Environment == MessageEnvironment.Private); 40 | } 41 | 42 | public static IObservable InChannel(this IObservable observable) 43 | { 44 | return observable.Where(msg => msg.Environment == MessageEnvironment.Channel); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Flandre.Core.Reactive/MessageReceivedExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive.Linq; 2 | using Flandre.Core.Events; 3 | using Flandre.Core.Messaging; 4 | 5 | namespace Flandre.Core.Reactive; 6 | 7 | public static class ObservableMessageReceivedExtensions 8 | { 9 | public static IObservable OfPlatform( 10 | this IObservable observable, 11 | params string[] platforms) 12 | { 13 | return observable.Where(e => 14 | platforms.Any(p => e.Message.Platform.Equals(p, StringComparison.OrdinalIgnoreCase))); 15 | } 16 | 17 | public static IObservable OfUser( 18 | this IObservable observable, 19 | params string[] userIds) 20 | { 21 | return observable.Where(e => userIds.Contains(e.Message.Sender.UserId)); 22 | } 23 | 24 | public static IObservable OfGuild( 25 | this IObservable observable, 26 | params string[] guildIds) 27 | { 28 | return observable.Where(e => guildIds.Contains(e.Message.GuildId)); 29 | } 30 | 31 | public static IObservable OfChannel( 32 | this IObservable observable, 33 | params string[] channelIds) 34 | { 35 | return observable.Where(e => channelIds.Contains(e.Message.ChannelId)); 36 | } 37 | 38 | public static IObservable InPrivate(this IObservable observable) 39 | { 40 | return observable.Where(e => e.Message.Environment == MessageEnvironment.Private); 41 | } 42 | 43 | public static IObservable InChannel(this IObservable observable) 44 | { 45 | return observable.Where(e => e.Message.Environment == MessageEnvironment.Channel); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Flandre.Core/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("Flandre.Framework")] 4 | [assembly: InternalsVisibleTo("Flandre.Core.Tests")] 5 | -------------------------------------------------------------------------------- /src/Flandre.Core/Common/Bot.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Events; 2 | using Flandre.Core.Messaging; 3 | using Flandre.Core.Models; 4 | 5 | namespace Flandre.Core.Common; 6 | 7 | /// 8 | /// 机器人 9 | /// 10 | public abstract partial class Bot 11 | { 12 | /// 13 | /// Bot 所在平台名称 14 | /// 15 | public abstract string Platform { get; } 16 | 17 | /// 18 | /// Bot 自身 ID 19 | /// 20 | public abstract string SelfId { get; } 21 | 22 | /// 23 | /// 日志记录 24 | /// 25 | /// 日志等级 26 | /// 日志消息 27 | public void Log(BotLogLevel level, string message) 28 | { 29 | Logging?.Invoke(this, new BotLoggingEvent(level, message)); 30 | } 31 | 32 | /// 33 | /// 启动 Bot 实例 34 | /// 35 | public virtual Task StartAsync() => Task.CompletedTask; 36 | 37 | /// 38 | /// 停止 Bot 实例 39 | /// 40 | public virtual Task StopAsync() => Task.CompletedTask; 41 | 42 | /// 43 | /// 该方法不受支持,发出警告 44 | /// 45 | /// 方法名称 46 | /// Task.CompletedTask 47 | protected Task LogNotSupportedAsync(string method) 48 | { 49 | Log(BotLogLevel.Debug, $"Platform {Platform} does not support method {method}."); 50 | return Task.CompletedTask; 51 | } 52 | 53 | /// 54 | /// 该方法不受支持,发出警告 55 | /// 56 | /// 方法名称 57 | /// 返回值 58 | /// Task.FromResult<TResult>(result) 59 | protected Task LogNotSupportedAsync(string method, TResult result) 60 | { 61 | Log(BotLogLevel.Debug, $"Platform {Platform} does not support method {method}."); 62 | return Task.FromResult(result); 63 | } 64 | 65 | /// 66 | /// 发送频道 (Channel) 消息 67 | /// 68 | /// Channel ID 69 | /// 消息内容 70 | /// 群组 ID 71 | public virtual Task SendChannelMessageAsync(string channelId, MessageContent content, 72 | string? guildId = null) 73 | => LogNotSupportedAsync(nameof(SendChannelMessageAsync), null); 74 | 75 | /// 76 | /// 发送私聊消息 77 | /// 78 | /// 用户 ID 79 | /// 消息内容 80 | public virtual Task SendPrivateMessageAsync(string userId, MessageContent content) 81 | => LogNotSupportedAsync(nameof(SendPrivateMessageAsync), null); 82 | 83 | /// 84 | /// 删除(撤回)消息 85 | /// 86 | /// 消息 ID 87 | public virtual Task DeleteMessageAsync(string messageId) 88 | => LogNotSupportedAsync(nameof(DeleteMessageAsync)); 89 | 90 | /// 91 | /// 获取自身信息 92 | /// 93 | public virtual Task GetSelfAsync() 94 | => LogNotSupportedAsync(nameof(GetSelfAsync), null); 95 | 96 | /// 97 | /// 获取用户信息 98 | /// 99 | /// 用户 ID 100 | /// 群组 ID 101 | public virtual Task GetUserAsync(string userId, string? guildId = null) 102 | => LogNotSupportedAsync(nameof(GetUserAsync), null); 103 | 104 | /// 105 | /// 获取好友列表 106 | /// 107 | public virtual Task> GetFriendListAsync() 108 | => LogNotSupportedAsync>(nameof(GetFriendListAsync), Array.Empty()); 109 | 110 | /// 111 | /// 获取群组信息 112 | /// 113 | /// 群组 ID 114 | public virtual Task GetGuildAsync(string guildId) 115 | => LogNotSupportedAsync(nameof(GetGuildAsync), null); 116 | 117 | /// 118 | /// 获取群组列表 119 | /// 120 | public virtual Task> GetGuildListAsync() 121 | => LogNotSupportedAsync>(nameof(GetGuildListAsync), Array.Empty()); 122 | 123 | /// 124 | /// 获取群组成员信息 125 | /// 126 | /// 群组 ID 127 | /// 用户 ID 128 | public virtual Task GetGuildMemberAsync(string guildId, string userId) 129 | => LogNotSupportedAsync(nameof(GetGuildMemberAsync), null); 130 | 131 | /// 132 | /// 获取群组成员列表 133 | /// 134 | /// 群组 ID 135 | public virtual Task> GetGuildMemberListAsync(string guildId) 136 | => LogNotSupportedAsync>(nameof(GetGuildListAsync), Array.Empty()); 137 | 138 | /// 139 | /// 获取频道信息 140 | /// 141 | /// 频道 ID 142 | /// 群组 ID 143 | public virtual Task GetChannelAsync(string channelId, string? guildId = null) 144 | => LogNotSupportedAsync(nameof(GetChannelAsync), null); 145 | 146 | /// 147 | /// 获取频道列表 148 | /// 149 | /// 群组 ID 150 | public virtual Task> GetChannelListAsync(string guildId) 151 | => LogNotSupportedAsync>(nameof(GetGuildListAsync), Array.Empty()); 152 | } 153 | 154 | /// 155 | /// Bot 基本配置 156 | /// 157 | public class BotConfig 158 | { 159 | /// 160 | /// 自身 ID 161 | /// 162 | public string SelfId { get; set; } = ""; 163 | } 164 | -------------------------------------------------------------------------------- /src/Flandre.Core/Common/BotContext.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Core.Common; 2 | 3 | /// 4 | /// 基础上下文 5 | /// 6 | public class BotContext 7 | { 8 | /// 9 | /// 当前 bot 实例 10 | /// 11 | public Bot Bot { get; } 12 | 13 | /// 14 | /// 构造上下文 15 | /// 16 | /// bot 实例 17 | public BotContext(Bot bot) 18 | { 19 | Bot = bot; 20 | } 21 | 22 | /// 23 | /// Bot 所在平台,等同于 Bot.Platform。 24 | /// 25 | public string Platform => Bot.Platform; 26 | 27 | /// 28 | /// Bot 自身 ID,等同于 Bot.SelfId。 29 | /// 30 | public string SelfId => Bot.SelfId; 31 | } 32 | -------------------------------------------------------------------------------- /src/Flandre.Core/Common/BotEvents.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Events; 2 | 3 | namespace Flandre.Core.Common; 4 | 5 | /// 6 | /// Bot 事件委托 7 | /// 8 | /// 事件类型 9 | public delegate void BotEventHandler(Bot bot, TEvent e) where TEvent : FlandreEvent; 10 | 11 | public abstract partial class Bot 12 | { 13 | /// 14 | /// 日志记录 15 | /// 16 | public event BotEventHandler? Logging; 17 | 18 | /// 19 | /// 收到消息 20 | /// 21 | public abstract event BotEventHandler? MessageReceived; 22 | 23 | /// 24 | /// 收到群组邀请 25 | /// 26 | public abstract event BotEventHandler? GuildInvited; 27 | 28 | /// 29 | /// 收到加群申请 30 | /// 31 | public abstract event BotEventHandler? GuildJoinRequested; 32 | 33 | /// 34 | /// 收到好友申请 35 | /// 36 | public abstract event BotEventHandler? FriendRequested; 37 | 38 | /// 39 | /// 处理拉群邀请 40 | /// 41 | /// 拉群邀请事件 42 | /// 是否同意 43 | /// 附加说明 44 | public virtual Task HandleGuildInvitationAsync(BotGuildInvitedEvent e, bool approve, string? comment = null) 45 | => LogNotSupportedAsync(nameof(HandleGuildInvitationAsync)); 46 | 47 | /// 48 | /// 处理加群申请 49 | /// 50 | /// 加群申请事件 51 | /// 是否同意 52 | /// 附加说明 53 | public virtual Task HandleGuildJoinRequestAsync(BotGuildJoinRequestedEvent e, bool approve, string? comment = null) 54 | => LogNotSupportedAsync(nameof(HandleGuildJoinRequestAsync)); 55 | 56 | /// 57 | /// 处理好友申请 58 | /// 59 | /// 好友申请事件 60 | /// 是否同意 61 | /// 附加说明 62 | public virtual Task HandleFriendRequestAsync(BotFriendRequestedEvent e, bool approve, string? comment = null) 63 | => LogNotSupportedAsync(nameof(HandleFriendRequestAsync)); 64 | } 65 | -------------------------------------------------------------------------------- /src/Flandre.Core/Common/BotExtensions.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Messaging; 2 | 3 | namespace Flandre.Core.Common; 4 | 5 | /// 6 | /// 机器人扩展方法 7 | /// 8 | public static class BotExtensions 9 | { 10 | /// 11 | /// 发送消息 12 | /// 13 | /// 发送消息的机器人 14 | /// 消息类型 15 | /// 频道 ID 16 | /// 用户 ID 17 | /// 消息内容 18 | /// 群组 ID 19 | public static Task SendMessageAsync(this Bot bot, MessageEnvironment environment, string? channelId, 20 | string? userId, 21 | MessageContent content, 22 | string? guildId = null) 23 | { 24 | return environment switch 25 | { 26 | MessageEnvironment.Channel => bot.SendChannelMessageAsync(channelId!, content, guildId), 27 | MessageEnvironment.Private => bot.SendPrivateMessageAsync(userId!, content), 28 | _ => Task.FromResult(null) 29 | }; 30 | } 31 | 32 | /// 33 | /// 发送消息 34 | /// 35 | /// 发送消息的机器人 36 | /// 消息对象 37 | /// 覆盖消息对象的内容,可选 38 | public static Task SendMessageAsync(this Bot bot, Message message, MessageContent? contentOverride = null) 39 | { 40 | return SendMessageAsync(bot, message.Environment, message.ChannelId, message.Sender.UserId, 41 | contentOverride ?? message.Content, message.GuildId); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Flandre.Core/Common/BotLogLevel.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 2 | 3 | namespace Flandre.Core.Common; 4 | 5 | /// 6 | /// 日志等级,与 Microsoft.Extension.Logging.LogLevel 兼容。 7 | /// 8 | public enum BotLogLevel 9 | { 10 | Trace = 0, 11 | Debug = 1, 12 | Information = 2, 13 | Warning = 3, 14 | Error = 4, 15 | Critical = 5, 16 | None = 6 17 | } 18 | -------------------------------------------------------------------------------- /src/Flandre.Core/Common/IAdapter.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Core.Common; 2 | 3 | /// 4 | /// 适配器接口 5 | /// 6 | public interface IAdapter 7 | { 8 | /// 9 | /// 适配器机器人列表 10 | /// 11 | public IEnumerable Bots { get; } 12 | 13 | /// 14 | /// 启动适配器 15 | /// 16 | public Task StartAsync(); 17 | 18 | /// 19 | /// 停止适配器 20 | /// 21 | public Task StopAsync(); 22 | } 23 | -------------------------------------------------------------------------------- /src/Flandre.Core/Events/BotFriendRequestedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Core.Events; 2 | 3 | /// 4 | /// bot 好友申请事件 5 | /// 6 | public class BotFriendRequestedEvent : FlandreEvent 7 | { 8 | /// 9 | /// 申请人名称 10 | /// 11 | public string RequesterName { get; } 12 | 13 | /// 14 | /// 申请人 ID 15 | /// 16 | public string RequesterId { get; } 17 | 18 | /// 19 | /// 20 | /// 21 | public string Comment { get; } 22 | 23 | /// 24 | /// 构造好友申请事件 25 | /// 26 | /// 申请人名称 27 | /// 申请人 ID 28 | /// 申请备注 29 | public BotFriendRequestedEvent(string requesterName, string requesterId, string comment) 30 | { 31 | RequesterName = requesterName; 32 | RequesterId = requesterId; 33 | Comment = comment; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Flandre.Core/Events/BotGuildInvitedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Core.Events; 2 | 3 | /// 4 | /// bot 收到 Guild 邀请事件 5 | /// 6 | public class BotGuildInvitedEvent : FlandreEvent 7 | { 8 | /// 9 | /// Guild 名称 10 | /// 11 | public string GuildName { get; } 12 | 13 | /// 14 | /// Guild ID 15 | /// 16 | public string GuildId { get; } 17 | 18 | /// 19 | /// 邀请人 ID 20 | /// 21 | public string InviterId { get; } 22 | 23 | /// 24 | /// 邀请人名称 25 | /// 26 | public string InviterName { get; } 27 | 28 | /// 29 | /// 邀请人是否为管理员。如果适配器不支持应始终返回 true。 30 | /// 31 | public bool InviterIsAdmin { get; } 32 | 33 | /// 34 | /// 构造事件 35 | /// 36 | /// Guild 名称 37 | /// Guild ID 38 | /// 邀请人名称 39 | /// 邀请人 ID 40 | /// 邀请人是否为管理员 41 | public BotGuildInvitedEvent(string guildName, string guildId, 42 | string inviterName, string inviterId, bool inviterIsAdmin) 43 | { 44 | GuildName = guildName; 45 | GuildId = guildId; 46 | InviterName = inviterName; 47 | InviterId = inviterId; 48 | InviterIsAdmin = inviterIsAdmin; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Flandre.Core/Events/BotGuildJoinRequestedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Core.Events; 2 | 3 | /// 4 | /// 入群申请事件 5 | /// 6 | public class BotGuildJoinRequestedEvent : FlandreEvent 7 | { 8 | /// 9 | /// Guild 名称 10 | /// 11 | public string GuildName { get; } 12 | 13 | /// 14 | /// Guild ID 15 | /// 16 | public string GuildId { get; } 17 | 18 | /// 19 | /// 申请人名称 20 | /// 21 | public string RequesterName { get; } 22 | 23 | /// 24 | /// 申请人 ID 25 | /// 26 | public string RequesterId { get; } 27 | 28 | /// 29 | /// 申请备注 30 | /// 31 | public string Comment { get; } 32 | 33 | /// 34 | /// 构造事件 35 | /// 36 | /// Guild 名称 37 | /// Guild ID 38 | /// 申请人名称 39 | /// 申请人 ID 40 | /// 申请备注 41 | public BotGuildJoinRequestedEvent(string guildName, string guildId, string requesterName, string requesterId, 42 | string comment) 43 | { 44 | GuildName = guildName; 45 | GuildId = guildId; 46 | RequesterName = requesterName; 47 | RequesterId = requesterId; 48 | Comment = comment; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Flandre.Core/Events/BotLoggingEvent.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Common; 2 | 3 | namespace Flandre.Core.Events; 4 | 5 | /// 6 | /// Bot 日志记录事件 7 | /// 8 | public class BotLoggingEvent : FlandreEvent 9 | { 10 | /// 11 | /// 日志等级 12 | /// 13 | public BotLogLevel LogLevel { get; } 14 | 15 | /// 16 | /// 日志消息 17 | /// 18 | public string LogMessage { get; } 19 | 20 | internal BotLoggingEvent(BotLogLevel level, string message) 21 | { 22 | LogLevel = level; 23 | LogMessage = message; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Flandre.Core/Events/BotMessageReceivedEvent.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Messaging; 2 | 3 | namespace Flandre.Core.Events; 4 | 5 | /// 6 | /// 消息接收事件 7 | /// 8 | public class BotMessageReceivedEvent : FlandreEvent 9 | { 10 | /// 11 | /// 接收到的消息 12 | /// 13 | public Message Message { get; } 14 | 15 | /// 16 | /// 构造事件 17 | /// 18 | /// 接收到的消息 19 | public BotMessageReceivedEvent(Message message) 20 | { 21 | Message = message; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Flandre.Core/Events/FlandreEvent.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Core.Events; 2 | 3 | /// 4 | /// 基础事件 5 | /// 6 | public abstract class FlandreEvent : EventArgs 7 | { 8 | /// 9 | /// 事件时间 10 | /// 11 | public DateTime EventTime { get; init; } 12 | 13 | /// 14 | /// 事件载荷 15 | /// 16 | public object? EventPayload { get; init; } 17 | 18 | internal FlandreEvent() 19 | { 20 | EventTime = DateTime.Now; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Flandre.Core/Flandre.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Flandre.Core 5 | 1.0.0-rc.4 6 | FlandreDevs,bsdayo 7 | 跨平台聊天机器人框架核心,提供基本交互操作,容易嵌入到已有项目中。 8 | bot;chatbot;flandre 9 | MIT 10 | avatar.jpg 11 | README.NuGet.md 12 | 13 | net6.0 14 | enable 15 | enable 16 | Library 17 | true 18 | 19 | https://github.com/FlandreDevs/Flandre 20 | https://github.com/FlandreDevs/Flandre.git 21 | git 22 | FlandreDevs (C) 2022-2023 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/Flandre.Core/Messaging/Message.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Models; 2 | 3 | namespace Flandre.Core.Messaging; 4 | 5 | #pragma warning disable CS8618 6 | 7 | /// 8 | /// 消息结构 9 | /// 10 | public class Message 11 | { 12 | /// 13 | /// 消息时间 14 | /// 15 | public DateTime Time { get; init; } = DateTime.Now; 16 | 17 | /// 18 | /// 平台 ID 19 | /// 20 | public string Platform { get; init; } = string.Empty; 21 | 22 | /// 23 | /// 消息来源类型 24 | /// 25 | public MessageEnvironment Environment { get; init; } 26 | 27 | /// 28 | /// 消息 ID 29 | /// 30 | public string MessageId { get; init; } = string.Empty; 31 | 32 | /// 33 | /// Guild ID 34 | /// 35 | public string? GuildId { get; init; } 36 | 37 | /// 38 | /// Channel ID 39 | /// 40 | public string? ChannelId { get; init; } 41 | 42 | /// 43 | /// 发送者信息 44 | /// 45 | public User Sender { get; init; } 46 | 47 | /// 48 | /// 消息内容 49 | /// 50 | public MessageContent Content { get; init; } 51 | 52 | /// 53 | /// 获取消息内容中的所有文本 54 | /// 55 | public string GetText() 56 | { 57 | return Content.GetText(); 58 | } 59 | } 60 | 61 | /// 62 | /// 消息来源类型 63 | /// 64 | public enum MessageEnvironment 65 | { 66 | /// 67 | /// 来自 Channel 68 | /// 69 | Channel, 70 | 71 | /// 72 | /// 来自私聊 73 | /// 74 | Private 75 | } 76 | -------------------------------------------------------------------------------- /src/Flandre.Core/Messaging/MessageBuilder.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Messaging.Segments; 2 | 3 | namespace Flandre.Core.Messaging; 4 | 5 | /// 6 | /// 消息构造器 7 | /// 8 | public class MessageBuilder 9 | { 10 | /// 11 | /// 已包含消息段 12 | /// 13 | public List Segments = new(); 14 | 15 | /// 16 | /// 添加文本消息段 17 | /// 18 | /// 文本 19 | public MessageBuilder Text(string text) 20 | { 21 | Segments.Add(new TextSegment(text)); 22 | return this; 23 | } 24 | 25 | /// 26 | /// 添加图片消息段 27 | /// 28 | /// 图片消息段 29 | public MessageBuilder Image(ImageSegment imageSegment) 30 | { 31 | Segments.Add(imageSegment); 32 | return this; 33 | } 34 | 35 | /// 36 | /// 添加图片消息段 37 | /// 38 | /// 图片数据 39 | public MessageBuilder Image(byte[] data) 40 | { 41 | return Image(ImageSegment.FromData(data)); 42 | } 43 | 44 | /// 45 | /// 添加消息段 46 | /// 47 | /// 消息段 48 | public MessageBuilder Add(MessageSegment segment) 49 | { 50 | Segments.Add(segment); 51 | return this; 52 | } 53 | 54 | /// 55 | /// 构造为 MessageContent 56 | /// 57 | public MessageContent Build() 58 | { 59 | return new MessageContent(Segments); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Flandre.Core/Messaging/MessageContent.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using Flandre.Core.Messaging.Segments; 3 | 4 | namespace Flandre.Core.Messaging; 5 | 6 | /// 7 | /// 消息内容 8 | /// 9 | public class MessageContent : IEnumerable 10 | { 11 | /// 12 | /// 消息内容包含的消息段 13 | /// 14 | public IEnumerable Segments { get; } 15 | 16 | /// 17 | /// 使用消息段构造消息内容 18 | /// 19 | /// 20 | public MessageContent(IEnumerable segments) 21 | { 22 | Segments = segments; 23 | } 24 | 25 | /// 26 | /// 获取类型匹配的消息段 27 | /// 28 | /// 消息段类型 29 | public TSegment? GetSegment() where TSegment : MessageSegment 30 | { 31 | return (TSegment?)Segments.FirstOrDefault(segment => segment is TSegment); 32 | } 33 | 34 | /// 35 | /// 获取所有类型匹配的消息段 36 | /// 37 | /// 消息段类型 38 | public IEnumerable GetSegments() where TSegment : MessageSegment 39 | { 40 | return Segments.Where(segment => segment is TSegment).Cast(); 41 | } 42 | 43 | /// 44 | /// 获取消息内容中的所有文本 45 | /// 46 | public string GetText() 47 | { 48 | return string.Join("", GetSegments().Select(s => s.Text)); 49 | } 50 | 51 | /// 52 | /// 由 Message 隐式转换 53 | /// 54 | public static implicit operator MessageContent(Message message) 55 | { 56 | return message.Content; 57 | } 58 | 59 | /// 60 | /// 由 MessageBuilder 隐式转换 61 | /// 62 | public static implicit operator MessageContent(MessageBuilder builder) 63 | { 64 | return builder.Build(); 65 | } 66 | 67 | /// 68 | /// 由消息段隐式转换 69 | /// 70 | public static implicit operator MessageContent(MessageSegment segment) 71 | { 72 | return new MessageContent(new[] { segment }); 73 | } 74 | 75 | /// 76 | /// 由字符串隐式转换 77 | /// 78 | public static implicit operator MessageContent(string? text) 79 | { 80 | return text is null 81 | ? new MessageContent(Array.Empty()) 82 | : new MessageContent(new[] { new TextSegment(text) }); 83 | } 84 | 85 | /// 86 | /// 获取 Enumerator 87 | /// 88 | /// 89 | public IEnumerator GetEnumerator() 90 | { 91 | return Segments.GetEnumerator(); 92 | } 93 | 94 | IEnumerator IEnumerable.GetEnumerator() 95 | { 96 | return GetEnumerator(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Flandre.Core/Messaging/MessageContext.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Common; 2 | 3 | namespace Flandre.Core.Messaging; 4 | 5 | /// 6 | /// 消息上下文 7 | /// 8 | public class MessageContext : BotContext 9 | { 10 | /// 11 | /// 当前消息 12 | /// 13 | public Message Message { get; init; } 14 | 15 | /// 16 | /// 构造消息上下文 17 | /// 18 | /// bot 实例 19 | /// 消息 20 | public MessageContext(Bot bot, Message message) 21 | : base(bot) 22 | { 23 | Message = message; 24 | } 25 | 26 | /// 27 | /// 用户 ID,等同于 .Sender.UserId 28 | /// 29 | public string UserId => Message.Sender.UserId; 30 | 31 | /// 32 | /// 群组 ID,等同于 .GuildId 33 | /// 34 | public string? GuildId => Message.GuildId; 35 | 36 | /// 37 | /// 频道 ID,等同于 .ChannelId 38 | /// 39 | public string? ChannelId => Message.ChannelId; 40 | } 41 | -------------------------------------------------------------------------------- /src/Flandre.Core/Messaging/MessageSegment.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Core.Messaging; 2 | 3 | /// 4 | /// 消息段基类 5 | /// 6 | public abstract class MessageSegment 7 | { 8 | } 9 | 10 | /// 11 | /// 内联消息段 12 | /// 13 | public abstract class InlineSegment : MessageSegment 14 | { 15 | } 16 | 17 | /// 18 | /// 资源消息段 19 | /// 20 | public abstract class ResourceSegment : MessageSegment 21 | { 22 | /// 23 | /// 资源数据 24 | /// 25 | public byte[]? Data { get; set; } 26 | 27 | /// 28 | /// 资源文件路径 29 | /// 30 | public string? Path { get; set; } 31 | 32 | /// 33 | /// 资源 URL 34 | /// 35 | public string? Url { get; set; } 36 | } 37 | 38 | /// 39 | /// 前缀消息段 40 | /// 41 | public abstract class PrefixSegment : MessageSegment 42 | { 43 | } 44 | -------------------------------------------------------------------------------- /src/Flandre.Core/Messaging/Segments/AtSegment.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Core.Messaging.Segments; 2 | 3 | /// 4 | /// at 消息段 5 | /// 6 | public class AtSegment : InlineSegment 7 | { 8 | /// 9 | /// 用户 ID 10 | /// 11 | public string? UserId { get; set; } 12 | 13 | /// 14 | /// at 范围 15 | /// 16 | public AtSegmentScope Scope { get; set; } = AtSegmentScope.Single; 17 | 18 | /// 19 | /// 使用用户 ID 构造 at 消息段 20 | /// 21 | /// 用户 ID 22 | public AtSegment(string userId) 23 | { 24 | UserId = userId; 25 | } 26 | 27 | /// 28 | /// 使用用户 ID 构造 at 消息段 29 | /// 30 | /// at 范围 31 | public AtSegment(AtSegmentScope scope) 32 | { 33 | Scope = scope; 34 | } 35 | 36 | /// 37 | /// 快捷 at 全体成员 38 | /// 39 | public static AtSegment AtAll() 40 | { 41 | return new AtSegment(AtSegmentScope.All); 42 | } 43 | } 44 | 45 | /// 46 | /// at 范围 47 | /// 48 | public enum AtSegmentScope 49 | { 50 | /// 51 | /// at 单个用户 52 | /// 53 | Single, 54 | 55 | /// 56 | /// at 全体成员 57 | /// 58 | All 59 | } 60 | -------------------------------------------------------------------------------- /src/Flandre.Core/Messaging/Segments/AudioSegment.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Core.Messaging.Segments; 2 | 3 | /// 4 | /// 语音消息段 5 | /// 6 | public class AudioSegment : ResourceSegment 7 | { 8 | /// 9 | /// 从数据构造 10 | /// 11 | /// 图片数据 12 | public static AudioSegment FromData(byte[] data) 13 | { 14 | return new AudioSegment { Data = data }; 15 | } 16 | 17 | /// 18 | /// 从本地路径构建 19 | /// 20 | /// 本地路径 21 | public static AudioSegment FromPath(string path) 22 | { 23 | return new AudioSegment { Path = path }; 24 | } 25 | 26 | /// 27 | /// 从网络 URL 构建 28 | /// 29 | /// 网络 URL 30 | public static AudioSegment FromUrl(string url) 31 | { 32 | return new AudioSegment { Url = url }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Flandre.Core/Messaging/Segments/FaceSegment.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Core.Messaging.Segments; 2 | 3 | /// 4 | /// 表情消息段 5 | /// 6 | public class FaceSegment : InlineSegment 7 | { 8 | /// 9 | /// 表情 ID 10 | /// 11 | public string FaceId { get; set; } 12 | 13 | /// 14 | /// 构造表情消息段 15 | /// 16 | /// 17 | public FaceSegment(string faceId) 18 | { 19 | FaceId = faceId; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Flandre.Core/Messaging/Segments/ImageSegment.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Core.Messaging.Segments; 2 | 3 | /// 4 | /// 图片消息段 5 | /// 6 | public class ImageSegment : ResourceSegment 7 | { 8 | /// 9 | /// 图片类型,提供给适配器做相应实现 10 | /// 11 | public string? Type { get; set; } 12 | 13 | /// 14 | /// 从数据构造 15 | /// 16 | /// 图片数据 17 | /// 类型 18 | public static ImageSegment FromData(byte[] data, string? type = null) 19 | { 20 | return new ImageSegment 21 | { 22 | Data = data, 23 | Type = type 24 | }; 25 | } 26 | 27 | /// 28 | /// 从本地路径构建 29 | /// 30 | /// 本地路径 31 | /// 类型 32 | public static ImageSegment FromPath(string path, string? type = null) 33 | { 34 | return new ImageSegment 35 | { 36 | Path = path, 37 | Type = type 38 | }; 39 | } 40 | 41 | /// 42 | /// 从网络 URL 构建 43 | /// 44 | /// 网络 URL 45 | /// 类型 46 | public static ImageSegment FromUrl(string url, string? type = null) 47 | { 48 | return new ImageSegment 49 | { 50 | Url = url, 51 | Type = type 52 | }; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Flandre.Core/Messaging/Segments/QuoteSegment.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Core.Messaging.Segments; 2 | 3 | /// 4 | /// 引用消息段(如回复) 5 | /// 6 | public class QuoteSegment : PrefixSegment 7 | { 8 | /// 9 | /// 引用的消息 10 | /// 11 | public Message QuotedMessage { get; set; } 12 | 13 | /// 14 | /// 构造引用消息段 15 | /// 16 | /// 引用的消息 17 | public QuoteSegment(Message message) 18 | { 19 | QuotedMessage = message; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Flandre.Core/Messaging/Segments/TextSegment.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Core.Messaging.Segments; 2 | 3 | /// 4 | /// 文本消息段 5 | /// 6 | public class TextSegment : InlineSegment 7 | { 8 | /// 9 | /// 文本 10 | /// 11 | public string Text { get; set; } 12 | 13 | /// 14 | /// 构造文本消息段 15 | /// 16 | /// 文本 17 | public TextSegment(string text) 18 | { 19 | Text = text; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Flandre.Core/Models/Channel.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Core.Models; 2 | 3 | /// 4 | /// Channel 信息 5 | /// 6 | public class Channel 7 | { 8 | /// 9 | /// Channel ID 10 | /// 11 | public string Id { get; init; } = ""; 12 | 13 | /// 14 | /// Channel 名称 15 | /// 16 | public string Name { get; init; } = ""; 17 | } 18 | -------------------------------------------------------------------------------- /src/Flandre.Core/Models/Guild.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Core.Models; 2 | 3 | /// 4 | /// Guild 信息 5 | /// 6 | public class Guild 7 | { 8 | /// 9 | /// Guild ID 10 | /// 11 | public string Id { get; init; } = ""; 12 | 13 | /// 14 | /// Guild 名称 15 | /// 16 | public string Name { get; init; } = ""; 17 | } 18 | -------------------------------------------------------------------------------- /src/Flandre.Core/Models/GuildMember.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Core.Models; 2 | 3 | /// 4 | /// Guild 成员 5 | /// 6 | public class GuildMember : User 7 | { 8 | /// 9 | /// 成员角色 10 | /// 11 | public List Roles { get; init; } = new(); 12 | } 13 | -------------------------------------------------------------------------------- /src/Flandre.Core/Models/User.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Core.Models; 2 | 3 | /// 4 | /// 用户信息 5 | /// 6 | public class User 7 | { 8 | /// 9 | /// 用户名称 10 | /// 11 | public string Name { get; init; } = ""; 12 | 13 | /// 14 | /// 用户昵称 15 | /// 16 | public string? Nickname { get; init; } 17 | 18 | /// 19 | /// 用户 ID 20 | /// 21 | public string UserId { get; init; } = ""; 22 | 23 | /// 24 | /// 用户头像 URL 25 | /// 26 | public string? AvatarUrl { get; init; } 27 | } 28 | -------------------------------------------------------------------------------- /src/Flandre.Core/Models/UserRole.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Core.Models; 2 | 3 | /// 4 | /// 用户身份 5 | /// 6 | public enum UserRole 7 | { 8 | /// 9 | /// 常规成员 10 | /// 11 | Member, 12 | 13 | /// 14 | /// 管理员 15 | /// 16 | Admin, 17 | 18 | /// 19 | /// 所有者 20 | /// 21 | Owner 22 | } 23 | -------------------------------------------------------------------------------- /src/Flandre.Core/Utils/FlandreCoreUtils.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Messaging; 2 | 3 | namespace Flandre.Core.Utils; 4 | 5 | /// 6 | /// Flandre 工具方法 7 | /// 8 | public static class FlandreCoreUtils 9 | { 10 | /// 11 | /// 自动检测 的属性并异步获取或下载资源。 12 | /// 优先级顺序为:直接返回 Data -> 根据 Path 读取文件 -> 根据 Url 下载文件 13 | /// 14 | /// 资源消息段 15 | public static async Task GetOrDownloadDataAsync(this ResourceSegment resource) 16 | { 17 | if (resource.Data is not null) 18 | return resource.Data; 19 | 20 | if (resource.Path is not null && File.Exists(resource.Path)) 21 | return await File.ReadAllBytesAsync(resource.Path); 22 | 23 | if (resource.Url is not null) 24 | return await new HttpClient().GetByteArrayAsync(resource.Url); 25 | 26 | return null; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Flandre.Core/Utils/StringParser.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Core.Utils; 2 | 3 | /// 4 | /// 字符串解析器 5 | /// 6 | public class StringParser 7 | { 8 | private readonly string _str; 9 | private int _pos; 10 | 11 | /// 12 | /// 左侧引号 13 | /// 14 | public char[] LeftQuotes { get; } 15 | 16 | /// 17 | /// 右侧引号 18 | /// 19 | public char[] RightQuotes { get; } 20 | 21 | /// 22 | /// 当前字符 23 | /// 24 | public char Current => _str[_pos]; 25 | 26 | /// 27 | /// 字符串是否解析完 28 | /// 29 | public bool IsEnd => _pos >= _str.Length; 30 | 31 | // ReSharper disable SuggestBaseTypeForParameterInConstructor 32 | /// 33 | /// 34 | /// 本方法会在 添加 '(直单引号)、"(直双引号)、`(反引号) 三个字符。如果这不是预期的行为,请传入副本 35 | /// 36 | public StringParser(string str, HashSet quoteChars, HashSet<(char Left, char Right)> quotePairs) 37 | { 38 | _str = str; 39 | _ = quoteChars.Add('\''); 40 | _ = quoteChars.Add('"'); 41 | _ = quoteChars.Add('`'); 42 | LeftQuotes = quotePairs.Select(t => t.Left).Concat(quoteChars).Where(c => !char.IsWhiteSpace(c)).ToArray(); 43 | RightQuotes = quotePairs.Select(t => t.Right).Concat(quoteChars).Where(c => !char.IsWhiteSpace(c)).ToArray(); 44 | } 45 | // ReSharper restore SuggestBaseTypeForParameterInConstructor 46 | 47 | /// 48 | /// 构造字符串构造解析器实例 49 | /// 50 | public StringParser(string str, params char[] quoteChars) 51 | : this(str, quoteChars.ToHashSet(), new HashSet<(char Left, char Right)>()) 52 | { 53 | } 54 | 55 | #region Skip 56 | 57 | /// 58 | /// 跳过指定长度 59 | /// 60 | public StringParser Skip(int length) 61 | { 62 | _pos += length; 63 | return this; 64 | } 65 | 66 | /// 67 | /// 跳到指定的字符位置 68 | /// 69 | /// 终点字符 70 | /// 71 | public StringParser Skip(char terminator, bool includeTerminator = false) 72 | { 73 | _pos = IndexOfOrEnd(terminator); 74 | if (includeTerminator) 75 | ++_pos; 76 | return this; 77 | } 78 | 79 | /// 80 | /// 跳到指定的字符位置 81 | /// 82 | /// false时停止 83 | /// 84 | public StringParser SkipWhen(Func predicate, bool includeTerminator = false) 85 | { 86 | while (!(IsEnd || !predicate(Current))) 87 | ++_pos; 88 | if (includeTerminator) 89 | ++_pos; 90 | return this; 91 | } 92 | 93 | /// 94 | /// 跳过空白字符 95 | /// 96 | public StringParser SkipWhiteSpaces() => SkipWhen(char.IsWhiteSpace); 97 | 98 | #endregion 99 | 100 | #region Peek 101 | 102 | /// 103 | /// 读取字符串,但不移动解析器指针 104 | /// 105 | /// 读取字符串的长度 106 | public string Peek(int length) => _str.Substring(_pos, length); 107 | 108 | /// 109 | /// 读取字符串,但不移动解析器指针 110 | /// 111 | /// 终点字符 112 | /// 113 | public string Peek(char terminator, bool includeTerminator = false) 114 | { 115 | var end = IndexOfOrEnd(terminator); 116 | if (includeTerminator) 117 | ++end; 118 | return _str[_pos..end]; 119 | } 120 | 121 | /// 122 | /// 读取字符串,但不移动解析器指针 123 | /// 124 | /// false时停止 125 | /// 126 | public string PeekWhen(Func predicate, bool includeTerminator = false) 127 | { 128 | var cur = _pos; 129 | while (!(cur >= _str.Length || !predicate(_str[cur]))) 130 | ++cur; 131 | if (includeTerminator) 132 | ++cur; 133 | return _str[_pos..cur]; 134 | } 135 | 136 | /// 137 | /// 读取字符串,且移动解析器指针,直至空白字符 138 | /// 139 | public string PeekToWhiteSpace() => PeekWhen(t => !char.IsWhiteSpace(t)); 140 | 141 | /// 142 | /// 查看包含引号的字符串 143 | /// 144 | public string PeekQuoted() 145 | { 146 | if (IsEnd) 147 | return ""; 148 | 149 | var index = Array.IndexOf(LeftQuotes, Current); 150 | 151 | if (index is -1) 152 | return PeekToWhiteSpace(); 153 | return Peek(RightQuotes[index], true)[..^1]; 154 | } 155 | 156 | #endregion 157 | 158 | #region Read 159 | 160 | /// 161 | /// 读取字符串,且移动解析器指针 162 | /// 163 | /// 终点字符,将解析器指针指向该字符 164 | /// 同时读取终点字符,解析器指针指向下一字符 165 | public string Read(char terminator, bool includeTerminator = false) 166 | { 167 | var start = _pos; 168 | _ = Skip(terminator, includeTerminator); 169 | return _str[start.._pos]; 170 | } 171 | 172 | /// 173 | /// 读取字符串,且移动解析器指针 174 | /// 175 | /// false时停止 176 | /// 同时读取终点字符,解析器指针指向下一字符 177 | public string ReadWhen(Func predicate, bool includeTerminator = false) 178 | { 179 | var start = _pos; 180 | _ = SkipWhen(predicate, includeTerminator); 181 | return _str[start.._pos]; 182 | } 183 | 184 | /// 185 | /// 读取字符串,且移动解析器指针,直至空白字符 186 | /// 187 | public string ReadToWhiteSpace() => ReadWhen(t => !char.IsWhiteSpace(t)); 188 | 189 | /// 190 | /// 读取字符串的剩余部分 191 | /// 192 | public string ReadToEnd() 193 | { 194 | var value = _str[_pos..]; 195 | _pos = _str.Length; 196 | return value; 197 | } 198 | 199 | /// 200 | /// 读取包含引号的字符串 201 | /// 202 | public string ReadQuoted() 203 | { 204 | if (IsEnd) 205 | return ""; 206 | 207 | var index = Array.IndexOf(LeftQuotes, Current); 208 | 209 | if (index is -1) 210 | return ReadToWhiteSpace(); 211 | ++_pos; 212 | return Read(RightQuotes[index], true)[..^1]; 213 | } 214 | 215 | #endregion 216 | 217 | private int IndexOfOrEnd(char value) 218 | { 219 | var r = _str.IndexOf(value, _pos); 220 | return r is -1 ? _str.Length : r; 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/Flandre.Core/Utils/TextUtils.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Core.Utils; 2 | 3 | internal static class TextUtils 4 | { 5 | internal static string RemoveString(this string text, string remove) 6 | { 7 | return text.Replace(remove, ""); 8 | } 9 | 10 | internal static string TrimStart(this string source, string value, 11 | StringComparison comparison = StringComparison.Ordinal) 12 | { 13 | if (value == "") 14 | return source; 15 | var valueLength = value.Length; 16 | var startIndex = 0; 17 | while (source.IndexOf(value, startIndex, comparison) == startIndex) 18 | startIndex += valueLength; 19 | 20 | return source[startIndex..]; 21 | } 22 | 23 | internal static string TrimEnd(this string source, string value, 24 | StringComparison comparison = StringComparison.Ordinal) 25 | { 26 | if (value == "") 27 | return source; 28 | var sourceLength = source.Length; 29 | var valueLength = value.Length; 30 | var count = sourceLength; 31 | while (source.LastIndexOf(value, count, comparison) == count - valueLength) 32 | count -= valueLength; 33 | 34 | return source[..count]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Flandre.Framework.Reactive/AppReactiveExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive.Linq; 2 | using Flandre.Core.Events; 3 | using Flandre.Framework.Events; 4 | 5 | namespace Flandre.Framework.Reactive; 6 | 7 | public static class AppReactiveExtensions 8 | { 9 | private static IObservable OnAppEvent( 10 | Action> add, Action> remove) where TEvent : FlandreEvent 11 | { 12 | return Observable.FromEventPattern, TEvent>(add, remove) 13 | .Select(pattern => pattern.EventArgs); 14 | } 15 | 16 | public static IObservable OnStarting(this FlandreApp app) 17 | { 18 | return OnAppEvent( 19 | add => app.Starting += add, 20 | remove => app.Starting -= remove); 21 | } 22 | 23 | public static IObservable OnReady(this FlandreApp app) 24 | { 25 | return OnAppEvent( 26 | add => app.Ready += add, 27 | remove => app.Ready -= remove); 28 | } 29 | 30 | public static IObservable OnStopped(this FlandreApp app) 31 | { 32 | return OnAppEvent( 33 | add => app.Stopped += add, 34 | remove => app.Stopped -= remove); 35 | } 36 | 37 | public static IObservable OnCommandInvoking(this FlandreApp app) 38 | { 39 | return OnAppEvent( 40 | add => app.CommandInvoking += add, 41 | remove => app.CommandInvoking -= remove); 42 | } 43 | 44 | public static IObservable OnCommandInvoked(this FlandreApp app) 45 | { 46 | return OnAppEvent( 47 | add => app.CommandInvoked += add, 48 | remove => app.CommandInvoked -= remove); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Flandre.Framework.Reactive/Flandre.Framework.Reactive.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Flandre.Framework.Reactive 5 | 1.0.0-rc.10 6 | FlandreDevs,bsdayo 7 | Reactive Extensions (Rx.NET) support for Flandre.Framework. 8 | flandre;rx.net;reactive 9 | MIT 10 | avatar.jpg 11 | 12 | net6.0 13 | enable 14 | enable 15 | Library 16 | true 17 | CS1591 18 | 19 | https://github.com/FlandreDevs/Flandre 20 | https://github.com/FlandreDevs/Flandre.git 21 | git 22 | FlandreDevs (C) 2022-2023 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/Flandre.Framework/AdapterCollection.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Common; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace Flandre.Framework; 5 | 6 | /// 7 | /// 适配器集合 8 | /// 9 | public interface IAdapterCollection 10 | { 11 | /// 12 | /// 适配器使用的服务 13 | /// 14 | IServiceCollection Services { get; } 15 | 16 | /// 17 | /// 添加一个适配器 18 | /// 19 | /// 适配器实例 20 | IAdapterCollection Add(IAdapter adapter); 21 | } 22 | 23 | internal sealed class AdapterCollection : IAdapterCollection 24 | { 25 | public IServiceCollection Services { get; } 26 | 27 | internal List Adapters { get; } = new(); 28 | 29 | internal AdapterCollection(IServiceCollection services) 30 | { 31 | Services = services; 32 | } 33 | 34 | public IAdapterCollection Add(IAdapter adapter) 35 | { 36 | Adapters.Add(adapter); 37 | return this; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Flandre.Framework/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("Flandre.Framework.Tests")] 4 | -------------------------------------------------------------------------------- /src/Flandre.Framework/Common/CommandContext.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Common; 2 | using Flandre.Core.Messaging; 3 | 4 | namespace Flandre.Framework.Common; 5 | 6 | /// 7 | /// 指令上下文 8 | /// 9 | public class CommandContext : MessageContext 10 | { 11 | /// 12 | /// 所在的 实例 13 | /// 14 | public FlandreApp App { get; } 15 | 16 | /// 17 | /// 构造实例 18 | /// 19 | /// 20 | /// 21 | /// 22 | public CommandContext(FlandreApp app, Bot bot, Message message) 23 | : base(bot, message) 24 | { 25 | App = app; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Flandre.Framework/Common/CommandExceptions.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Framework.Common; 2 | 3 | /// 4 | /// 指令调用异常 5 | /// 6 | public sealed class CommandInvokeException : Exception 7 | { 8 | /// 9 | /// 10 | /// 11 | /// 12 | public CommandInvokeException(string message) : base(message) 13 | { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Flandre.Framework/Common/CommandOption.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Framework.Common; 2 | 3 | /// 4 | /// 指令选项 5 | /// 6 | public sealed class CommandOption 7 | { 8 | /// 9 | /// 选项名称 10 | /// 11 | public string Name { get; } 12 | 13 | /// 14 | /// 选项短名称 15 | /// 16 | public char ShortName { get; } 17 | 18 | /// 19 | /// 是否有短名称 20 | /// 21 | public bool HasShortName { get; } 22 | 23 | /// 24 | /// 选项类型 25 | /// 26 | public Type Type { get; } 27 | 28 | /// 29 | /// 选项默认值 30 | /// 31 | public object? DefaultValue { get; } 32 | 33 | /// 34 | /// 选项描述 35 | /// 36 | public string? Description { get; init; } 37 | 38 | internal CommandOption(string name, char shortName, Type type, object? defaultValue) 39 | { 40 | Name = name; 41 | ShortName = shortName; 42 | HasShortName = shortName != default; 43 | Type = type; 44 | DefaultValue = defaultValue; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Flandre.Framework/Common/CommandParameter.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Framework.Common; 2 | 3 | /// 4 | /// 指令参数 5 | /// 6 | public sealed class CommandParameter 7 | { 8 | /// 9 | /// 参数名称 10 | /// 11 | public string Name { get; } 12 | 13 | /// 14 | /// 参数类型 15 | /// 16 | public Type Type { get; } 17 | 18 | /// 19 | /// 参数默认值 20 | /// 21 | public object? DefaultValue { get; } 22 | 23 | /// 24 | /// 参数描述 25 | /// 26 | public string? Description { get; init; } 27 | 28 | /// 29 | /// 是否被 params 修饰 30 | /// 31 | public bool IsParamArray { get; } 32 | 33 | /// 34 | /// 是否为必须参数 35 | /// 36 | public bool IsRequired { get; } 37 | 38 | internal CommandParameter(string name, Type type, bool isRequired, object? defaultValue, bool isParamArray) 39 | { 40 | Name = name; 41 | Type = type; 42 | IsRequired = isRequired; 43 | DefaultValue = isParamArray ? Array.CreateInstance(type.GetElementType()!, 0) : defaultValue; 44 | IsParamArray = isParamArray; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Flandre.Framework/Common/CommandParseResult.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Framework.Common; 2 | 3 | /// 4 | /// 指令解析结果 5 | /// 6 | public sealed class CommandParseResult 7 | { 8 | /// 9 | /// 参数解析结果 10 | /// 11 | public List ParsedArguments { get; } = new(); 12 | 13 | /// 14 | /// 选项解析结果(选项名-值) 15 | /// 16 | public Dictionary ParsedOptions { get; } = new(); 17 | 18 | internal string? ErrorMessage { get; private set; } 19 | 20 | /// 21 | /// 设置为解析错误,并发送一条提示消息给用户 22 | /// 23 | /// 错误消息 24 | /// 25 | public CommandParseResult SetError(string message) 26 | { 27 | ErrorMessage = message; 28 | return this; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Flandre.Framework/Common/CommandShortcut.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Text.RegularExpressions; 3 | using Flandre.Core.Utils; 4 | 5 | namespace Flandre.Framework.Common; 6 | 7 | internal abstract class CommandShortcut 8 | { 9 | public string Target { get; } 10 | 11 | public CommandShortcut(string target) 12 | { 13 | Target = target; 14 | } 15 | 16 | public abstract bool TryFormat(string text, [NotNullWhen(true)] out string? resultText); 17 | } 18 | 19 | internal class StringShortcut : CommandShortcut 20 | { 21 | private readonly string _shortcut; 22 | 23 | private readonly bool _allowArguments; 24 | 25 | public StringShortcut(string shortcut, string target, bool allowArguments) : base(target) 26 | { 27 | _shortcut = shortcut; 28 | _allowArguments = allowArguments; 29 | } 30 | 31 | public StringShortcut(StringShortcutAttribute attr) 32 | : this(attr.StringShortcut, attr.Target, attr.AllowArguments) 33 | { 34 | } 35 | 36 | public override bool TryFormat(string text, [NotNullWhen(true)] out string? resultText) 37 | { 38 | if (_allowArguments) 39 | { 40 | if (text.StartsWith(_shortcut)) 41 | { 42 | resultText = $"{Target} {text.TrimStart(_shortcut)}"; 43 | return true; 44 | } 45 | } 46 | else if (text == _shortcut) 47 | { 48 | resultText = Target; 49 | return true; 50 | } 51 | 52 | resultText = null; 53 | return false; 54 | } 55 | } 56 | 57 | internal class RegexShortcut : CommandShortcut 58 | { 59 | private readonly Regex _regex; 60 | 61 | public RegexShortcut(Regex regex, string target) : base(target) 62 | { 63 | _regex = regex; 64 | } 65 | 66 | public RegexShortcut(RegexShortcutAttribute attr) 67 | : this(attr.RegexShortcut, attr.Target) 68 | { 69 | } 70 | 71 | public override bool TryFormat(string text, [NotNullWhen(true)] out string? resultText) 72 | { 73 | var match = _regex.Match(text); 74 | if (match.Success) 75 | { 76 | // for (var i = 0; i < match.Groups.Count; i++) 77 | // { 78 | // var group = match.Groups[i]; 79 | // text = text.Replace($"${i + 1}", group.Value); 80 | // } 81 | resultText = _regex.Replace(text, Target); 82 | return true; 83 | } 84 | 85 | resultText = null; 86 | return false; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Flandre.Framework/Common/ICommandParser.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Utils; 2 | 3 | namespace Flandre.Framework.Common; 4 | 5 | /// 6 | /// 指令解析器 7 | /// 8 | public interface ICommandParser 9 | { 10 | /// 11 | /// 解析指令的参数、选项部分 12 | /// 13 | /// 当前指令 14 | /// 当前解析器 15 | /// 解析结果。如果解析失败,使用 说明。 16 | CommandParseResult Parse(Command command, StringParser parser); 17 | } 18 | -------------------------------------------------------------------------------- /src/Flandre.Framework/Common/MiddlewareContext.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Common; 2 | using Flandre.Core.Messaging; 3 | using Flandre.Core.Utils; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace Flandre.Framework.Common; 7 | 8 | /// 9 | /// 中间件上下文,包含当前时刻所需的全部对象 10 | /// 11 | public sealed class MiddlewareContext : MessageContext 12 | { 13 | internal readonly IServiceScope ServiceScope; 14 | 15 | /// 16 | /// 所在 实例,包含全局服务 17 | /// 18 | public FlandreApp App { get; } 19 | 20 | /// 21 | /// 域内服务 22 | /// 23 | public IServiceProvider Services => ServiceScope.ServiceProvider; 24 | 25 | /// 26 | /// 即将发送的回复 27 | /// 28 | public MessageContent? Response { get; set; } 29 | 30 | /// 31 | /// 当前所在的指令 32 | /// 33 | public Command? Command { get; internal set; } 34 | 35 | /// 36 | /// 执行指令产生的异常。如果指令成功执行,则该项为 null 37 | /// 38 | public Exception? Exception { get; internal set; } 39 | 40 | // TODO: 添加 IsFailed 和 FailReason 属性 41 | // TODO: enum MiddlewareFailReason { None, Exception, MissingArgument, ... } 42 | 43 | private IDictionary? _properties; 44 | 45 | /// 46 | /// 中间件属性,用于在中间件内传递消息 47 | /// 48 | public IDictionary Properties => _properties ??= new Dictionary(); 49 | 50 | internal StringParser? CommandStringParser { get; set; } 51 | 52 | internal MiddlewareContext(FlandreApp app, Bot bot, Message message, MessageContent? resp) 53 | : base(bot, message) 54 | { 55 | App = app; 56 | ServiceScope = app.Services.CreateScope(); 57 | Response = resp; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Flandre.Framework/Common/OptionAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Framework.Common; 2 | 3 | /// 4 | /// 选项 5 | /// 6 | [AttributeUsage(AttributeTargets.Parameter)] 7 | public sealed class OptionAttribute : Attribute 8 | { 9 | /// 10 | /// 短名称 11 | /// 12 | /// 长名称取决于参数名 13 | public char ShortName { get; init; } 14 | } 15 | -------------------------------------------------------------------------------- /src/Flandre.Framework/Common/Plugin.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Common; 2 | using Flandre.Core.Events; 3 | using Flandre.Core.Messaging; 4 | using Flandre.Framework.Routing; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace Flandre.Framework.Common; 8 | 9 | /// 10 | /// 插件 11 | /// 12 | public abstract class Plugin 13 | { 14 | /// 15 | /// 缓存日志类型 16 | /// 17 | private static Type? _loggerType; 18 | 19 | internal Type LoggerType 20 | { 21 | get 22 | { 23 | _loggerType ??= typeof(ILogger<>).MakeGenericType(GetType()); 24 | return _loggerType; 25 | } 26 | } 27 | 28 | /// 29 | /// 映射指令时调用 30 | /// 31 | /// 32 | public virtual void OnCommandMapping(ICommandRouteBuilder builder) { } 33 | 34 | /// 35 | /// 加载插件时调用 36 | /// 37 | /// 38 | public virtual Task OnLoadingAsync() => Task.CompletedTask; 39 | 40 | /// 41 | /// 卸载插件时调用 42 | /// 43 | /// 44 | public virtual Task OnUnloadingAsync() => Task.CompletedTask; 45 | 46 | /// 47 | /// 处理消息事件 48 | /// 49 | /// 当前消息上下文 50 | public virtual Task OnMessageReceivedAsync(MessageContext ctx) => Task.CompletedTask; 51 | 52 | /// 53 | /// 收到拉群邀请 54 | /// 55 | /// 当前上下文 56 | /// 拉群邀请事件 57 | public virtual Task OnGuildInvitedAsync(BotContext ctx, BotGuildInvitedEvent e) => Task.CompletedTask; 58 | 59 | /// 60 | /// 收到入群申请 61 | /// 62 | /// 当前上下文 63 | /// 入群申请事件 64 | public virtual Task OnGuildJoinRequestedAsync(BotContext ctx, BotGuildJoinRequestedEvent e) => Task.CompletedTask; 65 | 66 | /// 67 | /// 收到好友申请 68 | /// 69 | /// 当前上下文 70 | /// 好友申请事件 71 | public virtual Task OnFriendRequestedAsync(BotContext ctx, BotFriendRequestedEvent e) => Task.CompletedTask; 72 | } 73 | -------------------------------------------------------------------------------- /src/Flandre.Framework/Common/ShortcutAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace Flandre.Framework.Common; 4 | 5 | /// 6 | /// 为指令添加前缀式快捷方式 7 | /// 8 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] 9 | public class StringShortcutAttribute : Attribute 10 | { 11 | /// 12 | /// 字符串匹配快捷方式 13 | /// 14 | public string StringShortcut { get; } 15 | 16 | /// 17 | /// 目标指令文本 18 | /// 19 | public string Target { get; } 20 | 21 | /// 22 | /// 允许附加参数 23 | /// 24 | public bool AllowArguments { get; init; } 25 | 26 | /// 27 | /// 构造特性实例 28 | /// 29 | /// 字符串匹配快捷方式 30 | /// 目标指令文本 31 | public StringShortcutAttribute(string shortcut, string target) 32 | { 33 | StringShortcut = shortcut; 34 | Target = target; 35 | } 36 | 37 | /// 38 | /// 构造特性实例 39 | /// 40 | /// 字符串匹配快捷方式 41 | public StringShortcutAttribute(string shortcut) 42 | { 43 | StringShortcut = shortcut; 44 | Target = string.Empty; 45 | } 46 | } 47 | 48 | /// 49 | /// 为指令添加正则式快捷方式 50 | /// 51 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] 52 | public class RegexShortcutAttribute : Attribute 53 | { 54 | /// 55 | /// 正则式快捷方式 56 | /// 57 | public Regex RegexShortcut { get; } 58 | 59 | /// 60 | /// 目标指令文本 61 | /// 62 | public string Target { get; } 63 | 64 | /// 65 | /// 构造特性实例 66 | /// 67 | /// 正则式快捷方式 68 | /// 目标指令文本 69 | public RegexShortcutAttribute(string pattern, string target) 70 | { 71 | RegexShortcut = new Regex(pattern); 72 | Target = target; 73 | } 74 | 75 | /// 76 | /// 构造特性实例 77 | /// 78 | /// 正则式快捷方式 79 | public RegexShortcutAttribute(string pattern) 80 | { 81 | RegexShortcut = new Regex(pattern); 82 | Target = string.Empty; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Flandre.Framework/Common/TypeResolverDelegate.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace Flandre.Framework.Common; 4 | 5 | /// 6 | /// 类型解析器委托 7 | /// 8 | /// 需要解析的类型 9 | public delegate bool TypeResolverDelegate(string raw, out T result); 10 | 11 | internal delegate bool TypeResolverDelegate(string raw, [NotNullWhen(true)] out object? result); 12 | -------------------------------------------------------------------------------- /src/Flandre.Framework/Events/AppReadyEvent.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Events; 2 | 3 | namespace Flandre.Framework.Events; 4 | 5 | /// 6 | /// 应用就绪事件 7 | /// 8 | public sealed class AppReadyEvent : FlandreEvent 9 | { 10 | internal AppReadyEvent() 11 | { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Flandre.Framework/Events/AppStartingEvent.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Events; 2 | 3 | namespace Flandre.Framework.Events; 4 | 5 | /// 6 | /// 应用正在启动事件 7 | /// 8 | public sealed class AppStartingEvent : FlandreEvent 9 | { 10 | internal AppStartingEvent() 11 | { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Flandre.Framework/Events/AppStoppedEvent.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Events; 2 | 3 | namespace Flandre.Framework.Events; 4 | 5 | /// 6 | /// 应用退出事件 7 | /// 8 | public sealed class AppStoppedEvent : FlandreEvent 9 | { 10 | internal AppStoppedEvent() 11 | { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Flandre.Framework/Events/CommandInvokedEvent.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Events; 2 | using Flandre.Core.Messaging; 3 | using Flandre.Framework.Common; 4 | 5 | namespace Flandre.Framework.Events; 6 | 7 | /// 8 | /// 触发指令事件 9 | /// 10 | public class CommandInvokedEvent : FlandreEvent 11 | { 12 | /// 13 | /// 将触发的指令 14 | /// 15 | public Command Command { get; } 16 | 17 | /// 18 | /// 当前消息 19 | /// 20 | public Message Message { get; } 21 | 22 | /// 23 | /// 触发失败后抛出的异常 24 | /// 25 | public Exception? Exception { get; } 26 | 27 | /// 28 | /// 是否触发成功 29 | /// 30 | public bool IsSucceeded => Exception is null; 31 | 32 | /// 33 | /// 触发成功后将发送的消息 34 | /// 35 | public MessageContent? Response { get; } 36 | 37 | /// 38 | /// 用户 ID 39 | /// 40 | /// 等同于 .Sender.UserId 41 | public string UserId => Message.Sender.UserId; 42 | 43 | /// 44 | /// 群组 ID 45 | /// 46 | /// 等同于 .GuildId 47 | public string? GuildId => Message.GuildId; 48 | 49 | /// 50 | /// 频道 ID 51 | /// 52 | /// 等同于 .ChannelId 53 | public string? ChannelId => Message.ChannelId; 54 | 55 | internal CommandInvokedEvent(Command command, Message message, Exception? exception, MessageContent? resp) 56 | { 57 | Command = command; 58 | Message = message; 59 | Exception = exception; 60 | Response = resp; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Flandre.Framework/Events/CommandInvokingEvent.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Events; 2 | using Flandre.Core.Messaging; 3 | using Flandre.Framework.Common; 4 | 5 | namespace Flandre.Framework.Events; 6 | 7 | public class CommandInvokingEvent : FlandreEvent 8 | { 9 | public Command Command { get; } 10 | 11 | /// 12 | /// 当前消息 13 | /// 14 | public Message Message { get; } 15 | 16 | /// 17 | /// 用户 ID,等同于 Message.Sender.UserId 18 | /// 19 | public string UserId => Message.Sender.UserId; 20 | 21 | /// 22 | /// 群组 ID,等同于 Message.GuildId 23 | /// 24 | public string? GuildId => Message.GuildId; 25 | 26 | /// 27 | /// 频道 ID,等同于 Message.ChannelId 28 | /// 29 | public string? ChannelId => Message.ChannelId; 30 | 31 | internal bool IsCancelled { get; private set; } 32 | 33 | internal CommandInvokingEvent(Command command, Message message) 34 | { 35 | Command = command; 36 | Message = message; 37 | } 38 | 39 | public void Cancel() => IsCancelled = true; 40 | } 41 | -------------------------------------------------------------------------------- /src/Flandre.Framework/Extensions/FlandreAppExtensions.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Common; 2 | 3 | // ReSharper disable once CheckNamespace 4 | namespace Flandre.Framework; 5 | 6 | /// 7 | /// 的扩展方法 8 | /// 9 | public static class FlandreAppExtensions 10 | { 11 | /// 12 | /// 运行应用实例,并自动注册内置中间件 13 | /// 14 | public static Task StartWithDefaultsAsync(this FlandreApp app, CancellationToken cancellationToken = default) 15 | { 16 | app.UseCommandSession(); 17 | app.UseCommandParser(); 18 | app.UseCommandInvoker(); 19 | return app.StartAsync(cancellationToken); 20 | } 21 | 22 | /// 23 | /// 设置群组代理 (主 bot) 24 | /// 25 | /// 实例 26 | /// 平台 27 | /// 群组 ID 28 | /// 29 | public static void SetGuildAssignee(this FlandreApp app, string platform, string guildId, string botId) 30 | { 31 | app.GuildAssignees.AddOrUpdate($"{platform}:{guildId}", botId, (_, _) => botId); 32 | } 33 | 34 | /// 35 | /// 检查群组是否已被代理(已设置主 bot) 36 | /// 37 | /// 实例 38 | /// 平台 39 | /// 40 | public static bool IsGuildAssigned(this FlandreApp app, string platform, string botId) 41 | { 42 | return app.GuildAssignees.ContainsKey($"{platform}:{botId}"); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Flandre.Framework/Extensions/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | // ReSharper disable once CheckNamespace 5 | namespace Flandre.Framework; 6 | 7 | /// 8 | /// 服务集合扩展方法 9 | /// 10 | public static class ServiceCollectionExtensions 11 | { 12 | /// 13 | /// 配置 14 | /// 15 | /// 16 | /// 17 | /// 18 | public static IServiceCollection ConfigureFlandreApp(this IServiceCollection services, 19 | IConfiguration configuration) 20 | { 21 | return services.Configure(configuration); 22 | } 23 | 24 | /// 25 | /// 配置 26 | /// 27 | /// 28 | /// 29 | /// 30 | public static IServiceCollection ConfigureFlandreApp(this IServiceCollection services, 31 | Action action) 32 | { 33 | return services.Configure(action); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Flandre.Framework/Extensions/SessionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Messaging; 2 | using Flandre.Framework.Utils; 3 | 4 | // ReSharper disable once CheckNamespace 5 | namespace Flandre.Framework.Common; 6 | 7 | /// 8 | /// 会话扩展方法 9 | /// 10 | public static class SessionExtensions 11 | { 12 | /// 13 | /// 截获用户发送的下一条消息 14 | /// 15 | /// 当前指令上下文 16 | /// 超时时间 17 | /// 用户发送的下一条消息,如果超时则返回 18 | public static Task StartSessionAsync(this CommandContext ctx, TimeSpan timeout) 19 | { 20 | var cts = new CancellationTokenSource(timeout); 21 | var tcs = new TaskCompletionSource(); 22 | 23 | var mark = ctx.GetUserMark(); 24 | 25 | cts.Token.Register(() => 26 | { 27 | ctx.App.CommandSessions.TryRemove(mark, out _); 28 | tcs.TrySetResult(null); 29 | }); 30 | 31 | ctx.App.CommandSessions[mark] = tcs; 32 | 33 | return tcs.Task; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Flandre.Framework/Flandre.Framework.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Flandre.Framework 5 | 1.0.0-rc.11 6 | FlandreDevs,bsdayo 7 | 现代化、跨平台的聊天机器人框架,一次编写,多处运行。 8 | bot;chatbot;flandre;framework 9 | MIT 10 | avatar.jpg 11 | README.NuGet.md 12 | 13 | net6.0 14 | enable 15 | enable 16 | Library 17 | preview 18 | true 19 | 20 | https://github.com/FlandreDevs/Flandre 21 | https://github.com/FlandreDevs/Flandre.git 22 | git 23 | FlandreDevs (C) 2022-2023 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/Flandre.Framework/FlandreAppBuilder.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Common; 2 | using Flandre.Framework.Common; 3 | using Flandre.Framework.Internal; 4 | using Flandre.Framework.Services; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.DependencyInjection.Extensions; 8 | using Microsoft.Extensions.Hosting; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace Flandre.Framework; 12 | 13 | /// 14 | /// 构建器 15 | /// 16 | public sealed class FlandreAppBuilder 17 | { 18 | private readonly HostApplicationBuilder _hostAppBuilder; 19 | private readonly AdapterCollection _adapterCollection; 20 | private readonly PluginCollection _pluginCollection; 21 | 22 | /// 23 | /// 插件集合 24 | /// 25 | public IAdapterCollection Adapters => _adapterCollection; 26 | 27 | /// 28 | /// 插件集合 29 | /// 30 | public IPluginCollection Plugins => _pluginCollection; 31 | 32 | /// 33 | /// 全局服务 34 | /// 35 | public IServiceCollection Services => _hostAppBuilder.Services; 36 | 37 | /// 38 | /// 配置 39 | /// 40 | public ConfigurationManager Configuration => _hostAppBuilder.Configuration; 41 | 42 | /// 43 | /// 环境 44 | /// 45 | public IHostEnvironment Environment => _hostAppBuilder.Environment; 46 | 47 | /// 48 | /// 日志 49 | /// 50 | public ILoggingBuilder Logging => _hostAppBuilder.Logging; 51 | 52 | /// 53 | /// 配置容器 54 | /// 55 | /// 56 | /// 57 | /// 58 | public void ConfigureContainer( 59 | IServiceProviderFactory factory, Action? configure = null) 60 | where TContainerBuilder : notnull 61 | => _hostAppBuilder.ConfigureContainer(factory, configure); 62 | 63 | internal FlandreAppBuilder(string[]? args = null) 64 | : this(new HostApplicationBuilderSettings { Args = args }) 65 | { 66 | } 67 | 68 | internal FlandreAppBuilder(HostApplicationBuilderSettings? settings) 69 | { 70 | _hostAppBuilder = new HostApplicationBuilder(settings); 71 | _adapterCollection = new AdapterCollection(Services); 72 | _pluginCollection = new PluginCollection(Services, Configuration); 73 | AddInfrastructure(); 74 | } 75 | 76 | private void AddInfrastructure() 77 | { 78 | Services.AddSingleton(new CommandService()); 79 | } 80 | 81 | /// 82 | /// 添加插件 83 | /// 84 | /// 插件类型 85 | [Obsolete("FlandreAppBuilder.AddPlugin() is obsoleted. Use FlandreAppBuilder.Plugins.Plugins.Add() instead.")] 86 | public FlandreAppBuilder AddPlugin() where TPlugin : Plugin 87 | { 88 | Plugins.Add(); 89 | return this; 90 | } 91 | 92 | /// 93 | /// 94 | /// 95 | /// 96 | /// 97 | [Obsolete("FlandreAppBuilder.AddPlugin() is obsoleted. Use FlandreAppBuilder.Plugins.Add() instead.")] 98 | public FlandreAppBuilder AddPlugin(IConfiguration configuration) 99 | where TPlugin : Plugin where TPluginOptions : class 100 | { 101 | Plugins.Add(configuration); 102 | return this; 103 | } 104 | 105 | /// 106 | /// 107 | /// 108 | /// 109 | /// 110 | [Obsolete("FlandreAppBuilder.AddPlugin() is obsoleted. Use FlandreAppBuilder.Plugins.Add() instead.")] 111 | public FlandreAppBuilder AddPlugin(Action action) 112 | where TPlugin : Plugin where TPluginOptions : class 113 | { 114 | Plugins.Add(action); 115 | return this; 116 | } 117 | 118 | /// 119 | /// 添加机器人适配器 120 | /// 121 | /// 122 | /// 123 | [Obsolete("FlandreAppBuilder.AddAdapter() is obsoleted. Use FlandreAppBuilder.Adapters.Add() instead.")] 124 | public FlandreAppBuilder AddAdapter(IAdapter adapter) 125 | { 126 | _adapterCollection.Add(adapter); 127 | return this; 128 | } 129 | 130 | /// 131 | /// 构建 实例 132 | /// 133 | public FlandreApp Build() 134 | { 135 | Services.TryAddSingleton(); 136 | var app = new FlandreApp(_hostAppBuilder.Build(), 137 | _pluginCollection.PluginTypes, 138 | _adapterCollection.Adapters); 139 | return app; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Flandre.Framework/FlandreAppEvents.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Events; 2 | using Flandre.Framework.Events; 3 | 4 | namespace Flandre.Framework; 5 | 6 | /// 7 | /// 应用事件处理 8 | /// 9 | /// 10 | /// 11 | /// 12 | public delegate void AppEventHandler(FlandreApp app, TEvent e) where TEvent : FlandreEvent; 13 | 14 | public sealed partial class FlandreApp 15 | { 16 | /// 17 | /// 应用正在启动 18 | /// 19 | public event AppEventHandler? Starting; 20 | 21 | /// 22 | /// 应用就绪 23 | /// 24 | public event AppEventHandler? Ready; 25 | 26 | /// 27 | /// 应用已经退出 28 | /// 29 | public event AppEventHandler? Stopped; 30 | 31 | /// 32 | /// 应用正在触发指令 33 | /// 34 | public event AppEventHandler? CommandInvoking; 35 | 36 | /// 37 | /// 应用触发了指令 38 | /// 39 | public event AppEventHandler? CommandInvoked; 40 | } 41 | -------------------------------------------------------------------------------- /src/Flandre.Framework/FlandreAppOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Framework; 2 | 3 | /// 4 | /// 应用配置 5 | /// 6 | public sealed class FlandreAppOptions 7 | { 8 | /// 9 | /// 全局指令前缀 10 | /// 11 | public string CommandPrefix { get; set; } = string.Empty; 12 | 13 | /// 14 | /// 在用户调用指令时,不进行“指令未找到”提示 15 | /// 16 | public bool IgnoreUndefinedCommand { get; set; } = false; 17 | } 18 | -------------------------------------------------------------------------------- /src/Flandre.Framework/Internal/PluginCommandLoader.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Reflection; 3 | using Flandre.Framework.Common; 4 | using Flandre.Framework.Routing; 5 | using Flandre.Framework.Services; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace Flandre.Framework.Internal; 10 | 11 | /// 12 | /// 读取指令的上下文 13 | /// 14 | internal sealed class PluginCommandLoader 15 | { 16 | private readonly CommandService _cmdService; 17 | private readonly Type _pluginType; 18 | private readonly ILogger _logger; 19 | 20 | /// 如果为 null,代表将要加载一个闭包 21 | /// 22 | internal PluginCommandLoader(Type pluginType, IServiceProvider services) 23 | { 24 | _pluginType = pluginType; 25 | _cmdService = services.GetRequiredService(); 26 | _logger = services.GetRequiredService>(); 27 | } 28 | 29 | #region Internal Processing 30 | 31 | internal void LoadFromAttributes() 32 | { 33 | foreach (var method in _pluginType.GetMethods()) 34 | { 35 | _logger.LogTrace("正在加载方法 {PluginType}.{MethodName}", _pluginType, method.Name); 36 | var cmdAttr = method.GetCustomAttribute(); 37 | if (cmdAttr is null) 38 | continue; 39 | 40 | var cmd = _cmdService.RootNode.MapCommand(_pluginType, cmdAttr.FullName ?? method.Name) 41 | .WithAction(method); 42 | 43 | foreach (var alias in cmdAttr.Aliases) 44 | cmd.AddAlias(alias); 45 | 46 | cmd.Shortcuts.AddRange(method.GetCustomAttributes() 47 | .Select(attr => new StringShortcut(attr))); 48 | cmd.Shortcuts.AddRange(method.GetCustomAttributes() 49 | .Select(attr => new RegexShortcut(attr))); 50 | 51 | // cmd.StringShortcuts = method.GetCustomAttributes() 52 | // .Select(attr => attr.StringShortcut).ToList(); 53 | // cmd.RegexShortcuts = method.GetCustomAttributes() 54 | // .Select(attr => attr.RegexShortcut).ToList(); 55 | 56 | var obsoleteAttr = method.GetCustomAttribute(); 57 | cmd.IsObsolete = obsoleteAttr is not null; 58 | cmd.ObsoleteMessage = obsoleteAttr?.Message; 59 | cmd.Description = method.GetCustomAttribute()?.Description; 60 | } 61 | } 62 | 63 | internal void LoadCommandAliases() 64 | { 65 | var toBeAdded = new Dictionary(); 66 | 67 | void LoadNodeAliases(CommandNode node) 68 | { 69 | if (node.HasCommand) 70 | foreach (var alias in node.Command!.Aliases) 71 | toBeAdded[alias] = node.Command; 72 | 73 | foreach (var (_, subNode) in node.SubNodes) 74 | LoadNodeAliases(subNode); 75 | } 76 | 77 | LoadNodeAliases(_cmdService.RootNode); 78 | 79 | foreach (var (alias, cmd) in toBeAdded) 80 | { 81 | var currentNode = _cmdService.RootNode; 82 | var segments = alias.Split('.', 83 | StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); 84 | for (var i = 0; i < segments.Length; i++) 85 | { 86 | currentNode = currentNode.SubNodes.TryGetValue(segments[i], out var nextNode) 87 | ? nextNode 88 | : currentNode.SubNodes[segments[i]] = new CommandNode( 89 | string.Join('.', segments[..(i + 1)])); 90 | 91 | if (i == segments.Length - 1) 92 | { 93 | currentNode.Command = cmd; 94 | currentNode.IsAlias = true; 95 | } 96 | } 97 | } 98 | } 99 | 100 | internal void LoadCommandShortcuts() 101 | { 102 | void LoadNodeShortcuts(CommandNode node) 103 | { 104 | if (node.HasCommand) 105 | { 106 | foreach (var shortcut in node.Command!.Shortcuts) 107 | { 108 | switch (shortcut) 109 | { 110 | case StringShortcut strShortcut: 111 | _cmdService.StringShortcuts[strShortcut] = node.Command; 112 | break; 113 | 114 | case RegexShortcut regShortcut: 115 | _cmdService.RegexShortcuts[regShortcut] = node.Command; 116 | break; 117 | } 118 | } 119 | // foreach (var strShortcut in node.Command!.StringShortcuts) 120 | // stringShortcuts[strShortcut] = node.Command; 121 | // foreach (var regexShortcut in node.Command!.RegexShortcuts) 122 | // regexShortcuts[regexShortcut] = node.Command; 123 | } 124 | 125 | foreach (var (_, subNode) in node.SubNodes) 126 | LoadNodeShortcuts(subNode); 127 | } 128 | 129 | LoadNodeShortcuts(_cmdService.RootNode); 130 | } 131 | 132 | #endregion 133 | } 134 | -------------------------------------------------------------------------------- /src/Flandre.Framework/PluginCollection.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Framework.Common; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace Flandre.Framework; 6 | 7 | /// 8 | /// 插件集合 9 | /// 10 | public interface IPluginCollection 11 | { 12 | /// 13 | /// 插件使用的服务 14 | /// 15 | IServiceCollection Services { get; } 16 | 17 | /// 18 | /// 配置 19 | /// 20 | IConfiguration Configuration { get; } 21 | 22 | /// 23 | /// 添加插件 24 | /// 25 | /// 插件类型 26 | IPluginCollection Add(Type pluginType); 27 | } 28 | 29 | internal sealed class PluginCollection : IPluginCollection 30 | { 31 | public PluginCollection(IServiceCollection services, IConfiguration configuration) 32 | { 33 | Services = services; 34 | Configuration = configuration; 35 | } 36 | 37 | public List PluginTypes { get; } = new(); 38 | 39 | public IServiceCollection Services { get; } 40 | 41 | public IConfiguration Configuration { get; } 42 | 43 | public IPluginCollection Add(Type pluginType) 44 | { 45 | PluginTypes.Add(pluginType); 46 | Services.AddScoped(pluginType); 47 | return this; 48 | } 49 | } 50 | 51 | /// 52 | /// 插件集合扩展方法 53 | /// 54 | public static class PluginCollectionExtensions 55 | { 56 | /// 57 | /// 添加插件 58 | /// 59 | /// 60 | /// 61 | /// 62 | public static IPluginCollection Add(this IPluginCollection plugins) where TPlugin : Plugin 63 | { 64 | return plugins.Add(typeof(TPlugin)); 65 | } 66 | 67 | /// 68 | /// 添加一个带配置的插件 69 | /// 70 | /// 71 | /// 72 | /// 73 | /// 74 | /// 75 | public static IPluginCollection Add(this IPluginCollection plugins, 76 | IConfiguration configuration) 77 | where TPlugin : Plugin where TPluginOptions : class 78 | { 79 | plugins.Add(); 80 | plugins.Services.Configure(configuration); 81 | return plugins; 82 | } 83 | 84 | /// 85 | /// 添加一个带配置的插件 86 | /// 87 | /// 88 | /// 89 | /// 90 | /// 91 | /// 92 | public static IPluginCollection Add(this IPluginCollection plugins, 93 | Action action) 94 | where TPlugin : Plugin where TPluginOptions : class 95 | { 96 | plugins.Add(); 97 | plugins.Services.Configure(action); 98 | return plugins; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Flandre.Framework/Routing/CommandAttribute.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Models; 2 | 3 | namespace Flandre.Framework.Routing; 4 | 5 | /// 6 | /// 指令 7 | /// 8 | /// 被这条特性指定的方法,会自动作为指令被机器人加载 9 | [AttributeUsage(AttributeTargets.Method)] 10 | public class CommandAttribute : Attribute 11 | { 12 | /// 13 | /// 构造特性实例 14 | /// 15 | /// 16 | /// 17 | public CommandAttribute(string fullName, params string[] aliases) 18 | { 19 | FullName = fullName; 20 | Aliases = aliases; 21 | } 22 | 23 | /// 24 | /// 构造特性实例 25 | /// 26 | public CommandAttribute() 27 | { 28 | FullName = null; 29 | Aliases = Array.Empty(); 30 | } 31 | 32 | /// 33 | /// 指令的全名 34 | /// 35 | public string? FullName { get; } 36 | 37 | /// 38 | /// 指令别名(全名) 39 | /// 40 | public string[] Aliases { get; } 41 | 42 | /// 43 | /// 能触发该指令的用户身份 44 | /// 45 | /// 默认值为 46 | public UserRole Role { get; init; } = UserRole.Member; 47 | } 48 | -------------------------------------------------------------------------------- /src/Flandre.Framework/Routing/CommandNode.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Framework.Common; 2 | 3 | namespace Flandre.Framework.Routing; 4 | 5 | /// 6 | /// 指令节点 7 | /// 8 | public sealed class CommandNode 9 | { 10 | /// 11 | /// 以 . 分割的指令完整路径 12 | /// 13 | public string FullName { get; } 14 | 15 | /// 16 | /// 指令对象,如果该节点不包含指令则为 null 17 | /// 18 | public Command? Command { get; internal set; } 19 | 20 | /// 21 | /// 子节点 22 | /// 23 | public Dictionary SubNodes { get; } = new(); 24 | 25 | /// 26 | /// 当前节点包含指令 27 | /// 28 | public bool HasCommand => Command is not null; 29 | 30 | /// 31 | /// 当前指令节点为某个指令的别名 32 | /// 33 | public bool IsAlias { get; internal set; } 34 | 35 | internal CommandNode(string fullName) => FullName = fullName; 36 | 37 | internal Command MapCommand(Type? pluginType, string relativePath) 38 | { 39 | var segments = relativePath.Split('.', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); 40 | var currentNode = this; 41 | 42 | for (var i = 0; i < segments.Length; i++) 43 | { 44 | currentNode = currentNode.SubNodes.TryGetValue(segments[i], out var nextNode) 45 | ? nextNode 46 | : currentNode.SubNodes[segments[i]] = new CommandNode( 47 | string.Join('.', segments[..(i + 1)])); 48 | } 49 | 50 | var finalName = segments[^1]; 51 | var command = new Command(currentNode, pluginType, finalName, currentNode.FullName); 52 | currentNode.Command = command; 53 | return command; 54 | } 55 | 56 | /// 57 | /// 移除本身节点所含指令,并清除所有子节点 58 | /// 59 | public void Clear() 60 | { 61 | Command = null; 62 | IsAlias = false; 63 | SubNodes.Clear(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Flandre.Framework/Routing/CommandNodeExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Framework.Routing; 2 | 3 | /// 4 | /// 5 | /// 6 | public static class CommandNodeExtensions 7 | { 8 | /// 9 | /// 寻找子节点 10 | /// 11 | /// 12 | /// 13 | /// 14 | public static CommandNode? FindSubNode(this CommandNode node, string relativePath) 15 | { 16 | foreach (var name in relativePath.Split('.', 17 | StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)) 18 | if (node.SubNodes.Keys.FirstOrDefault(k => k.Equals(name, StringComparison.OrdinalIgnoreCase)) is { } key) 19 | node = node.SubNodes[key]; 20 | else 21 | return null; 22 | 23 | return node; 24 | } 25 | 26 | public static int CountCommands(this CommandNode node) 27 | { 28 | var count = 0; 29 | 30 | void CountNodeCommands(CommandNode nowNode) 31 | { 32 | if (nowNode is { HasCommand: true, IsAlias: false }) 33 | count++; 34 | foreach (var (_, subNode) in nowNode.SubNodes) 35 | CountNodeCommands(subNode); 36 | } 37 | 38 | CountNodeCommands(node); 39 | return count; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Flandre.Framework/Routing/CommandRouteBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Framework.Services; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace Flandre.Framework.Routing; 5 | 6 | /// 7 | /// 指令路由构造器的扩展方法 8 | /// 9 | public static class CommandRouteBuilderExtensions 10 | { 11 | /// 12 | /// 添加指令 13 | /// 14 | /// 15 | /// 16 | /// 17 | public static void MapCommand(this ICommandRouteBuilder builder, string fullname, Delegate commandDelegate) 18 | { 19 | var cmdService = builder.Services.GetRequiredService(); 20 | cmdService.RootNode.MapCommand(null, fullname) 21 | .WithAction(commandDelegate); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Flandre.Framework/Routing/ICommandRouteBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Framework.Routing; 2 | 3 | /// 4 | /// 指令路由构造器 5 | /// 6 | public interface ICommandRouteBuilder 7 | { 8 | /// 9 | /// 服务提供源 10 | /// 11 | IServiceProvider Services { get; } 12 | } 13 | -------------------------------------------------------------------------------- /src/Flandre.Framework/Services/CommandService.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Utils; 2 | using Flandre.Framework.Common; 3 | using Flandre.Framework.Routing; 4 | 5 | namespace Flandre.Framework.Services; 6 | 7 | internal sealed class CommandService 8 | { 9 | public CommandNode RootNode { get; } = new(""); 10 | 11 | public Dictionary StringShortcuts { get; } = new(); 12 | 13 | public Dictionary RegexShortcuts { get; } = new(); 14 | 15 | internal Dictionary TypeFriendlyNames { get; } = new(); 16 | 17 | internal Dictionary TypeResolvers { get; } = new(); 18 | 19 | internal CommandService() 20 | { 21 | AddInternalTypeResolvers(); 22 | } 23 | 24 | public void MapTypeResolver(string? typeFriendlyName, TypeResolverDelegate resolver) 25 | { 26 | var type = typeof(T); 27 | TypeResolvers[type] = (string raw, out object? result) => 28 | { 29 | var suc = resolver(raw, out var res); 30 | result = res; 31 | return suc; 32 | }; 33 | 34 | if (typeFriendlyName is not null) 35 | TypeFriendlyNames[type] = typeFriendlyName; 36 | } 37 | 38 | public bool TryParseArgumentValue(Type type, string raw, out object? result) 39 | { 40 | if (TypeResolvers.TryGetValue(type, out var typeResolver)) 41 | return typeResolver(raw, out result); 42 | 43 | result = null; 44 | return false; 45 | } 46 | 47 | public string GetTypeFriendlyName(Type type) 48 | { 49 | return TypeFriendlyNames.TryGetValue(type, out var name) 50 | ? name 51 | : type.Name; 52 | } 53 | 54 | internal void Reset() 55 | { 56 | RootNode.Clear(); 57 | StringShortcuts.Clear(); 58 | RegexShortcuts.Clear(); 59 | TypeFriendlyNames.Clear(); 60 | TypeResolvers.Clear(); 61 | AddInternalTypeResolvers(); 62 | } 63 | 64 | #region 初始化 65 | 66 | private void AddInternalTypeResolvers() 67 | { 68 | MapTypeResolver("整数", int.TryParse); 69 | MapTypeResolver("整数", long.TryParse); 70 | MapTypeResolver("整数", byte.TryParse); 71 | MapTypeResolver("整数", sbyte.TryParse); 72 | MapTypeResolver("整数", short.TryParse); 73 | 74 | MapTypeResolver("正整数", uint.TryParse); 75 | MapTypeResolver("正整数", ulong.TryParse); 76 | MapTypeResolver("正整数", ushort.TryParse); 77 | 78 | MapTypeResolver("小数", double.TryParse); 79 | MapTypeResolver("小数", float.TryParse); 80 | MapTypeResolver("小数", decimal.TryParse); 81 | 82 | MapTypeResolver("\"true\"或\"false\"", bool.TryParse); 83 | 84 | MapTypeResolver("字符", char.TryParse); 85 | 86 | // ReSharper disable once RedundantTypeArgumentsOfMethod 87 | MapTypeResolver("不带空格的文本", (string raw, out string result) => 88 | { 89 | result = new StringParser(raw).ReadQuoted(); 90 | return true; 91 | }); 92 | } 93 | 94 | #endregion 95 | } 96 | -------------------------------------------------------------------------------- /src/Flandre.Framework/Services/MiddlewareService.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Framework.Common; 2 | 3 | namespace Flandre.Framework.Services; 4 | 5 | /// 6 | /// 中间件服务 7 | /// 8 | public sealed class MiddlewareService 9 | { 10 | private readonly List, Task>> _middleware = new(); 11 | } 12 | -------------------------------------------------------------------------------- /src/Flandre.Framework/Utils/LogUtils.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace Flandre.Framework.Utils; 4 | 5 | internal static class LogUtils 6 | { 7 | internal static ILogger GetTempLogger() 8 | { 9 | using var factory = new LoggerFactory(); 10 | return factory.CreateLogger(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Flandre.Framework/Utils/MessageUtils.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Messaging; 2 | 3 | namespace Flandre.Framework.Utils; 4 | 5 | internal static class MessageUtils 6 | { 7 | internal static string GetUserMark(this MessageContext ctx) 8 | { 9 | return $"{ctx.Platform}:{ctx.GuildId}:{ctx.UserId}"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/Flandre.Core.Reactive.Tests/CoreReactiveExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive.Linq; 2 | using Flandre.Adapters.Mock; 3 | 4 | namespace Flandre.Core.Reactive.Tests; 5 | 6 | public class CoreReactiveExtensionsTests 7 | { 8 | [Fact] 9 | public void TestObserveMessageReceived() 10 | { 11 | var adapter = new MockAdapter(); 12 | var client1 = adapter.GetChannelClient("testG1", "testC1", "123"); 13 | var client2 = adapter.GetChannelClient("testG2", "testC2", "456"); 14 | var bot = adapter.Bots.First(); 15 | 16 | var messageCountFrom1 = 0; 17 | var messageCountFrom2 = 0; 18 | var allMessageReceived = 0; 19 | 20 | bot.OnMessageReceived() 21 | .OfUser("123") 22 | .Subscribe(_ => messageCountFrom1++); 23 | 24 | bot.OnMessageReceived() 25 | .Select(e => e.Message) 26 | .OfGuild("testG1") 27 | .Subscribe(_ => messageCountFrom1++); 28 | 29 | bot.OnMessageReceived() 30 | .Select(e => e.Message) 31 | .OfUser("456") 32 | .Subscribe(_ => messageCountFrom2++); 33 | 34 | bot.OnMessageReceived() 35 | .OfChannel("testC2") 36 | .Subscribe(_ => messageCountFrom2++); 37 | 38 | bot.OnMessageReceived() 39 | .Subscribe(_ => allMessageReceived++); 40 | 41 | 42 | client1.SendMessage("abc"); 43 | client1.SendMessage("abc"); 44 | client2.SendMessage("abc"); 45 | 46 | Assert.Equal(4, messageCountFrom1); 47 | Assert.Equal(2, messageCountFrom2); 48 | Assert.Equal(3, allMessageReceived); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/Flandre.Core.Reactive.Tests/Flandre.Core.Reactive.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | all 17 | 18 | 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | all 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/Flandre.Core.Reactive.Tests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; 2 | -------------------------------------------------------------------------------- /tests/Flandre.Core.Tests/Flandre.Core.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | all 17 | 18 | 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | all 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/Flandre.Core.Tests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; 2 | -------------------------------------------------------------------------------- /tests/Flandre.Core.Tests/UtilsTests/StringParserTests.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Utils; 2 | 3 | namespace Flandre.Core.Tests.UtilsTests; 4 | 5 | public class StringParserTests 6 | { 7 | [Fact] 8 | public void TestStringParser() 9 | { 10 | const string str = "Str ing Parser_[Tests]"; 11 | var parser = new StringParser(str); 12 | 13 | Assert.Equal('S', parser.Current); 14 | 15 | parser.Skip(1); 16 | Assert.Equal('t', parser.Current); 17 | 18 | parser.Skip('r'); 19 | Assert.Equal('r', parser.Current); 20 | 21 | parser.Skip(1).SkipWhiteSpaces(); // skip 'r' and skip spaces 22 | Assert.Equal('i', parser.Current); 23 | 24 | Assert.Equal("ing", parser.Peek(3)); 25 | 26 | Assert.Equal("ing P", parser.Peek('a')); 27 | 28 | Assert.Equal("ing", parser.Read(' ')); 29 | Assert.Equal(' ', parser.Current); 30 | 31 | Assert.Equal("Parser_[Tests]", parser.SkipWhiteSpaces().ReadToEnd()); 32 | } 33 | 34 | [Fact] 35 | public void TestQuotes() 36 | { 37 | const string str = "\"alpha\" 'beta' gamma other"; 38 | var parser = new StringParser(str); 39 | 40 | Assert.Equal("alpha", parser.ReadQuoted()); 41 | parser.SkipWhiteSpaces(); 42 | 43 | Assert.Equal("beta", parser.ReadQuoted()); 44 | parser.SkipWhiteSpaces(); 45 | 46 | Assert.Equal("gamma", parser.ReadQuoted()); 47 | 48 | // 'x' is not in the rest of the string, so default to ReadToEnd. 49 | Assert.Equal(" other", parser.Read('x')); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Flandre.Core.Tests/UtilsTests/TextUtilsTests.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Core.Utils; 2 | 3 | namespace Flandre.Core.Tests.UtilsTests; 4 | 5 | public class TextUtilsTests 6 | { 7 | [Theory] 8 | [InlineData(" Alp ha Bet a ", "AlphaBeta", " ")] 9 | [InlineData("-a --beta", "-a beta", "--")] 10 | public void TestRemoveString(string source, string result, string removal) 11 | { 12 | Assert.Equal(result, source.RemoveString(removal)); 13 | } 14 | 15 | [Theory] 16 | [InlineData("string", "ring", "st")] 17 | [InlineData("string", "string", "")] 18 | public void TestTrimStart(string source, string result, string trim) 19 | { 20 | Assert.Equal(result, source.TrimStart(trim)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Flandre.Framework.Tests/AppEventsTests.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Framework.Routing; 2 | 3 | namespace Flandre.Framework.Tests; 4 | 5 | public class AppEventsTests 6 | { 7 | private class TestPlugin : Plugin 8 | { 9 | [Command("throw-ex")] 10 | public static MessageContent OnThrowEx(CommandContext ctx) => 11 | throw new Exception("Test Exception"); 12 | } 13 | 14 | [Fact] 15 | public async Task TestEvents() 16 | { 17 | await using var app = Utils.CreateTestApp(out var client); 18 | 19 | var count = 0; 20 | string? cmdName = null; 21 | Exception? ex = null; 22 | 23 | app.Starting += (_, _) => count += 1; 24 | app.Ready += (_, _) => 25 | { 26 | count += 10; 27 | client.SendMessage("throw-ex"); 28 | }; 29 | app.Stopped += (_, _) => count += 100; 30 | 31 | app.CommandInvoking += (_, e) => cmdName = e.Command.Name; 32 | app.CommandInvoked += (_, e) => { ex = e.Exception; }; 33 | 34 | await app.StartWithDefaultsAsync(); 35 | await Task.Delay(TimeSpan.FromSeconds(1)); 36 | await app.StopAsync(); 37 | 38 | Assert.Equal(111, count); 39 | Assert.Equal("throw-ex", cmdName); 40 | Assert.Equal("Test Exception", ex?.Message); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Flandre.Framework.Tests/CommandNodeTests.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Framework.Routing; 2 | using Flandre.Framework.Services; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace Flandre.Framework.Tests; 6 | 7 | public class CommandNodeTests 8 | { 9 | private class TestPlugin : Plugin 10 | { 11 | [Command("cmd-xxx")] 12 | public static MessageContent? ACommandInRoot() => null; 13 | 14 | [Command("...cmd-aaa.cmd-bbb...")] 15 | public static MessageContent? ASubCommand() => null; 16 | 17 | [Command(".cmd-bbb")] 18 | public static MessageContent? AnotherCommandInRoot() => null; 19 | } 20 | 21 | [Fact] 22 | public void TestNode() 23 | { 24 | using var app = Utils.StartTestApp(out _); 25 | 26 | var cmdService = app.Services.GetRequiredService(); 27 | 28 | Assert.Equal(3, cmdService.RootNode.CountCommands()); 29 | Assert.Equal("cmd-xxx", cmdService.RootNode.FindSubNode("cmd-xxx")?.FullName); 30 | Assert.Equal("cmd-aaa.cmd-bbb", cmdService.RootNode.FindSubNode("cmd-aaa..cmd-bbb")?.FullName); 31 | Assert.Equal("cmd-aaa.cmd-bbb", cmdService.RootNode.FindSubNode("cmd-aaa..cmd-bbb")?.Command?.FullName); 32 | Assert.Equal("cmd-bbb", cmdService.RootNode.FindSubNode("cmd-bbb . .")?.Command?.FullName); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Flandre.Framework.Tests/CommandTests.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using Flandre.Core.Common; 3 | using Flandre.Framework.Routing; 4 | using Flandre.Framework.Services; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | // ReSharper disable UnusedMember.Local 8 | 9 | namespace Flandre.Framework.Tests; 10 | 11 | public class CommandTests 12 | { 13 | private sealed class TestPlugin : Plugin 14 | { 15 | public override async Task OnMessageReceivedAsync(MessageContext ctx) 16 | { 17 | if (ctx.Message.GetText().StartsWith("OMR:")) 18 | await ctx.Bot.SendMessageAsync(ctx.Message); 19 | } 20 | 21 | [Command] 22 | [Description("This is a test command.")] 23 | public static MessageContent Test1(bool arg1, [Option] double opt = 0) 24 | { 25 | return new MessageBuilder() 26 | .Text($"{arg1} {opt + 200}") 27 | .Build(); 28 | } 29 | 30 | [Command("test2", "..test111.11...45.14.")] 31 | [Obsolete("This command is obsoleted.")] 32 | public static string Test2(int arg1, float arg2, CommandContext ctx, 33 | [Option] bool opt1 = true, [Option(ShortName = 'o')] bool opt2 = false) 34 | { 35 | return $"{arg1} {arg2} {opt1} {opt2}"; 36 | } 37 | 38 | [Command] 39 | [RegexShortcut("测([0-9A-Za-z_])试", "$1 someStr")] 40 | public static string Test3(string arg1, string arg2) 41 | { 42 | return $"{arg1} {arg2}"; 43 | } 44 | 45 | [Command] 46 | [StringShortcut("测试4", "123.456")] 47 | public static string Test4(double arg1) 48 | { 49 | return $"{arg1}"; 50 | } 51 | 52 | [Command] 53 | [StringShortcut("测试5", "111.222 --opt1", AllowArguments = true)] 54 | public static string Test5(double arg1, int arg2, [Option] bool opt1) 55 | { 56 | return $"{arg1} {arg2} {opt1}"; 57 | } 58 | 59 | // Array parameter 60 | [Command] 61 | public string Test6(double arg, params string[] strArr) 62 | { 63 | return $"{arg} | {string.Join(',', strArr)} | {strArr.Length}"; 64 | } 65 | 66 | [Command] 67 | public static async ValueTask TestAsync() 68 | { 69 | // simulates async tasks 70 | await Task.Run(() => { }); 71 | return "ok!"; 72 | } 73 | } 74 | 75 | [Fact] 76 | public void TestCommands() 77 | { 78 | using var app = Utils.StartTestApp(out var client); 79 | 80 | var service = app.Services.GetRequiredService(); 81 | 82 | Assert.Equal(7, service.RootNode.CountCommands()); 83 | 84 | MessageContent? content; 85 | 86 | content = client.SendMessageForReply("OMR:114514"); 87 | Assert.Equal("OMR:114514", content?.GetText()); 88 | 89 | content = client.SendMessageForReply("test1 --opt 114.514 true"); 90 | Assert.Equal("True 314.514", content?.GetText()); 91 | // 92 | content = client.SendMessageForReply("test2 -o 123 191.981 --no-opt1"); 93 | Assert.Equal("123 191.981 False True", 94 | content?.GetText()); 95 | 96 | // test async 97 | content = client.SendMessageForReply("testasync"); 98 | Assert.Equal("ok!", content?.GetText()); 99 | } 100 | 101 | [Fact] 102 | public void TestShortcuts() 103 | { 104 | using var app = Utils.StartTestApp(out var client); 105 | 106 | MessageContent? content; 107 | 108 | content = client.SendMessageForReply("测3试"); 109 | Assert.Equal("3 someStr", content?.GetText()); 110 | 111 | content = client.SendMessageForReply("测试4"); 112 | Assert.Equal("123.456", content?.GetText()); 113 | 114 | content = client.SendMessageForReply("测试4 114.514", TimeSpan.FromSeconds(2)); 115 | Assert.Null(content?.GetText()); 116 | 117 | content = client.SendMessageForReply("测试5 333"); 118 | Assert.Equal("111.222 333 True", content?.GetText()); 119 | } 120 | 121 | [Fact] 122 | public void TestMapCommand() 123 | { 124 | using var app = Utils.StartTestApp(out var client); 125 | 126 | app.MapCommand("test1", (int a, int b) => a + b); 127 | app.MapCommand("test2.sub", (int x) => Math.Pow(x, 2)); 128 | 129 | var content = client.SendMessageForReply("test1 123 456"); 130 | Assert.Equal("579", content?.GetText()); 131 | 132 | content = client.SendMessageForReply("test2 sub 12"); 133 | Assert.Equal("144", content?.GetText()); 134 | } 135 | 136 | [Fact] 137 | public void TestArrayParameter() 138 | { 139 | using var app = Utils.StartTestApp(out var client); 140 | 141 | var content = client.SendMessageForReply("test6 1.23 aaa bbb ccc "); 142 | Assert.Equal("1.23 | aaa,bbb,ccc | 3", content?.GetText()); 143 | 144 | content = client.SendMessageForReply("test6 2.34"); 145 | Assert.Equal("2.34 | | 0", content?.GetText()); 146 | } 147 | 148 | [Fact] 149 | public void TestInformalAttributes() 150 | { 151 | using var app = Utils.StartTestApp(out _); 152 | 153 | var cmdService = app.Services.GetRequiredService(); 154 | 155 | Assert.Equal("This is a test command.", 156 | cmdService.RootNode.FindSubNode("test1")?.Command?.Description); 157 | 158 | Assert.True(cmdService.RootNode.FindSubNode("test2")?.Command?.IsObsolete); 159 | Assert.Equal("This command is obsoleted.", 160 | cmdService.RootNode.FindSubNode("test2")?.Command?.ObsoleteMessage); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /tests/Flandre.Framework.Tests/Flandre.Framework.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | all 17 | 18 | 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | all 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/Flandre.Framework.Tests/FlandreAppTests.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable StringLiteralTypo 2 | 3 | using Flandre.Framework.Routing; 4 | using Flandre.Framework.Services; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace Flandre.Framework.Tests; 8 | 9 | public class FlandreAppTests 10 | { 11 | private class TestPlugin : Plugin 12 | { 13 | [Command("test", " .. test111..11.45..14 . ")] 14 | [StringShortcut("测试")] 15 | public static MessageContent OnTest(CommandContext ctx, bool arg1, [Option] double opt = 0) 16 | { 17 | return $"{arg1} {opt + 200}"; 18 | } 19 | 20 | [Command("sub.test", "sssuuubbb")] 21 | [StringShortcut("子测试")] 22 | public static MessageContent? OnSubTest(CommandContext ctx) => null; 23 | 24 | [Command("...sub....sub..sub......test..")] 25 | // lang=regex 26 | [RegexShortcut(@"\d\d\d", "$1")] 27 | public static MessageContent? OnSubSubSubTest(CommandContext ctx) => null; 28 | } 29 | 30 | [Fact] 31 | public void TestAliases() 32 | { 33 | using var app = Utils.StartTestApp(out _); 34 | 35 | var cmdService = app.Services.GetRequiredService(); 36 | 37 | Assert.Equal(3, cmdService.RootNode.CountCommands()); 38 | 39 | Assert.NotNull(cmdService.RootNode.FindSubNode("test")); 40 | Assert.NotNull(cmdService.RootNode.FindSubNode("sub.test")); 41 | Assert.NotNull(cmdService.RootNode.FindSubNode("sub.sub.sub.test")); 42 | 43 | // alias 44 | Assert.NotNull(cmdService.RootNode.FindSubNode("test111.11.45.14")); 45 | Assert.Equal(cmdService.RootNode.FindSubNode("sssuuubbb")?.Command, 46 | cmdService.RootNode.FindSubNode("sub.test")?.Command); 47 | } 48 | 49 | [Fact] 50 | public void TestShortcutCount() 51 | { 52 | using var app = Utils.StartTestApp(out _); 53 | 54 | var cmdService = app.Services.GetRequiredService(); 55 | 56 | Assert.Equal(2, cmdService.StringShortcuts.Count); 57 | Assert.Single(cmdService.RegexShortcuts); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/Flandre.Framework.Tests/MiddlewareTests.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Framework.Tests; 2 | 3 | public class MiddlewareTests 4 | { 5 | [Fact] 6 | public async Task TestMiddleware() 7 | { 8 | await using var app = Utils.CreateTestApp(out var client); 9 | 10 | // Order Note 11 | // 1,2,3 ↓ ↑ 2 12 | // [Custom Middleware #1] 13 | // 2 ↓ ↑ 2 14 | // [Custom Middleware #2] 15 | 16 | int count1In = 0, count1Out = 0, count2 = 0; 17 | 18 | // Custom Middleware #1 19 | app.Use(async (ctx, next) => 20 | { 21 | // 1, 2, 3 pass through 22 | 23 | if (ctx.Message.GetText().Contains("(3)")) 24 | // 3 shorts here 25 | return; 26 | 27 | count1In++; 28 | 29 | if (ctx.Message.GetText().Contains("(1)")) 30 | { 31 | ctx.Response = "ok"; 32 | // 1 shorts here 33 | return; 34 | } 35 | 36 | // only 2 passes through the next middleware 37 | await next(); 38 | // 2 goes out 39 | 40 | count1Out++; 41 | }); 42 | 43 | // Custom Middleware #2 44 | app.Use(async (ctx, next) => 45 | { 46 | // 2 passes through 47 | count2++; 48 | 49 | ctx.Response = ctx.Message.Content; 50 | 51 | // 2 goes out 52 | await next(); 53 | }); 54 | 55 | await app.StartAsync(); 56 | 57 | var content1 = client.SendMessageForReply("test (1) short me at middleware #1"); 58 | Assert.NotNull(content1); 59 | 60 | var content2 = client.SendMessageForReply("test (2) pass me through all middleware"); 61 | Assert.NotNull(content2); 62 | 63 | var content3 = client.SendMessageForReply("test (3) don't pass me", TimeSpan.FromSeconds(2)); 64 | Assert.Null(content3); 65 | 66 | Assert.Equal(2, count1In); 67 | Assert.Equal(1, count2); 68 | Assert.Equal(1, count1Out); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/Flandre.Framework.Tests/SessionTests.cs: -------------------------------------------------------------------------------- 1 | using Flandre.Framework.Routing; 2 | 3 | namespace Flandre.Framework.Tests; 4 | 5 | public class SessionTests 6 | { 7 | private sealed class TestPlugin : Plugin 8 | { 9 | [Command("start-session")] 10 | public static async Task OnStartSession(CommandContext ctx) 11 | { 12 | var nextMsg = await ctx.StartSessionAsync(TimeSpan.FromSeconds(2)); 13 | return nextMsg?.Content; 14 | } 15 | } 16 | 17 | [Fact] 18 | public async Task TestCommandSession() 19 | { 20 | await using var app = Utils.StartTestApp(out var client); 21 | 22 | var task1 = client.SendMessageForReplyAsync("start-session"); 23 | await Task.Delay(TimeSpan.FromSeconds(1)); 24 | client.SendMessage("return this"); 25 | Assert.NotNull(await task1); 26 | 27 | var task2 = client.SendMessageForReplyAsync("start-session"); 28 | await Task.Delay(TimeSpan.FromSeconds(3)); 29 | client.SendMessage("timeout!"); 30 | Assert.Null(await task2); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Flandre.Framework.Tests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; 2 | global using Flandre.Adapters.Mock; 3 | global using Flandre.Framework.Common; 4 | global using Flandre.Core.Messaging; 5 | -------------------------------------------------------------------------------- /tests/Flandre.Framework.Tests/Utils.cs: -------------------------------------------------------------------------------- 1 | namespace Flandre.Framework.Tests; 2 | 3 | public static class Utils 4 | { 5 | public static FlandreAppBuilder CreateTestBuilder() where TPlugin : Plugin 6 | { 7 | var builder = FlandreApp.CreateBuilder(); 8 | builder.Plugins.Add(); 9 | return builder; 10 | } 11 | 12 | public static FlandreApp CreateTestApp(out MockClient client, bool useFriendClient = false) 13 | where TPlugin : Plugin 14 | { 15 | var builder = CreateTestBuilder(); 16 | 17 | var adapter = new MockAdapter(); 18 | client = useFriendClient ? adapter.GetFriendClient() : adapter.GetChannelClient(); 19 | builder.Adapters.Add(adapter); 20 | 21 | return builder.Build(); 22 | } 23 | 24 | public static FlandreApp CreateTestApp(out MockClient client, bool useFriendClient = false) 25 | { 26 | var builder = FlandreApp.CreateBuilder(); 27 | 28 | var adapter = new MockAdapter(); 29 | client = useFriendClient ? adapter.GetFriendClient() : adapter.GetChannelClient(); 30 | builder.Adapters.Add(adapter); 31 | 32 | return builder.Build(); 33 | } 34 | 35 | public static FlandreApp StartTestApp(out MockClient client, bool useFriendClient = false) 36 | where TPlugin : Plugin 37 | { 38 | var app = CreateTestApp(out client, useFriendClient); 39 | app.StartWithDefaultsAsync().Wait(); 40 | return app; 41 | } 42 | 43 | public static FlandreApp StartTestApp(out MockClient client, bool useFriendClient = false) 44 | { 45 | var app = CreateTestApp(out client, useFriendClient); 46 | app.StartWithDefaultsAsync().Wait(); 47 | return app; 48 | } 49 | } 50 | --------------------------------------------------------------------------------