├── .gitignore ├── CocoaFramework.sln ├── CocoaFramework ├── AsyncMeeting.cs ├── Attributes.cs ├── AutoData.cs ├── BotEventHandlerBase.cs ├── BotMiddlewareBase.cs ├── BotModuleBase.cs ├── BotStartup.cs ├── CocoaFramework.csproj ├── Core │ ├── BotCore.cs │ ├── MiddlewareCore.cs │ ├── ModuleCore.MessageLock.cs │ └── ModuleCore.cs ├── Extensions.cs ├── MessageBuilder.cs ├── MessageInfo.cs ├── MessageSource.cs ├── Models │ ├── Meeting.cs │ ├── Processing │ │ ├── AsyncTask.cs │ │ ├── GetValue.cs │ │ ├── ListeningTarget.cs │ │ ├── MeetingTimeout.cs │ │ ├── MessageReceiver.cs │ │ └── NotFit.cs │ ├── QGroup.cs │ ├── QUser.cs │ └── Route │ │ ├── BuiltIn │ │ ├── RegexRoute.cs │ │ └── TextRoute.cs │ │ ├── RouteInfo.cs │ │ └── RouteResultProcessor.cs ├── QMessage.cs ├── Support │ ├── BotAPI.cs │ ├── BotAuth.cs │ ├── BotInfo.cs │ ├── BotReg.cs │ ├── DataHosting.cs │ └── DataManager.cs └── UserIdentity.cs ├── Docs ├── Manual │ ├── API.md │ ├── API │ │ ├── Attributes.md │ │ ├── Core │ │ │ ├── Middleware.md │ │ │ ├── Module.md │ │ │ ├── UserIdentity.md │ │ │ └── index.md │ │ ├── Meeting │ │ │ ├── AsyncTask.md │ │ │ ├── GetValue.md │ │ │ ├── ListeningTarget.md │ │ │ ├── MeetingTimeout.md │ │ │ ├── MessageReceiver.md │ │ │ ├── NotFit.md │ │ │ └── index.md │ │ ├── Startup │ │ │ ├── BotStartup.md │ │ │ ├── BotStartupConfig.md │ │ │ └── index.md │ │ └── Support │ │ │ ├── BotAPI.md │ │ │ ├── BotAuth.md │ │ │ ├── BotInfo.md │ │ │ ├── BotReg.md │ │ │ ├── DataManager.md │ │ │ └── index.md │ ├── AsyncMeeting.md │ ├── AutoData.md │ ├── BotEventHandler.md │ ├── CustomRoute.md │ ├── Data.md │ ├── Meeting.md │ ├── Permission.md │ └── Route.md ├── Samples │ ├── Blacklist.md │ ├── Cocode.md │ └── Repeater.md ├── Tutorial │ ├── CreateModule.md │ ├── Hellococoa.md │ ├── Overview.md │ └── Route.md ├── Whatsnew │ ├── NewFeatures.md │ └── UpdateLog.md └── index.md ├── LICENSE ├── README.md ├── README_nuget.md └── logo.png /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # IDE cache/options directory or files 36 | .vs/ 37 | .vscode/ 38 | .idea/ 39 | .editorconfig 40 | 41 | # Uncomment if you have tasks that create the project's static files in wwwroot 42 | #wwwroot/ 43 | 44 | # Test project 45 | Test/ 46 | 47 | # Visual Studio 2017 auto generated files 48 | Generated\ Files/ 49 | 50 | # MSTest test Results 51 | [Tt]est[Rr]esult*/ 52 | [Bb]uild[Ll]og.* 53 | 54 | # NUnit 55 | *.VisualState.xml 56 | TestResult.xml 57 | nunit-*.xml 58 | 59 | # Build Results of an ATL Project 60 | [Dd]ebugPS/ 61 | [Rr]eleasePS/ 62 | dlldata.c 63 | 64 | # Benchmark Results 65 | BenchmarkDotNet.Artifacts/ 66 | 67 | # .NET Core 68 | project.lock.json 69 | project.fragment.lock.json 70 | artifacts/ 71 | 72 | # ASP.NET Scaffolding 73 | ScaffoldingReadMe.txt 74 | 75 | # StyleCop 76 | StyleCopReport.xml 77 | 78 | # Files built by Visual Studio 79 | *_i.c 80 | *_p.c 81 | *_h.h 82 | *.ilk 83 | *.meta 84 | *.obj 85 | *.iobj 86 | *.pch 87 | *.pdb 88 | *.ipdb 89 | *.pgc 90 | *.pgd 91 | *.rsp 92 | *.sbr 93 | *.tlb 94 | *.tli 95 | *.tlh 96 | *.tmp 97 | *.tmp_proj 98 | *_wpftmp.csproj 99 | *.log 100 | *.vspscc 101 | *.vssscc 102 | .builds 103 | *.pidb 104 | *.svclog 105 | *.scc 106 | 107 | # Chutzpah Test files 108 | _Chutzpah* 109 | 110 | # Visual C++ cache files 111 | ipch/ 112 | *.aps 113 | *.ncb 114 | *.opendb 115 | *.opensdf 116 | *.sdf 117 | *.cachefile 118 | *.VC.db 119 | *.VC.VC.opendb 120 | 121 | # Visual Studio profiler 122 | *.psess 123 | *.vsp 124 | *.vspx 125 | *.sap 126 | 127 | # Visual Studio Trace Files 128 | *.e2e 129 | 130 | # TFS 2012 Local Workspace 131 | $tf/ 132 | 133 | # Guidance Automation Toolkit 134 | *.gpState 135 | 136 | # ReSharper is a .NET coding add-in 137 | _ReSharper*/ 138 | *.[Rr]e[Ss]harper 139 | *.DotSettings.user 140 | 141 | # TeamCity is a build add-in 142 | _TeamCity* 143 | 144 | # DotCover is a Code Coverage Tool 145 | *.dotCover 146 | 147 | # AxoCover is a Code Coverage Tool 148 | .axoCover/* 149 | !.axoCover/settings.json 150 | 151 | # Coverlet is a free, cross platform Code Coverage Tool 152 | coverage*[.json, .xml, .info] 153 | 154 | # Visual Studio code coverage results 155 | *.coverage 156 | *.coveragexml 157 | 158 | # NCrunch 159 | _NCrunch_* 160 | .*crunch*.local.xml 161 | nCrunchTemp_* 162 | 163 | # MightyMoose 164 | *.mm.* 165 | AutoTest.Net/ 166 | 167 | # Web workbench (sass) 168 | .sass-cache/ 169 | 170 | # Installshield output folder 171 | [Ee]xpress/ 172 | 173 | # DocProject is a documentation generator add-in 174 | DocProject/buildhelp/ 175 | DocProject/Help/*.HxT 176 | DocProject/Help/*.HxC 177 | DocProject/Help/*.hhc 178 | DocProject/Help/*.hhk 179 | DocProject/Help/*.hhp 180 | DocProject/Help/Html2 181 | DocProject/Help/html 182 | 183 | # Click-Once directory 184 | publish/ 185 | 186 | # Publish Web Output 187 | *.[Pp]ublish.xml 188 | *.azurePubxml 189 | # Note: Comment the next line if you want to checkin your web deploy settings, 190 | # but database connection strings (with potential passwords) will be unencrypted 191 | *.pubxml 192 | *.publishproj 193 | 194 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 195 | # checkin your Azure Web App publish settings, but sensitive information contained 196 | # in these scripts will be unencrypted 197 | PublishScripts/ 198 | 199 | # NuGet Packages 200 | *.nupkg 201 | # NuGet Symbol Packages 202 | *.snupkg 203 | # The packages folder can be ignored because of Package Restore 204 | **/[Pp]ackages/* 205 | # except build/, which is used as an MSBuild target. 206 | !**/[Pp]ackages/build/ 207 | # Uncomment if necessary however generally it will be regenerated when needed 208 | #!**/[Pp]ackages/repositories.config 209 | # NuGet v3's project.json files produces more ignorable files 210 | *.nuget.props 211 | *.nuget.targets 212 | 213 | # Microsoft Azure Build Output 214 | csx/ 215 | *.build.csdef 216 | 217 | # Microsoft Azure Emulator 218 | ecf/ 219 | rcf/ 220 | 221 | # Windows Store app package directories and files 222 | AppPackages/ 223 | BundleArtifacts/ 224 | Package.StoreAssociation.xml 225 | _pkginfo.txt 226 | *.appx 227 | *.appxbundle 228 | *.appxupload 229 | 230 | # Visual Studio cache files 231 | # files ending in .cache can be ignored 232 | *.[Cc]ache 233 | # but keep track of directories ending in .cache 234 | !?*.[Cc]ache/ 235 | 236 | # Others 237 | ClientBin/ 238 | ~$* 239 | *~ 240 | *.dbmdl 241 | *.dbproj.schemaview 242 | *.jfm 243 | *.pfx 244 | *.publishsettings 245 | orleans.codegen.cs 246 | 247 | # Including strong name files can present a security risk 248 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 249 | #*.snk 250 | 251 | # Since there are multiple workflows, uncomment next line to ignore bower_components 252 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 253 | #bower_components/ 254 | 255 | # RIA/Silverlight projects 256 | Generated_Code/ 257 | 258 | # Backup & report files from converting an old project file 259 | # to a newer Visual Studio version. Backup files are not needed, 260 | # because we have git ;-) 261 | _UpgradeReport_Files/ 262 | Backup*/ 263 | UpgradeLog*.XML 264 | UpgradeLog*.htm 265 | ServiceFabricBackup/ 266 | *.rptproj.bak 267 | 268 | # SQL Server files 269 | *.mdf 270 | *.ldf 271 | *.ndf 272 | 273 | # Business Intelligence projects 274 | *.rdl.data 275 | *.bim.layout 276 | *.bim_*.settings 277 | *.rptproj.rsuser 278 | *- [Bb]ackup.rdl 279 | *- [Bb]ackup ([0-9]).rdl 280 | *- [Bb]ackup ([0-9][0-9]).rdl 281 | 282 | # Microsoft Fakes 283 | FakesAssemblies/ 284 | 285 | # GhostDoc plugin setting file 286 | *.GhostDoc.xml 287 | 288 | # Node.js Tools for Visual Studio 289 | .ntvs_analysis.dat 290 | node_modules/ 291 | 292 | # Visual Studio 6 build log 293 | *.plg 294 | 295 | # Visual Studio 6 workspace options file 296 | *.opt 297 | 298 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 299 | *.vbw 300 | 301 | # Visual Studio LightSwitch build output 302 | **/*.HTMLClient/GeneratedArtifacts 303 | **/*.DesktopClient/GeneratedArtifacts 304 | **/*.DesktopClient/ModelManifest.xml 305 | **/*.Server/GeneratedArtifacts 306 | **/*.Server/ModelManifest.xml 307 | _Pvt_Extensions 308 | 309 | # Paket dependency manager 310 | .paket/paket.exe 311 | paket-files/ 312 | 313 | # FAKE - F# Make 314 | .fake/ 315 | 316 | # CodeRush personal settings 317 | .cr/personal 318 | 319 | # Python Tools for Visual Studio (PTVS) 320 | __pycache__/ 321 | *.pyc 322 | 323 | # Cake - Uncomment if you are using it 324 | # tools/** 325 | # !tools/packages.config 326 | 327 | # Tabs Studio 328 | *.tss 329 | 330 | # Telerik's JustMock configuration file 331 | *.jmconfig 332 | 333 | # BizTalk build output 334 | *.btp.cs 335 | *.btm.cs 336 | *.odx.cs 337 | *.xsd.cs 338 | 339 | # OpenCover UI analysis results 340 | OpenCover/ 341 | 342 | # Azure Stream Analytics local run output 343 | ASALocalRun/ 344 | 345 | # MSBuild Binary and Structured Log 346 | *.binlog 347 | 348 | # NVidia Nsight GPU debugger configuration file 349 | *.nvuser 350 | 351 | # MFractors (Xamarin productivity tool) working folder 352 | .mfractor/ 353 | 354 | # Local History for Visual Studio 355 | .localhistory/ 356 | 357 | # BeatPulse healthcheck temp database 358 | healthchecksdb 359 | 360 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 361 | MigrationBackup/ 362 | 363 | # Ionide (cross platform F# VS Code tools) working folder 364 | .ionide/ 365 | 366 | # Fody - auto-generated XML schema 367 | FodyWeavers.xsd 368 | -------------------------------------------------------------------------------- /CocoaFramework.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31129.286 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CocoaFramework", "CocoaFramework\CocoaFramework.csproj", "{B3AA3A0C-40BE-47D0-9FF4-81CB188C98D2}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{64AC6751-C57E-42C8-907C-C595EC93B1E7}" 9 | ProjectSection(SolutionItems) = preProject 10 | .editorconfig = .editorconfig 11 | EndProjectSection 12 | EndProject 13 | Global 14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 15 | Debug|Any CPU = Debug|Any CPU 16 | Release|Any CPU = Release|Any CPU 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {B3AA3A0C-40BE-47D0-9FF4-81CB188C98D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {B3AA3A0C-40BE-47D0-9FF4-81CB188C98D2}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {B3AA3A0C-40BE-47D0-9FF4-81CB188C98D2}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {B3AA3A0C-40BE-47D0-9FF4-81CB188C98D2}.Release|Any CPU.Build.0 = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(SolutionProperties) = preSolution 25 | HideSolutionNode = FALSE 26 | EndGlobalSection 27 | GlobalSection(ExtensibilityGlobals) = postSolution 28 | SolutionGuid = {2CBC5CF0-DBCE-4E54-8F02-8B6D912DDA0E} 29 | EndGlobalSection 30 | EndGlobal 31 | -------------------------------------------------------------------------------- /CocoaFramework/AsyncMeeting.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System; 5 | using System.Runtime.CompilerServices; 6 | using System.Text.RegularExpressions; 7 | using System.Threading.Tasks; 8 | using Maila.Cocoa.Framework.Core; 9 | using Maila.Cocoa.Framework.Models.Processing; 10 | 11 | namespace Maila.Cocoa.Framework 12 | { 13 | public class AsyncMeeting 14 | { 15 | public ListeningTarget Target { get; set; } 16 | public TimeSpan Timeout { get; set; } = TimeSpan.Zero; 17 | 18 | private readonly MessageSource src; 19 | 20 | internal AsyncMeeting(MessageSource source) 21 | { 22 | src = source; 23 | Target = ListeningTarget.FromTarget(source); 24 | } 25 | 26 | public int Send(string message) 27 | { 28 | return src.Send(message); 29 | } 30 | 31 | public int Send(MessageBuilder message) 32 | { 33 | return src.Send(message); 34 | } 35 | 36 | public Task SendAsync(string message) 37 | { 38 | return src.SendAsync(message); 39 | } 40 | 41 | public Task SendAsync(MessageBuilder message) 42 | { 43 | return src.SendAsync(message); 44 | } 45 | 46 | 47 | public Task Wait() 48 | { 49 | return Wrapper(new(Target, Timeout)); 50 | } 51 | 52 | public Task WaitFor(string pattern) 53 | { 54 | return Wrapper(new(Target, Timeout, msg => msg.PlainText == pattern)); 55 | } 56 | 57 | public Task WaitFor(Regex regex) 58 | { 59 | return Wrapper(new(Target, Timeout, msg => regex.IsMatch(msg.PlainText))); 60 | } 61 | 62 | public Task WaitFor(Predicate predicator) 63 | { 64 | return Wrapper(new(Target, Timeout, predicator)); 65 | } 66 | 67 | 68 | public async Task SendAndWait(string message) 69 | { 70 | await src.SendAsync(message); 71 | return await new WaitForMessage(Target, Timeout); 72 | } 73 | 74 | public async Task SendAndWait(MessageBuilder message) 75 | { 76 | await src.SendAsync(message); 77 | return await new WaitForMessage(Target, Timeout); 78 | } 79 | 80 | public async Task SendAndWaitFor(string message, string pattern) 81 | { 82 | await src.SendAsync(message); 83 | return await new WaitForMessage(Target, Timeout, msg => msg.PlainText == pattern); 84 | } 85 | 86 | public async Task SendAndWaitFor(MessageBuilder message, string pattern) 87 | { 88 | await src.SendAsync(message); 89 | return await new WaitForMessage(Target, Timeout, msg => msg.PlainText == pattern); 90 | } 91 | 92 | public async Task SendAndWaitFor(string message, Regex regex) 93 | { 94 | await src.SendAsync(message); 95 | return await new WaitForMessage(Target, Timeout, msg => regex.IsMatch(msg.PlainText)); 96 | } 97 | 98 | public async Task SendAndWaitFor(MessageBuilder message, Regex regex) 99 | { 100 | await src.SendAsync(message); 101 | return await new WaitForMessage(Target, Timeout, msg => regex.IsMatch(msg.PlainText)); 102 | } 103 | 104 | public async Task SendAndWaitFor(string message, Predicate predicator) 105 | { 106 | await src.SendAsync(message); 107 | return await new WaitForMessage(Target, Timeout, predicator); 108 | } 109 | 110 | public async Task SendAndWaitFor(MessageBuilder message, Predicate predicator) 111 | { 112 | await src.SendAsync(message); 113 | return await new WaitForMessage(Target, Timeout, predicator); 114 | } 115 | 116 | 117 | public async Task WaitAndSelect( 118 | Func selector, 119 | Predicate? messagePredicator = null, 120 | Predicate? resultPredicator = null) 121 | { 122 | while (true) 123 | { 124 | var msg = await new WaitForMessage(Target, Timeout, messagePredicator); 125 | var result = selector(msg); 126 | if (resultPredicator is null || resultPredicator(result)) 127 | { 128 | return result; 129 | } 130 | } 131 | } 132 | 133 | private static async Task Wrapper(WaitForMessage waitForMessage) 134 | { 135 | return await waitForMessage; 136 | } 137 | } 138 | 139 | internal class WaitForMessage 140 | { 141 | private Action? onReceived = null; 142 | private MessageInfo? result; 143 | private bool received = false; 144 | 145 | private readonly Predicate? predicator; 146 | 147 | public WaitForMessage(ListeningTarget target, TimeSpan timeout, Predicate? predicator = null) 148 | { 149 | this.predicator = predicator; 150 | 151 | ModuleCore.AddLock(OnMessage, target.Pred, timeout, () => OnMessage(null, null)); 152 | } 153 | 154 | public Awaiter GetAwaiter() => new(this); 155 | 156 | private readonly object onMessageLock = new(); 157 | public LockState OnMessage(MessageSource? src, QMessage? msg) 158 | { 159 | lock (onMessageLock) 160 | { 161 | if (received) 162 | { 163 | return LockState.ContinueAndRemove; 164 | } 165 | 166 | if (msg is not null && !(predicator?.Invoke(msg) ?? true)) 167 | { 168 | return LockState.Continue; 169 | } 170 | 171 | received = true; 172 | } 173 | 174 | if (src is not null && msg is not null) 175 | { 176 | result = new(src, msg); 177 | } 178 | 179 | onReceived?.Invoke(); 180 | return LockState.Finished; 181 | } 182 | 183 | public readonly struct Awaiter : INotifyCompletion 184 | { 185 | private readonly WaitForMessage waitForMessage; 186 | public Awaiter(WaitForMessage waitForMessage) 187 | { 188 | this.waitForMessage = waitForMessage; 189 | } 190 | 191 | public MessageInfo? GetResult() => waitForMessage.result; 192 | 193 | public bool IsCompleted => waitForMessage.received; 194 | 195 | public void OnCompleted(Action continuation) 196 | { 197 | waitForMessage.onReceived += continuation; 198 | } 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /CocoaFramework/Attributes.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System; 5 | using System.Reflection; 6 | using System.Text.RegularExpressions; 7 | using Maila.Cocoa.Beans.Models; 8 | using Maila.Cocoa.Framework.Models.Route; 9 | using Maila.Cocoa.Framework.Models.Route.BuiltIn; 10 | 11 | namespace Maila.Cocoa.Framework 12 | { 13 | [AttributeUsage( 14 | AttributeTargets.Class | 15 | AttributeTargets.Method | 16 | AttributeTargets.Field | 17 | AttributeTargets.Parameter)] 18 | public sealed class DisabledAttribute : Attribute { } 19 | 20 | #region === Behavior === 21 | 22 | [AttributeUsage(AttributeTargets.Class)] 23 | public sealed class BotModuleAttribute : Attribute 24 | { 25 | public string? Name { get; } 26 | public int Priority { get; set; } = 0; 27 | 28 | public BotModuleAttribute() { } 29 | public BotModuleAttribute(string? name) 30 | { 31 | Name = name; 32 | } 33 | } 34 | 35 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] 36 | public sealed class IdentityRequirementsAttribute : Attribute 37 | { 38 | internal readonly UserIdentity? Identity; 39 | internal readonly GroupPermission? Permission; 40 | 41 | public IdentityRequirementsAttribute(UserIdentity identity) 42 | { 43 | Identity = identity; 44 | } 45 | public IdentityRequirementsAttribute(GroupPermission permission) 46 | { 47 | Permission = permission; 48 | } 49 | public IdentityRequirementsAttribute(UserIdentity identity, GroupPermission permission) 50 | { 51 | Identity = identity; 52 | Permission = permission; 53 | } 54 | 55 | internal bool Check(UserIdentity identity, GroupPermission? permission) 56 | { 57 | if (Identity is not null && !identity.Fit(Identity.Value)) 58 | { 59 | return false; 60 | } 61 | 62 | if (Permission is not null && (permission is null || Permission.Value > permission.Value)) 63 | { 64 | return false; 65 | } 66 | return true; 67 | } 68 | } 69 | 70 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] 71 | public sealed class DisableInGroupAttribute : Attribute { } 72 | 73 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] 74 | public sealed class DisableInPrivateAttribute : Attribute { } 75 | 76 | #endregion 77 | 78 | #region === Feature === 79 | 80 | [AttributeUsage(AttributeTargets.Method)] 81 | public sealed class ThreadSafeAttribute : Attribute { } 82 | 83 | [AttributeUsage(AttributeTargets.Field)] 84 | public sealed class HostingAttribute : Attribute { } 85 | 86 | #endregion 87 | 88 | #region === Route === 89 | 90 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] 91 | public abstract class RouteAttribute : Attribute 92 | { 93 | public abstract RouteInfo GetRouteInfo(BotModuleBase module, MethodInfo route); 94 | } 95 | 96 | public sealed class RegexRouteAttribute : RouteAttribute 97 | { 98 | public Regex Regex { get; } 99 | public bool AtRequired { get; set; } = false; 100 | 101 | public RegexRouteAttribute(string pattern) 102 | { 103 | Regex = new(pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled); 104 | } 105 | 106 | public RegexRouteAttribute(string pattern, RegexOptions options) 107 | { 108 | Regex = new(pattern, options | RegexOptions.Compiled); 109 | } 110 | 111 | public override RouteInfo GetRouteInfo(BotModuleBase module, MethodInfo route) 112 | { 113 | return new RegexRoute(module, route, Regex, AtRequired); 114 | } 115 | } 116 | 117 | public sealed class TextRouteAttribute : RouteAttribute 118 | { 119 | public string Text { get; } 120 | public bool IgnoreCase { get; set; } = true; 121 | public bool AtRequired { get; set; } = false; 122 | 123 | public TextRouteAttribute(string text) 124 | { 125 | Text = text; 126 | } 127 | 128 | public override RouteInfo GetRouteInfo(BotModuleBase module, MethodInfo route) 129 | { 130 | return new TextRoute(module, route, Text, IgnoreCase, AtRequired); 131 | } 132 | } 133 | 134 | [AttributeUsage(AttributeTargets.Parameter)] 135 | public sealed class GroupNameAttribute : Attribute 136 | { 137 | public string Name { get; } 138 | 139 | public GroupNameAttribute(string name) 140 | { 141 | Name = name; 142 | } 143 | } 144 | 145 | [AttributeUsage(AttributeTargets.Parameter)] 146 | public sealed class MemoryOnlyAttribute : Attribute { } 147 | 148 | [AttributeUsage(AttributeTargets.Parameter)] 149 | public sealed class SharedFromAttribute : Attribute 150 | { 151 | public Type Type { get; } 152 | 153 | public SharedFromAttribute(Type type) 154 | { 155 | Type = type; 156 | } 157 | } 158 | 159 | #endregion 160 | } 161 | -------------------------------------------------------------------------------- /CocoaFramework/AutoData.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System; 5 | using System.Collections.Concurrent; 6 | using Newtonsoft.Json.Linq; 7 | 8 | namespace Maila.Cocoa.Framework 9 | { 10 | public class UserAutoData 11 | { 12 | private readonly Func getter; 13 | private readonly Action setter; 14 | 15 | public T? Value { get => getter(); set => setter(value); } 16 | 17 | internal UserAutoData(ConcurrentDictionary> data, long key1, string key2) 18 | { 19 | var userData = data[key1]; 20 | getter = () => 21 | { 22 | object? val = userData[key2]; 23 | if (val is T t) 24 | { 25 | return t; 26 | } 27 | if (val is JObject jobj) 28 | { 29 | T? newVal = jobj.ToObject(); 30 | userData[key2] = newVal; 31 | return newVal; 32 | } 33 | return default; 34 | }; 35 | setter = val => userData[key2] = val; 36 | } 37 | } 38 | 39 | public class GroupAutoData 40 | { 41 | private readonly Func getter; 42 | private readonly Action setter; 43 | 44 | public T? Value { get => getter(); set => setter(value); } 45 | 46 | internal GroupAutoData(ConcurrentDictionary> data, long key1, string key2) 47 | { 48 | var groupData = data[key1]; 49 | getter = () => 50 | { 51 | object? val = groupData[key2]; 52 | if (val is T t) 53 | { 54 | return t; 55 | } 56 | if (val is JObject jobj) 57 | { 58 | T? newVal = jobj.ToObject(); 59 | groupData[key2] = newVal; 60 | return newVal; 61 | } 62 | return default; 63 | }; 64 | setter = val => groupData[key2] = val; 65 | } 66 | } 67 | 68 | public class SourceAutoData 69 | { 70 | private readonly Func getter; 71 | private readonly Action setter; 72 | 73 | public T? Value { get => getter(); set => setter(value); } 74 | 75 | internal SourceAutoData(ConcurrentDictionary<(long?, long), ConcurrentDictionary> data, (long?, long) key1, string key2) 76 | { 77 | var sourceData = data[key1]; 78 | getter = () => 79 | { 80 | object? val = sourceData[key2]; 81 | if (val is T t) 82 | { 83 | return t; 84 | } 85 | if (val is JObject jobj) 86 | { 87 | T? newVal = jobj.ToObject(); 88 | sourceData[key2] = newVal; 89 | return newVal; 90 | } 91 | return default; 92 | }; 93 | setter = val => sourceData[key2] = val; 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /CocoaFramework/BotEventHandlerBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Collections.Immutable; 7 | using System.Linq; 8 | using System.Reflection; 9 | using Maila.Cocoa.Beans.Models.Events; 10 | 11 | namespace Maila.Cocoa.Framework 12 | { 13 | public abstract class BotEventHandlerBase 14 | { 15 | internal readonly ImmutableDictionary> EventListeners; 16 | 17 | private static readonly Type BaseType = typeof(BotEventHandlerBase); 18 | 19 | protected internal BotEventHandlerBase() 20 | { 21 | Type realType = GetType(); 22 | 23 | Dictionary> listeners = new(); 24 | foreach (var method in realType 25 | .GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) 26 | .Where(m => m.DeclaringType != BaseType && m.GetCustomAttribute() is null)) 27 | { 28 | var parameters = method.GetParameters(); 29 | if (parameters.Length != 1 || !parameters[0].ParameterType.IsSubclassOf(typeof(Event))) 30 | { 31 | continue; 32 | } 33 | 34 | var eventType = parameters[0].ParameterType; 35 | void handler(Event e) 36 | { 37 | method.Invoke(this, new object[] { e }); 38 | } 39 | 40 | if (listeners.ContainsKey(eventType)) 41 | { 42 | listeners[eventType] += handler; 43 | } 44 | else 45 | { 46 | listeners[eventType] = handler; 47 | } 48 | } 49 | 50 | EventListeners = listeners.ToImmutableDictionary(); 51 | } 52 | 53 | internal void HandleEvent(Event evt) 54 | { 55 | if (EventListeners.TryGetValue(evt.GetType(), out var action)) 56 | { 57 | action(evt); 58 | } 59 | } 60 | 61 | protected internal virtual void OnException(Exception e) { } 62 | protected internal virtual void OnDisconnect() { } 63 | } 64 | } -------------------------------------------------------------------------------- /CocoaFramework/BotMiddlewareBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Runtime.CompilerServices; 8 | using Maila.Cocoa.Beans.Models.Messages; 9 | using Maila.Cocoa.Framework.Support; 10 | 11 | namespace Maila.Cocoa.Framework 12 | { 13 | public abstract class BotMiddlewareBase 14 | { 15 | public string DataRoot { get; } 16 | 17 | internal string TypeName { get; } 18 | 19 | private static readonly Type BaseType = typeof(BotModuleBase); 20 | 21 | protected internal BotMiddlewareBase() 22 | { 23 | Type realType = GetType(); 24 | TypeName = realType.Name; 25 | DataRoot = $"MiddlewareData/{TypeName}_{realType.FullName!.CalculateCRC16():X}/"; 26 | 27 | InitOverrode = realType.GetMethod(nameof(Init), BindingFlags.Instance | BindingFlags.NonPublic)!.DeclaringType != BaseType; 28 | DestroyOverrode = realType.GetMethod(nameof(Destroy), BindingFlags.Instance | BindingFlags.NonPublic)!.DeclaringType != BaseType; 29 | 30 | MethodInfo onMessageInfo = realType.GetMethod(nameof(OnMessage), BindingFlags.Instance | BindingFlags.NonPublic)!; 31 | OnMessageOverrode = onMessageInfo.DeclaringType != BaseType && onMessageInfo.GetCustomAttribute() is null; 32 | OnMessageThreadSafe = !OnMessageOverrode 33 | || onMessageInfo.GetCustomAttribute() is not null 34 | || onMessageInfo.GetCustomAttribute() is not null; 35 | 36 | MethodInfo onSendMessageInfo = realType.GetMethod(nameof(OnSendMessage), BindingFlags.Instance | BindingFlags.NonPublic)!; 37 | OnSendMessageOverrode = onSendMessageInfo.DeclaringType != BaseType && onSendMessageInfo.GetCustomAttribute() is null; 38 | OnSendMessageThreadSafe = !OnSendMessageOverrode 39 | || onSendMessageInfo.GetCustomAttribute() is not null 40 | || onSendMessageInfo.GetCustomAttribute() is not null; 41 | 42 | foreach (var field in realType 43 | .GetFields(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) 44 | .Where(f => f.GetCustomAttribute() is not null && f.GetCustomAttribute() is null)) 45 | { 46 | DataHosting.AddHosting(field, this, $"{DataRoot}Field_{field.Name}"); 47 | } 48 | } 49 | 50 | internal bool InitOverrode { get; } 51 | protected internal virtual void Init() { } 52 | 53 | internal bool DestroyOverrode { get; } 54 | protected internal virtual void Destroy() { } 55 | 56 | internal bool OnMessageOverrode { get; } 57 | internal bool OnMessageThreadSafe { get; } 58 | private readonly object onMessageLock = new(); 59 | 60 | protected internal virtual void OnMessage(MessageSource src, QMessage msg, Action next) 61 | { 62 | next(src, msg); 63 | } 64 | 65 | internal void OnMessageInternal(MessageSource src, QMessage msg, Action next) 66 | { 67 | if (!OnMessageOverrode) 68 | { 69 | next(src, msg); 70 | } 71 | if (OnMessageThreadSafe) 72 | { 73 | OnMessage(src, msg, next); 74 | } 75 | else 76 | { 77 | lock (onMessageLock) 78 | { 79 | OnMessage(src, msg, next); 80 | } 81 | } 82 | } 83 | 84 | internal bool OnSendMessageOverrode { get; } 85 | internal bool OnSendMessageThreadSafe { get; } 86 | private readonly object onSendMessageLock = new(); 87 | 88 | protected internal virtual bool OnSendMessage(ref long id, ref bool isGroup, ref IMessage[] chain, ref int? quote) 89 | => true; 90 | 91 | internal bool OnSendMessageInternal(ref long id, ref bool isGroup, ref IMessage[] chain, ref int? quote) 92 | { 93 | if (!OnSendMessageOverrode) 94 | { 95 | return true; 96 | } 97 | if (OnSendMessageThreadSafe) 98 | { 99 | return OnSendMessage(ref id, ref isGroup, ref chain, ref quote); 100 | } 101 | else 102 | { 103 | lock (onSendMessageLock) 104 | { 105 | return OnSendMessage(ref id, ref isGroup, ref chain, ref quote); 106 | } 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /CocoaFramework/BotModuleBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System; 5 | using System.Collections.Concurrent; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Reflection; 9 | using System.Runtime.CompilerServices; 10 | using System.Threading; 11 | using Maila.Cocoa.Framework.Models.Route; 12 | using Maila.Cocoa.Framework.Support; 13 | 14 | namespace Maila.Cocoa.Framework 15 | { 16 | public abstract class BotModuleBase 17 | { 18 | public string? Name { get; } 19 | public int Priority { get; } 20 | 21 | public bool EnableInGroup { get; } 22 | public bool EnableInPrivate { get; } 23 | 24 | public bool IsAnonymous { get; } 25 | public string DataRoot { get; } 26 | 27 | private bool enabled; 28 | public bool Enabled 29 | { 30 | get => enabled; 31 | set 32 | { 33 | enabled = value; 34 | BotReg.SetBool($"MODULE/{TypeName}/ENABLED", value); 35 | } 36 | } 37 | 38 | private readonly Predicate identityPred; 39 | 40 | internal Type RealType { get; } 41 | 42 | internal string TypeName { get; } 43 | 44 | private readonly List routes = new(); 45 | 46 | private static readonly Type BaseType = typeof(BotModuleBase); 47 | 48 | protected internal BotModuleBase() 49 | { 50 | RealType = GetType(); 51 | 52 | #region === Module Info === 53 | 54 | TypeName = RealType.Name; 55 | DataRoot = $"ModuleData/{TypeName}_{RealType.FullName!.CalculateCRC16():X}/"; 56 | if (RealType.GetCustomAttribute() is { } moduleInfo) 57 | { 58 | Name = moduleInfo.Name; 59 | Priority = moduleInfo.Priority; 60 | IsAnonymous = Name is null; 61 | } 62 | 63 | enabled = BotReg.GetBool($"MODULE/{TypeName}/ENABLED", true); 64 | 65 | #endregion 66 | 67 | #region === Conditions === 68 | 69 | var identityReqs = RealType.GetCustomAttributes() 70 | .Select>(r => src => r.Check(src.User.Identity, src.Permission)) 71 | .ToList(); 72 | identityPred = identityReqs.Any() 73 | ? src => identityReqs.Any(p => p(src)) 74 | : src => true; 75 | 76 | EnableInGroup = RealType.GetCustomAttribute() is null; 77 | EnableInPrivate = RealType.GetCustomAttribute() is null; 78 | 79 | #endregion 80 | 81 | #region === Method Info === 82 | 83 | InitOverrode = RealType.GetMethod(nameof(Init), BindingFlags.Instance | BindingFlags.NonPublic)!.DeclaringType != BaseType; 84 | DestroyOverrode = RealType.GetMethod(nameof(Destroy), BindingFlags.Instance | BindingFlags.NonPublic)!.DeclaringType != BaseType; 85 | 86 | MethodInfo onMessageInfo = RealType.GetMethod(nameof(OnMessage), BindingFlags.Instance | BindingFlags.NonPublic)!; 87 | OnMessageOverrode = onMessageInfo.DeclaringType != BaseType && onMessageInfo.GetCustomAttribute() is null; 88 | OnMessageThreadSafe = !OnMessageOverrode 89 | || onMessageInfo.GetCustomAttribute() is not null 90 | || onMessageInfo.GetCustomAttribute() is not null; 91 | OnMessageEnableInGroup = OnMessageOverrode && onMessageInfo.GetCustomAttribute() is null; 92 | OnMessageEnableInPrivate = OnMessageOverrode && onMessageInfo.GetCustomAttribute() is null; 93 | if (OnMessageOverrode) 94 | { 95 | var mreqs = onMessageInfo.GetCustomAttributes() 96 | .Select>(r => src => r.Check(src.User.Identity, src.Permission)) 97 | .ToList(); 98 | onMessagePred = mreqs.Any() 99 | ? src => mreqs.Any(p => p(src)) 100 | : src => true; 101 | } 102 | else 103 | { 104 | onMessagePred = src => false; 105 | } 106 | 107 | MethodInfo onMessageFinishedInfo = RealType.GetMethod(nameof(OnMessageFinished), BindingFlags.Instance | BindingFlags.NonPublic)!; 108 | OnMessageFinishedOverrode = onMessageFinishedInfo.DeclaringType != BaseType && onMessageFinishedInfo.GetCustomAttribute() is null; 109 | OnMessageFinishedThreadSafe = !OnMessageFinishedOverrode 110 | || onMessageFinishedInfo.GetCustomAttribute() is not null 111 | || onMessageFinishedInfo.GetCustomAttribute() is not null; 112 | OnMessageFinishedEnableInGroup = OnMessageFinishedOverrode && onMessageFinishedInfo.GetCustomAttribute() is null; 113 | OnMessageFinishedEnableInPrivate = OnMessageFinishedOverrode && onMessageFinishedInfo.GetCustomAttribute() is null; 114 | 115 | #endregion 116 | 117 | #region === Data Hosting === 118 | 119 | foreach (var field in RealType 120 | .GetFields(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) 121 | .Where(f => f.GetCustomAttribute() is not null 122 | && f.GetCustomAttribute() is null 123 | && !(f.IsStatic && f.IsInitOnly))) 124 | { 125 | DataHosting.AddHosting(field, this, $"{DataRoot}Field_{field.Name}"); 126 | } 127 | 128 | DataHosting.AddOptimizeEnabledHosting( 129 | BaseType.GetField(nameof(userAutoData), BindingFlags.NonPublic | BindingFlags.Instance)!, 130 | this, 131 | $"{DataRoot}UserAutoData"); 132 | 133 | DataHosting.AddOptimizeEnabledHosting( 134 | BaseType.GetField(nameof(groupAutoData), BindingFlags.NonPublic | BindingFlags.Instance)!, 135 | this, 136 | $"{DataRoot}GroupAutoData"); 137 | 138 | DataHosting.AddOptimizeEnabledHosting( 139 | BaseType.GetField(nameof(sourceAutoData), BindingFlags.NonPublic | BindingFlags.Instance)!, 140 | this, 141 | $"{DataRoot}SourceAutoData"); 142 | 143 | #endregion 144 | 145 | #region === Route === 146 | 147 | foreach (var method in RealType.GetMethods()) 148 | { 149 | if (method.HasAttribute()) 150 | { 151 | continue; 152 | } 153 | 154 | var methodThreadSafe = method.HasAttribute() 155 | || method.HasAttribute(); 156 | 157 | var routeProcessLock = methodThreadSafe ? new SemaphoreSlim(1) : null; 158 | 159 | foreach (var route in method.GetCustomAttributes()) 160 | { 161 | var routeInfo = route.GetRouteInfo(this, method); 162 | routeInfo.processLock = routeProcessLock; 163 | routes.Add(routeInfo); 164 | } 165 | } 166 | 167 | #endregion 168 | } 169 | 170 | #region === Event Handling === 171 | 172 | internal bool InitOverrode { get; } 173 | protected internal virtual void Init() { } 174 | 175 | internal bool DestroyOverrode { get; } 176 | protected internal virtual void Destroy() { } 177 | 178 | private readonly Predicate onMessagePred; 179 | internal bool OnMessageOverrode { get; } 180 | internal bool OnMessageThreadSafe { get; } 181 | internal bool OnMessageEnableInGroup { get; } 182 | internal bool OnMessageEnableInPrivate { get; } 183 | private readonly object onMessageLock = new(); 184 | protected internal virtual bool OnMessage(MessageSource src, QMessage msg) 185 | { 186 | return false; 187 | } 188 | internal bool OnMessageInternal(MessageSource src, QMessage msg) 189 | { 190 | if (!identityPred(src)) 191 | { 192 | return false; 193 | } 194 | 195 | if (routes.Any(route => route.Run(src, msg))) 196 | { 197 | return true; 198 | } 199 | 200 | if (!OnMessageOverrode) 201 | { 202 | return false; 203 | } 204 | if (!(src.IsGroup ? OnMessageEnableInGroup : OnMessageEnableInPrivate)) 205 | { 206 | return false; 207 | } 208 | if (!onMessagePred(src)) 209 | { 210 | return false; 211 | } 212 | if (OnMessageThreadSafe) 213 | { 214 | return OnMessage(src, msg); 215 | } 216 | lock (onMessageLock) 217 | { 218 | return OnMessage(src, msg); 219 | } 220 | } 221 | 222 | internal bool OnMessageFinishedOverrode { get; } 223 | internal bool OnMessageFinishedThreadSafe { get; } 224 | internal bool OnMessageFinishedEnableInGroup { get; } 225 | internal bool OnMessageFinishedEnableInPrivate { get; } 226 | private readonly object onMessageFinishedLock = new(); 227 | protected internal virtual void OnMessageFinished(MessageSource src, QMessage msg, MessageSource origSrc, QMessage origMsg, bool processed, BotModuleBase? processModule) { } 228 | internal void OnMessageFinishedInternal(MessageSource src, QMessage msg, MessageSource origSrc, QMessage origMsg, bool processed, BotModuleBase? processModule) 229 | { 230 | if (!OnMessageFinishedOverrode) 231 | { 232 | return; 233 | } 234 | if (!(src.IsGroup ? OnMessageFinishedEnableInGroup : OnMessageFinishedEnableInPrivate)) 235 | { 236 | return; 237 | } 238 | if (OnMessageFinishedThreadSafe) 239 | { 240 | OnMessageFinished(src, msg, origSrc, origMsg, processed, processModule); 241 | } 242 | else 243 | { 244 | lock (onMessageFinishedLock) 245 | { 246 | OnMessageFinished(src, msg, origSrc, origMsg, processed, processModule); 247 | } 248 | } 249 | } 250 | 251 | #endregion 252 | 253 | #region === AutoData === 254 | 255 | private static readonly Type UserAutoDataType = typeof(UserAutoData<>); 256 | private static readonly Type GroupAutoDataType = typeof(GroupAutoData<>); 257 | private static readonly Type SourceAutoDataType = typeof(SourceAutoData<>); 258 | 259 | internal readonly ConcurrentDictionary> userAutoData = new(); 260 | internal readonly ConcurrentDictionary> groupAutoData = new(); 261 | internal readonly ConcurrentDictionary<(long?, long), ConcurrentDictionary> sourceAutoData = new(); 262 | internal readonly ConcurrentDictionary> userTempData = new(); 263 | internal readonly ConcurrentDictionary> groupTempData = new(); 264 | internal readonly ConcurrentDictionary<(long?, long), ConcurrentDictionary> sourceTempData = new(); 265 | 266 | internal readonly ConcurrentDictionary> userAutoDataCache = new(); 267 | internal readonly ConcurrentDictionary> groupAutoDataCache = new(); 268 | internal readonly ConcurrentDictionary<(long?, long), ConcurrentDictionary> sourceAutoDataCache = new(); 269 | internal readonly ConcurrentDictionary> userTempDataCache = new(); 270 | internal readonly ConcurrentDictionary> groupTempDataCache = new(); 271 | internal readonly ConcurrentDictionary<(long?, long), ConcurrentDictionary> sourceTempDataCache = new(); 272 | 273 | internal object? GetUserAutoData(MessageSource src, string name, Type type) 274 | { 275 | var key = src.User.Id; 276 | if (!userAutoDataCache.TryGetValue(key, out var autoDatas)) 277 | { 278 | autoDatas = new(); 279 | userAutoDataCache[key] = autoDatas; 280 | } 281 | if (autoDatas.TryGetValue(name, out var autoData)) 282 | { 283 | return autoData; 284 | } 285 | 286 | if (!userAutoData.TryGetValue(key, out var vals)) 287 | { 288 | vals = new(); 289 | userAutoData[key] = vals; 290 | } 291 | if (!vals.ContainsKey(name)) 292 | { 293 | vals[name] = null; 294 | } 295 | autoData = Activator.CreateInstance(UserAutoDataType.MakeGenericType(type), 296 | BindingFlags.NonPublic | BindingFlags.Instance, null, 297 | new object[] { userAutoData, key, name }, null); 298 | autoDatas[name] = autoData; 299 | return autoData; 300 | } 301 | 302 | internal object? GetGroupAutoData(MessageSource src, string name, Type type) 303 | { 304 | var key = src.Group?.Id ?? 0; 305 | if (!groupAutoDataCache.TryGetValue(key, out var autoDatas)) 306 | { 307 | autoDatas = new(); 308 | groupAutoDataCache[key] = autoDatas; 309 | } 310 | if (autoDatas.TryGetValue(name, out var autoData)) 311 | { 312 | return autoData; 313 | } 314 | 315 | if (!groupAutoData.TryGetValue(key, out var vals)) 316 | { 317 | vals = new(); 318 | groupAutoData[key] = vals; 319 | } 320 | if (!vals.ContainsKey(name)) 321 | { 322 | vals[name] = null; 323 | } 324 | autoData = Activator.CreateInstance(GroupAutoDataType.MakeGenericType(type), 325 | BindingFlags.NonPublic | BindingFlags.Instance, null, 326 | new object[] { groupAutoData, key, name }, null); 327 | autoDatas[name] = autoData; 328 | return autoData; 329 | } 330 | 331 | internal object? GetSourceAutoData(MessageSource src, string name, Type type) 332 | { 333 | var key = (src.Group?.Id, src.User.Id); 334 | if (!sourceAutoDataCache.TryGetValue(key, out var autoDatas)) 335 | { 336 | autoDatas = new(); 337 | sourceAutoDataCache[key] = autoDatas; 338 | } 339 | if (autoDatas.TryGetValue(name, out var autoData)) 340 | { 341 | return autoData; 342 | } 343 | 344 | if (!sourceAutoData.TryGetValue(key, out var vals)) 345 | { 346 | vals = new(); 347 | sourceAutoData[key] = vals; 348 | } 349 | if (!vals.ContainsKey(name)) 350 | { 351 | vals[name] = null; 352 | } 353 | autoData = Activator.CreateInstance(SourceAutoDataType.MakeGenericType(type), 354 | BindingFlags.NonPublic | BindingFlags.Instance, null, 355 | new object[] { sourceAutoData, key, name }, null); 356 | autoDatas[name] = autoData; 357 | return autoData; 358 | } 359 | 360 | internal object? GetUserTempData(MessageSource src, string name, Type type) 361 | { 362 | var key = src.User.Id; 363 | if (!userTempDataCache.TryGetValue(key, out var autoDatas)) 364 | { 365 | autoDatas = new(); 366 | userTempDataCache[key] = autoDatas; 367 | } 368 | if (autoDatas.TryGetValue(name, out var autoData)) 369 | { 370 | return autoData; 371 | } 372 | 373 | if (!userTempData.TryGetValue(key, out var vals)) 374 | { 375 | vals = new(); 376 | userTempData[key] = vals; 377 | } 378 | if (!vals.ContainsKey(name)) 379 | { 380 | vals[name] = null; 381 | } 382 | autoData = Activator.CreateInstance(UserAutoDataType.MakeGenericType(type), 383 | BindingFlags.NonPublic | BindingFlags.Instance, null, 384 | new object[] { userTempData, key, name }, null); 385 | autoDatas[name] = autoData; 386 | return autoData; 387 | } 388 | 389 | internal object? GetGroupTempData(MessageSource src, string name, Type type) 390 | { 391 | var key = src.Group?.Id ?? 0; 392 | if (!groupTempDataCache.TryGetValue(key, out var autoDatas)) 393 | { 394 | autoDatas = new(); 395 | groupTempDataCache[key] = autoDatas; 396 | } 397 | if (autoDatas.TryGetValue(name, out var autoData)) 398 | { 399 | return autoData; 400 | } 401 | 402 | if (!groupTempData.TryGetValue(key, out var vals)) 403 | { 404 | vals = new(); 405 | groupTempData[key] = vals; 406 | } 407 | if (!vals.ContainsKey(name)) 408 | { 409 | vals[name] = null; 410 | } 411 | autoData = Activator.CreateInstance(GroupAutoDataType.MakeGenericType(type), 412 | BindingFlags.NonPublic | BindingFlags.Instance, null, 413 | new object[] { groupTempData, key, name }, null); 414 | autoDatas[name] = autoData; 415 | return autoData; 416 | } 417 | 418 | internal object? GetSourceTempData(MessageSource src, string name, Type type) 419 | { 420 | var key = (src.Group?.Id, src.User.Id); 421 | if (!sourceTempDataCache.TryGetValue(key, out var autoDatas)) 422 | { 423 | autoDatas = new(); 424 | sourceTempDataCache[key] = autoDatas; 425 | } 426 | if (autoDatas.TryGetValue(name, out var autoData)) 427 | { 428 | return autoData; 429 | } 430 | 431 | if (!sourceTempData.TryGetValue(key, out var vals)) 432 | { 433 | vals = new(); 434 | sourceTempData[key] = vals; 435 | } 436 | if (!vals.ContainsKey(name)) 437 | { 438 | vals[name] = null; 439 | } 440 | autoData = Activator.CreateInstance(SourceAutoDataType.MakeGenericType(type), 441 | BindingFlags.NonPublic | BindingFlags.Instance, null, 442 | new object[] { sourceTempData, key, name }, null); 443 | autoDatas[name] = autoData; 444 | return autoData; 445 | } 446 | 447 | #endregion 448 | } 449 | } 450 | -------------------------------------------------------------------------------- /CocoaFramework/BotStartup.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Reflection; 7 | using System.Threading.Tasks; 8 | using Maila.Cocoa.Framework.Core; 9 | 10 | namespace Maila.Cocoa.Framework 11 | { 12 | public static class BotStartup 13 | { 14 | public static bool Connected => BotCore.Connected; 15 | 16 | [Obsolete("Use ConnectAndInit instead.")] 17 | public static Task Connect(BotStartupConfig config) 18 | => BotCore.Connect(config); 19 | 20 | [Obsolete("Use DisconnectAndSaveData instead.")] 21 | public static Task Disconnect() 22 | => BotCore.Disconnect(); 23 | 24 | /// Connect to the server, and automatically release the existing connection if it exists. 25 | public static Task ConnectAndInit(BotStartupConfig config) 26 | => BotCore.ConnectAndInit(config); 27 | 28 | /// Disconnect and release related resources. 29 | public static Task DisconnectAndSaveData() 30 | => BotCore.DisconnectAndSaveData(); 31 | 32 | /// Reconnect to the server without releasing resources. 33 | public static Task Reconnect() 34 | => BotCore.Reconnect(); 35 | } 36 | 37 | public class BotStartupConfig 38 | { 39 | public string host; 40 | public int port; 41 | public string verifyKey; 42 | public long qqId; 43 | 44 | internal List Middlewares { get; } = new(); 45 | public List Assemblies { get; } = new(); 46 | public List EventHandlers { get; } = new(); 47 | public TimeSpan autoSave; 48 | 49 | public BotStartupConfig(string verifyKey, long qqId, string host) : this(verifyKey, qqId, host, 80) { } 50 | public BotStartupConfig(string verifyKey, long qqId, int port) : this(verifyKey, qqId, "127.0.0.1", port) { } 51 | public BotStartupConfig(string verifyKey, long qqId, string host = "127.0.0.1", int port = 8080) 52 | { 53 | this.verifyKey = verifyKey; 54 | this.qqId = qqId; 55 | this.host = host; 56 | this.port = port; 57 | Assemblies.Add(Assembly.GetEntryAssembly()!); 58 | autoSave = TimeSpan.FromMinutes(5); 59 | } 60 | 61 | public BotStartupConfig AddMiddleware() where T : BotMiddlewareBase 62 | { 63 | Middlewares.Add(typeof(T)); 64 | return this; 65 | } 66 | 67 | public BotStartupConfig AddMiddleware(Type type) 68 | { 69 | if (type.IsAssignableTo(typeof(BotMiddlewareBase))) 70 | { 71 | Middlewares.Add(type); 72 | } 73 | return this; 74 | } 75 | 76 | public BotStartupConfig AddAssembly(Assembly assem) 77 | { 78 | Assemblies.Add(assem); 79 | return this; 80 | } 81 | 82 | public BotStartupConfig AddEventHandler(BotEventHandlerBase handler) 83 | { 84 | EventHandlers.Add(handler); 85 | return this; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /CocoaFramework/CocoaFramework.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | Maila.Cocoa.Framework 6 | enable 7 | 2.1.8.3 8 | Maila 9 | Miyakowww 10 | Cocoa Framework 11 | Maila.Cocoa.Framework 12 | true 13 | An efficient, intelligent and convenient QQ robot development framework. 14 | Copyright © Maila 2021 15 | 16 | https://github.com/Miyakowww/CocoaFramework2 17 | git 18 | mirai mirai-api-http maila cocoa 19 | true 20 | true 21 | true 22 | true 23 | true 24 | AGPL-3.0-or-later 25 | README_nuget.md 26 | 27 | 28 | 29 | 1591;1701;1702 30 | 31 | 32 | 33 | 1591;1701;1702 34 | 35 | 36 | 37 | 38 | True 39 | \ 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /CocoaFramework/Core/BotCore.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System; 5 | using System.Diagnostics.CodeAnalysis; 6 | using System.Linq; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using Maila.Cocoa.Beans.API; 10 | using Maila.Cocoa.Framework.Support; 11 | 12 | namespace Maila.Cocoa.Framework.Core 13 | { 14 | internal static class BotCore 15 | { 16 | public static long? BindingQQ { get; private set; } 17 | public static string? SessionKey { get; private set; } 18 | 19 | [MemberNotNullWhen(true, new[] { nameof(host), nameof(BindingQQ), nameof(SessionKey) })] 20 | public static bool Connected => host is not null && BindingQQ is not null && SessionKey is not null; 21 | 22 | internal static string? host; 23 | internal static string verifyKey = string.Empty; 24 | 25 | private static readonly SemaphoreSlim connLock = new(1); 26 | 27 | public static async Task TestNetwork() 28 | => host is not null && await MiraiAPI.About(host) is not null; 29 | 30 | [Obsolete("Use ConnectAndInit instead.")] 31 | public static Task Connect(BotStartupConfig config) 32 | { 33 | return ConnectAndInit(config); 34 | } 35 | 36 | public static async Task ConnectAndInit(BotStartupConfig config) 37 | { 38 | await connLock.WaitAsync(); 39 | 40 | if (Connected) 41 | { 42 | try 43 | { 44 | await MiraiAPI.Release(host, SessionKey, BindingQQ.Value); 45 | } 46 | catch { } 47 | } 48 | 49 | host = $"{config.host}:{config.port}"; 50 | 51 | string? ver = await MiraiAPI.About(host); 52 | if (ver is null) 53 | { 54 | host = null; 55 | SessionKey = null; 56 | BindingQQ = null; 57 | connLock.Release(); 58 | return false; 59 | } 60 | 61 | bool IsVer2 = ver.StartsWith('2'); 62 | try 63 | { 64 | verifyKey = config.verifyKey; 65 | SessionKey = await (IsVer2 ? MiraiAPI.Verify(host, verifyKey) : MiraiAPI.Authv1(host, verifyKey)); 66 | if (SessionKey is null) 67 | { 68 | host = null; 69 | SessionKey = null; 70 | BindingQQ = null; 71 | connLock.Release(); 72 | return false; 73 | } 74 | await (IsVer2 ? MiraiAPI.Bind(host, SessionKey, config.qqId) : MiraiAPI.Verifyv1(host, SessionKey, config.qqId)); 75 | BindingQQ = config.qqId; 76 | if (!IsVer2) 77 | { 78 | await MiraiAPI.SetConfig(host, SessionKey, null, true); 79 | } 80 | } 81 | catch 82 | { 83 | host = null; 84 | SessionKey = null; 85 | BindingQQ = null; 86 | connLock.Release(); 87 | return false; 88 | } 89 | 90 | try 91 | { 92 | BotAPI.Init(config.EventHandlers); 93 | BotAuth.Init(); 94 | BotReg.Init(); 95 | 96 | await BotInfo.ReloadAll(); 97 | 98 | ModuleCore.Init(config.Assemblies.Distinct()); 99 | MiddlewareCore.Init(config.Middlewares); 100 | 101 | DataHosting.StartHosting(config.autoSave); 102 | 103 | return true; 104 | } 105 | finally 106 | { 107 | connLock.Release(); 108 | } 109 | } 110 | 111 | [Obsolete("Use DisconnectAndSaveData instead.")] 112 | public static Task Disconnect() 113 | { 114 | return DisconnectAndSaveData(); 115 | } 116 | 117 | public static async Task DisconnectAndSaveData() 118 | { 119 | await connLock.WaitAsync(); 120 | 121 | if (Connected) 122 | { 123 | try 124 | { 125 | await MiraiAPI.Release(host, SessionKey, BindingQQ.Value); 126 | } 127 | catch { } 128 | } 129 | 130 | host = null; 131 | SessionKey = null; 132 | BindingQQ = null; 133 | 134 | try 135 | { 136 | await DataHosting.StopHosting(); 137 | 138 | BotAPI.Reset(); 139 | BotAuth.Reset(); 140 | BotReg.Reset(); 141 | BotInfo.Reset(); 142 | 143 | ModuleCore.Reset(); 144 | MiddlewareCore.Reset(); 145 | 146 | while (DataManager.SavingData) 147 | { 148 | await Task.Delay(10); 149 | } 150 | } 151 | finally 152 | { 153 | connLock.Release(); 154 | } 155 | } 156 | 157 | public static async Task Reconnect() 158 | { 159 | if (!Connected) 160 | { 161 | return; 162 | } 163 | 164 | await connLock.WaitAsync(); 165 | 166 | try 167 | { 168 | await MiraiAPI.Release(host, SessionKey, BindingQQ.Value); 169 | } 170 | catch { } 171 | 172 | string? ver = await MiraiAPI.About(host); 173 | if (ver is null) 174 | { 175 | SessionKey = null; 176 | BotAPI.Reset(); 177 | connLock.Release(); 178 | return; 179 | } 180 | 181 | bool IsVer2 = ver.StartsWith('2'); 182 | 183 | try 184 | { 185 | SessionKey = await (IsVer2 ? MiraiAPI.Verify(host, verifyKey) : MiraiAPI.Authv1(host, verifyKey)); 186 | if (SessionKey is null) 187 | { 188 | BotAPI.Reset(); 189 | return; 190 | } 191 | await (IsVer2 ? MiraiAPI.Bind(host, SessionKey, BindingQQ.Value) : MiraiAPI.Verifyv1(host, SessionKey, BindingQQ.Value)); 192 | if (!IsVer2) 193 | { 194 | await MiraiAPI.SetConfig(host, SessionKey, null, true); 195 | } 196 | } 197 | catch 198 | { 199 | SessionKey = null; 200 | BotAPI.Reset(); 201 | return; 202 | } 203 | finally 204 | { 205 | connLock.Release(); 206 | } 207 | } 208 | 209 | internal static void OnMessage(MessageSource src, QMessage msg) 210 | { 211 | try 212 | { 213 | MiddlewareCore.OnMessage?.Invoke(src, msg); 214 | } 215 | catch (AggregateException ae) 216 | { 217 | Exception e = ae; 218 | while (e.InnerException is not null) 219 | { 220 | if (e.InnerException.Message.Contains("InternalServerError")) 221 | { 222 | _ = Reconnect(); 223 | return; 224 | } 225 | e = e.InnerException; 226 | } 227 | BotAPI.OnException(e); 228 | } 229 | catch (Exception e) 230 | { 231 | BotAPI.OnException(e); 232 | } 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /CocoaFramework/Core/MiddlewareCore.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Collections.Immutable; 7 | using System.Linq; 8 | using System.Linq.Expressions; 9 | using System.Reflection; 10 | using Maila.Cocoa.Beans.Models.Messages; 11 | 12 | namespace Maila.Cocoa.Framework.Core 13 | { 14 | public static class MiddlewareCore 15 | { 16 | public static ImmutableArray Middlewares { get; private set; } 17 | internal static Action? OnMessage { get; private set; } 18 | 19 | internal static void Init(IEnumerable middlewares) 20 | { 21 | Middlewares = middlewares 22 | .Where(m => m.GetCustomAttribute() is null) 23 | .Select(Activator.CreateInstance) 24 | .Cast() 25 | .ToImmutableArray(); 26 | 27 | foreach (var middleware in Middlewares) 28 | { 29 | if (middleware.InitOverrode) 30 | { 31 | middleware.Init(); 32 | } 33 | } 34 | 35 | if (!middlewares.Any()) 36 | { 37 | OnMessage = (src, msg) => ModuleCore.OnMessage(src, msg, src, msg); 38 | return; 39 | } 40 | 41 | #region === Compile OnMessage === 42 | 43 | ParameterExpression 44 | src = Expression.Parameter(typeof(MessageSource)), 45 | msg = Expression.Parameter(typeof(QMessage)), 46 | origSrc = Expression.Parameter(typeof(MessageSource)), 47 | origMsg = Expression.Parameter(typeof(QMessage)); 48 | 49 | MethodCallExpression call = Expression.Call( 50 | typeof(ModuleCore).GetMethod(nameof(ModuleCore.OnMessage), BindingFlags.Static | BindingFlags.NonPublic)!, 51 | src, msg, origSrc, origMsg); 52 | 53 | MethodInfo onMessageInfo = typeof(BotMiddlewareBase) 54 | .GetMethod(nameof(BotMiddlewareBase.OnMessageInternal), BindingFlags.Instance | BindingFlags.NonPublic)!; 55 | 56 | for (int i = 1; i <= Middlewares.Length; i++) 57 | { 58 | if (!Middlewares[^i].OnMessageOverrode 59 | || Middlewares[^i] 60 | .GetType() 61 | .GetMethod(nameof(BotMiddlewareBase.OnMessage), BindingFlags.Instance | BindingFlags.NonPublic)! 62 | .GetCustomAttribute() is not null) 63 | { 64 | continue; 65 | } 66 | 67 | LambdaExpression next = Expression.Lambda>(call, src, msg); 68 | 69 | src = Expression.Parameter(typeof(MessageSource)); 70 | msg = Expression.Parameter(typeof(QMessage)); 71 | 72 | call = Expression.Call( 73 | Expression.Constant(Middlewares[^i]), 74 | onMessageInfo, 75 | src, msg, next); 76 | } 77 | 78 | OnMessage = Expression.Lambda>(Expression.Block( 79 | new[] { src, msg }, 80 | Expression.Assign(src, origSrc), 81 | Expression.Assign(msg, origMsg), 82 | call 83 | ), origSrc, origMsg).Compile(); 84 | 85 | #endregion 86 | } 87 | 88 | internal static void Reset() 89 | { 90 | foreach (var middleware in Middlewares.Where(middleware => middleware.DestroyOverrode)) 91 | { 92 | middleware.Destroy(); 93 | } 94 | 95 | Middlewares = ImmutableArray.Empty; 96 | OnMessage = null; 97 | } 98 | 99 | internal static bool OnSendMessage(ref long id, ref bool isGroup, ref IMessage[] chain, ref int? quote) 100 | { 101 | foreach (var m in Middlewares) 102 | { 103 | bool send = m.OnSendMessageInternal(ref id, ref isGroup, ref chain, ref quote); 104 | if (!send) 105 | { 106 | return false; 107 | } 108 | } 109 | return true; 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /CocoaFramework/Core/ModuleCore.MessageLock.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Maila.Cocoa.Framework.Models.Processing; 9 | 10 | namespace Maila.Cocoa.Framework.Core 11 | { 12 | public static partial class ModuleCore 13 | { 14 | private static readonly List> messageLocks = new(); 15 | 16 | public static void AddLock(Func lockRun) 17 | => messageLocks.Add(lockRun); 18 | 19 | public static void AddLock(Func lockRun, Predicate predicate) 20 | => messageLocks.Add(new MessageLock(lockRun, predicate, TimeSpan.Zero, null).Run); 21 | 22 | public static void AddLock(Func lockRun, ListeningTarget target) 23 | => messageLocks.Add(new MessageLock(lockRun, target.Pred, TimeSpan.Zero, null).Run); 24 | 25 | public static void AddLock(Func lockRun, MessageSource src) 26 | => messageLocks.Add(new MessageLock(lockRun, s => s.Equals(src), TimeSpan.Zero, null).Run); 27 | 28 | public static void AddLock(Func lockRun, Predicate predicate, TimeSpan timeout, Action? onTimeout = null) 29 | => messageLocks.Add(new MessageLock(lockRun, predicate, timeout, onTimeout).Run); 30 | 31 | public static void AddLock(Func lockRun, ListeningTarget target, TimeSpan timeout, Action? onTimeout = null) 32 | => messageLocks.Add(new MessageLock(lockRun, target.Pred, timeout, onTimeout).Run); 33 | 34 | public static void AddLock(Func lockRun, MessageSource src, TimeSpan timeout, Action? onTimeout = null) 35 | => messageLocks.Add(new MessageLock(lockRun, s => s.Equals(src), timeout, onTimeout).Run); 36 | 37 | private class MessageLock 38 | { 39 | public readonly Predicate predicate; 40 | public readonly Func run; 41 | private readonly TimeSpan timeout; 42 | private readonly Action? onTimeout; 43 | private int counter; 44 | private DateTime lastRun; 45 | 46 | private CancellationTokenSource lastToken = new(); 47 | private readonly SemaphoreSlim runningLock = new(1); 48 | 49 | private readonly Action timeoutAction; 50 | 51 | public MessageLock(Func run, Predicate predicate, TimeSpan timeout, Action? onTimeout) 52 | { 53 | this.predicate = predicate; 54 | this.run = run; 55 | this.timeout = timeout; 56 | this.onTimeout = onTimeout; 57 | if (timeout <= TimeSpan.Zero) 58 | { 59 | timeoutAction = (_, _) => { }; 60 | return; 61 | } 62 | lastRun = DateTime.Now; 63 | timeoutAction = async (count, token) => 64 | { 65 | try 66 | { 67 | await Task.Delay(this.timeout, token); 68 | // Make a time gap with timeout judgment to avoid boundary problems 69 | await Task.Delay(10, token); 70 | } 71 | catch (TaskCanceledException) { return; } 72 | 73 | if (counter != count || runningLock.CurrentCount < 1) 74 | { 75 | return; 76 | } 77 | messageLocks.Remove(Run); 78 | this.onTimeout?.Invoke(); 79 | }; 80 | int count = counter; 81 | Task.Run(() => timeoutAction(count, lastToken.Token)); 82 | } 83 | 84 | public LockState Run(MessageSource src, QMessage msg) 85 | { 86 | if (timeout > TimeSpan.Zero && DateTime.Now - lastRun > timeout) 87 | { 88 | return LockState.Continue; 89 | } 90 | if (!predicate(src)) 91 | { 92 | return LockState.Continue; 93 | } 94 | 95 | runningLock.Wait(); 96 | 97 | var state = (InternalLockState)run(src, msg); 98 | if (timeout > TimeSpan.Zero) 99 | { 100 | if (state.HasFlag(InternalLockState.Remove)) 101 | { 102 | counter++; 103 | lastToken.Cancel(); 104 | } 105 | else if (state == InternalLockState.Processed) 106 | { 107 | lastRun = DateTime.Now; 108 | counter++; 109 | lastToken.Cancel(); 110 | lastToken = new(); 111 | int count = counter; 112 | new Task(() => timeoutAction(count, lastToken.Token)).Start(); 113 | } 114 | } 115 | 116 | runningLock.Release(); 117 | return (LockState)state; 118 | } 119 | } 120 | } 121 | 122 | public enum LockState 123 | { 124 | Finished = 0b11, 125 | NotFinished = 0b01, 126 | Continue = 0b00, 127 | ContinueAndRemove = 0b10, 128 | } 129 | 130 | [Flags] 131 | internal enum InternalLockState 132 | { 133 | Processed = 0b01, 134 | Remove = 0b10, 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /CocoaFramework/Core/ModuleCore.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Collections.Immutable; 7 | using System.Linq; 8 | using System.Reflection; 9 | using System.Threading.Tasks; 10 | 11 | namespace Maila.Cocoa.Framework.Core 12 | { 13 | public static partial class ModuleCore 14 | { 15 | public static ImmutableArray Modules { get; private set; } 16 | 17 | internal static void Init(IEnumerable assemblies) 18 | { 19 | List modules = new(); 20 | foreach (var assem in assemblies) 21 | { 22 | foreach (var type in assem.GetTypes()) 23 | { 24 | if (!type.IsAssignableTo(typeof(BotModuleBase))) 25 | { 26 | continue; 27 | } 28 | 29 | if (type.GetCustomAttribute() is not null) 30 | { 31 | continue; 32 | } 33 | 34 | if (type.GetCustomAttribute() is null) 35 | { 36 | continue; 37 | } 38 | 39 | if (Activator.CreateInstance(type) is not BotModuleBase module) 40 | { 41 | continue; 42 | } 43 | 44 | if (module.InitOverrode) 45 | { 46 | module.Init(); 47 | } 48 | 49 | modules.Add(module); 50 | } 51 | } 52 | 53 | modules.Sort((a, b) => a.Priority.CompareTo(b.Priority)); 54 | Modules = modules.ToImmutableArray(); 55 | } 56 | 57 | internal static void Reset() 58 | { 59 | foreach (var module in Modules.Where(module => module.DestroyOverrode)) 60 | { 61 | module.Destroy(); 62 | } 63 | 64 | Modules = ImmutableArray.Empty; 65 | } 66 | 67 | internal static void OnMessage(MessageSource src, QMessage msg, MessageSource origSrc, QMessage origMsg) 68 | { 69 | for (int i = messageLocks.Count - 1; i >= 0; i--) 70 | { 71 | var state = (InternalLockState)messageLocks[i](src, msg); 72 | if (state.HasFlag(InternalLockState.Remove)) 73 | { 74 | messageLocks.RemoveAt(i); 75 | } 76 | 77 | if (state.HasFlag(InternalLockState.Processed)) 78 | { 79 | OnMessageFinished(src, msg, origSrc, origMsg, true, null); 80 | return; 81 | } 82 | } 83 | 84 | foreach (var module in Modules.Where(m => m.Enabled && (src.IsGroup ? m.EnableInGroup : m.EnableInPrivate))) 85 | { 86 | try 87 | { 88 | if (!module.OnMessageInternal(src, msg)) 89 | { 90 | continue; 91 | } 92 | } 93 | catch (Exception e) 94 | { 95 | throw new AggregateException($"Module Run Error: {module.TypeName}", e); 96 | } 97 | OnMessageFinished(src, msg, origSrc, origMsg, true, module); 98 | return; 99 | } 100 | 101 | OnMessageFinished(src, msg, origSrc, origMsg, false, null); 102 | } 103 | 104 | private static void OnMessageFinished(MessageSource src, QMessage msg, MessageSource origSrc, QMessage origMsg, bool processed, BotModuleBase? processModule) 105 | { 106 | foreach (var module in Modules.Where(module => module.Enabled && (src.IsGroup ? module.EnableInGroup : module.EnableInPrivate))) 107 | { 108 | _ = Task.Run(() => module.OnMessageFinishedInternal(src, msg, origSrc, origMsg, processed, processModule)); 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /CocoaFramework/Extensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System; 5 | using System.Diagnostics.CodeAnalysis; 6 | using System.Reflection; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using Maila.Cocoa.Beans.API; 10 | using Maila.Cocoa.Beans.Models.Events; 11 | using Maila.Cocoa.Framework.Support; 12 | 13 | namespace Maila.Cocoa.Framework 14 | { 15 | public static partial class Extensions 16 | { 17 | #region === User Identity === 18 | 19 | public static bool Fit(this UserIdentity identity, UserIdentity requirements) 20 | { 21 | return (identity & requirements) == requirements; 22 | } 23 | 24 | #endregion 25 | 26 | #region === Request Event === 27 | 28 | public static Task Response(this NewFriendRequestEvent e, NewFriendRequestOperate operate, string message = "") 29 | => BotAPI.NewFriendRequestResp(e, operate, message); 30 | 31 | public static Task Response(this MemberJoinRequestEvent e, MemberJoinRequestOperate operate, string message = "") 32 | => BotAPI.MemberJoinRequestResp(e, operate, message); 33 | 34 | public static Task Response(this BotInvitedJoinGroupRequestEvent e, BotInvitedJoinGroupRequestOperate operate, string message = "") 35 | => BotAPI.BotInvitedJoinGroupRequestResp(e, operate, message); 36 | 37 | #endregion 38 | 39 | #region === Attribute === 40 | 41 | public static bool TryGetAttribute(this MemberInfo member, [NotNullWhen(true)] out T? attribute) where T : Attribute 42 | { 43 | attribute = member?.GetCustomAttribute(); 44 | return attribute != null; 45 | } 46 | 47 | public static bool HasAttribute(this MemberInfo member) where T : Attribute 48 | { 49 | return member?.IsDefined(typeof(T), true) ?? false; 50 | } 51 | 52 | public static bool TryGetAttribute(this ParameterInfo parameter, [NotNullWhen(true)] out T? attribute) where T : Attribute 53 | { 54 | attribute = parameter?.GetCustomAttribute(); 55 | return attribute != null; 56 | } 57 | 58 | public static bool HasAttribute(this ParameterInfo parameter) where T : Attribute 59 | { 60 | return parameter?.IsDefined(typeof(T), true) ?? false; 61 | } 62 | 63 | #endregion 64 | 65 | internal static ushort CalculateCRC16(this string str) 66 | { 67 | if (string.IsNullOrEmpty(str)) 68 | { 69 | return 0; 70 | } 71 | 72 | byte[] data = Encoding.UTF8.GetBytes(str); 73 | uint crc = 0xFFFF; 74 | foreach (var b in data) 75 | { 76 | crc ^= b; 77 | for (int i = 0; i < 8; i++) 78 | { 79 | crc = (crc & 1) != 0 ? (crc >> 1) ^ 0xA001 : crc >> 1; 80 | crc &= 0xFFFF; 81 | } 82 | } 83 | return (ushort)crc; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /CocoaFramework/MessageBuilder.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System.Collections; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | using Maila.Cocoa.Beans.API; 8 | using Maila.Cocoa.Beans.Models.Messages; 9 | using Maila.Cocoa.Framework.Support; 10 | 11 | namespace Maila.Cocoa.Framework 12 | { 13 | public class MessageBuilder : IEnumerable 14 | { 15 | private readonly List chain = new(); 16 | 17 | public MessageBuilder Add(IMessage message) 18 | { 19 | chain.Add(message); 20 | return this; 21 | } 22 | 23 | public MessageBuilder AddAll(IEnumerable messages) 24 | { 25 | chain.AddRange(messages); 26 | return this; 27 | } 28 | 29 | public MessageBuilder Insert(int index, IMessage message) 30 | { 31 | chain.Insert(index, message); 32 | return this; 33 | } 34 | 35 | public MessageBuilder RemoveAt(int index) 36 | { 37 | chain.RemoveAt(index); 38 | return this; 39 | } 40 | 41 | public MessageBuilder RemoveAll() where T : IMessage 42 | { 43 | for (int i = chain.Count - 1; i >= 0; i--) 44 | { 45 | if (chain[i] is T) 46 | { 47 | chain.RemoveAt(i); 48 | } 49 | } 50 | return this; 51 | } 52 | 53 | public MessageBuilder Clear() 54 | { 55 | chain.Clear(); 56 | return this; 57 | } 58 | 59 | public IMessage[] ToMessageChain() 60 | { 61 | return chain.ToArray(); 62 | } 63 | 64 | #region === Messages === 65 | 66 | public MessageBuilder At(long target) 67 | { 68 | chain.Add(new AtMessage(target)); 69 | return this; 70 | } 71 | 72 | public MessageBuilder AtAll() 73 | { 74 | chain.Add(AtAllMessage.Instance); 75 | return this; 76 | } 77 | 78 | public MessageBuilder Face(int id) 79 | { 80 | chain.Add(new FaceMessage(id)); 81 | return this; 82 | } 83 | 84 | public MessageBuilder Plain(string text) 85 | { 86 | chain.Add(new PlainMessage(text)); 87 | return this; 88 | } 89 | 90 | public MessageBuilder MiraiCode(string code) 91 | { 92 | chain.Add(new MiraiCodeMessage(code)); 93 | return this; 94 | } 95 | 96 | 97 | public static Task Image(UploadType type, string path) 98 | => BotAPI.UploadImage(type, path); 99 | 100 | public static async Task FlashImage(UploadType type, string path) 101 | => ((ImageMessage)await BotAPI.UploadImage(type, path)).ToFlashImage(); 102 | 103 | public static Task Voice(string path) 104 | => BotAPI.UploadVoice(path); 105 | 106 | public static IXmlMessage Xml(string xml) 107 | => new XmlMessage(xml); 108 | 109 | public static IJsonMessage Json(string json) 110 | => new JsonMessage(json); 111 | 112 | public static IAppMessage App(string content) 113 | => new AppMessage(content); 114 | 115 | public static IPokeMessage Poke(PokeType type) 116 | => new PokeMessage(type.ToString()); 117 | 118 | public static IDiceMessage Dice(int value) 119 | => new DiceMessage(value); 120 | 121 | public static IMusicShareMessage MusicShare(MusicShareKind kind, string title, string summary, string jumpUrl, string pictureUrl, string musicUrl, string brief) 122 | => new MusicShareMessage(kind.ToString(), title, summary, jumpUrl, pictureUrl, musicUrl, brief); 123 | 124 | #endregion 125 | 126 | IEnumerator IEnumerable.GetEnumerator() 127 | { 128 | return chain.GetEnumerator(); 129 | } 130 | 131 | IEnumerator IEnumerable.GetEnumerator() 132 | { 133 | return chain.GetEnumerator(); 134 | } 135 | } 136 | 137 | public enum PokeType 138 | { 139 | Poke, 140 | ShowLove, 141 | Like, 142 | Heartbroken, 143 | SixSixSix, 144 | FangDaZhao, 145 | } 146 | 147 | public enum MusicShareKind 148 | { 149 | NeteaseCloudMusic, 150 | QQMusic, 151 | MiguMusic, 152 | KugouMusic, 153 | KuwoMusic, 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /CocoaFramework/MessageInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | namespace Maila.Cocoa.Framework 5 | { 6 | public class MessageInfo 7 | { 8 | public MessageSource Source { get; private set; } 9 | public QMessage Message { get; private set; } 10 | 11 | public MessageInfo(MessageSource source, QMessage message) 12 | { 13 | Source = source; 14 | Message = message; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /CocoaFramework/MessageSource.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Diagnostics.CodeAnalysis; 7 | using System.Threading.Tasks; 8 | using Maila.Cocoa.Beans.API; 9 | using Maila.Cocoa.Beans.Models; 10 | using Maila.Cocoa.Beans.Models.Messages; 11 | using Maila.Cocoa.Framework.Models; 12 | using Maila.Cocoa.Framework.Support; 13 | 14 | namespace Maila.Cocoa.Framework 15 | { 16 | public class MessageSource 17 | { 18 | public QGroup? Group { get; } 19 | public QUser User { get; } 20 | 21 | public bool IsFriend { get; } 22 | public bool IsStranger { get; } 23 | 24 | [MemberNotNullWhen(true, new[] { nameof(Group), nameof(Permission), nameof(MemberCard) })] 25 | public bool IsGroup { get; } 26 | 27 | [MemberNotNullWhen(true, nameof(Group))] 28 | public bool IsTemp { get; } 29 | 30 | public GroupPermission? Permission { get; } 31 | public string? MemberCard { get; } 32 | 33 | public MessageSource(long qqId, bool isFriend = true) 34 | { 35 | if (isFriend && !BotInfo.HasFriend(qqId)) 36 | { 37 | _ = BotInfo.ReloadFriends(); 38 | } 39 | 40 | IsFriend = isFriend; 41 | IsStranger = !isFriend; 42 | IsGroup = false; 43 | IsTemp = false; 44 | Group = null; 45 | User = new(qqId); 46 | } 47 | 48 | public MessageSource(long groupId, long qqId, GroupPermission? permission, string? memberCard) 49 | { 50 | IsFriend = BotInfo.HasFriend(qqId); 51 | IsStranger = BotInfo.HasStranger(qqId); 52 | IsGroup = permission is not null && memberCard is not null; 53 | IsTemp = !IsGroup; 54 | 55 | Group = new(groupId); 56 | User = new(qqId); 57 | 58 | Permission = permission; 59 | MemberCard = memberCard; 60 | } 61 | 62 | public override bool Equals(object? obj) 63 | => obj is MessageSource src 64 | && src.IsGroup == IsGroup 65 | && src.IsTemp == IsTemp 66 | && src.Group?.Id == Group?.Id 67 | && src.User.Id == User.Id; 68 | public override int GetHashCode() 69 | => (IsGroup || IsTemp) 70 | ? Group.Id.GetHashCode() ^ User.Id.GetHashCode() 71 | : User.Id.GetHashCode(); 72 | 73 | public int Send(string message) 74 | => SendAsync(new PlainMessage(message)).Result; 75 | 76 | public int Send(MessageBuilder builder) 77 | => SendAsync(builder.ToMessageChain()).Result; 78 | 79 | public int Send(params IMessage[] chain) 80 | => SendAsync(chain).Result; 81 | 82 | public Task SendAsync(string message) 83 | => SendAsync(new PlainMessage(message)); 84 | 85 | public Task SendAsync(MessageBuilder builder) 86 | => SendAsync(builder.ToMessageChain()); 87 | 88 | public Task SendAsync(params IMessage[] chain) 89 | => IsGroup 90 | ? BotAPI.SendGroupMessage(Group.Id, chain) 91 | : BotAPI.SendPrivateMessage(User.Id, chain); 92 | 93 | 94 | [Obsolete("Use SendWithAt instead.")] 95 | public int SendEx(bool addAtWhenGroup, string? groupDelimiter, string message) 96 | => SendExAsync(addAtWhenGroup, groupDelimiter, message).Result; 97 | 98 | [Obsolete("Use SendWithAt instead.")] 99 | public int SendEx(bool addAtWhenGroup, string? groupDelimiter, params IMessage[] chain) 100 | => SendExAsync(addAtWhenGroup, groupDelimiter, chain).Result; 101 | 102 | [Obsolete("Use SendWithAtAsync instead.")] 103 | public Task SendExAsync(bool addAtWhenGroup, string? groupDelimiter, string message) 104 | { 105 | return SendExAsync(addAtWhenGroup, groupDelimiter, new PlainMessage(message)); 106 | } 107 | 108 | [Obsolete("Use SendWithAtAsync instead.")] 109 | public Task SendExAsync(bool addAtWhenGroup, string? groupDelimiter, params IMessage[] chain) 110 | { 111 | if (!IsGroup) 112 | { 113 | return BotAPI.SendPrivateMessage(User.Id, chain); 114 | } 115 | 116 | List newChain = new(chain.Length + 2); 117 | if (addAtWhenGroup) 118 | { 119 | newChain.Add(new AtMessage(User.Id)); 120 | } 121 | if (!string.IsNullOrEmpty(groupDelimiter)) 122 | { 123 | newChain.Add(new PlainMessage(groupDelimiter)); 124 | } 125 | 126 | newChain.AddRange(chain); 127 | return BotAPI.SendGroupMessage(Group.Id, newChain.ToArray()); 128 | } 129 | 130 | 131 | public int SendWithAt(string message) 132 | => SendWithAtAsync(new PlainMessage(message)).Result; 133 | 134 | public int SendWithAt(MessageBuilder builder) 135 | => SendWithAtAsync(builder.ToMessageChain()).Result; 136 | 137 | public int SendWithAt(params IMessage[] chain) 138 | => SendWithAtAsync(chain).Result; 139 | 140 | public Task SendWithAtAsync(string message) 141 | => SendWithAtAsync(new PlainMessage(message)); 142 | 143 | public Task SendWithAtAsync(MessageBuilder builder) 144 | => SendWithAtAsync(builder.ToMessageChain()); 145 | 146 | public Task SendWithAtAsync(params IMessage[] chain) 147 | { 148 | if (!IsGroup) 149 | { 150 | return BotAPI.SendPrivateMessage(User.Id, chain); 151 | } 152 | 153 | List newChain = new(chain.Length + 2) 154 | { 155 | new AtMessage(User.Id), 156 | new PlainMessage(" ") 157 | }; 158 | newChain.AddRange(chain); 159 | 160 | return BotAPI.SendGroupMessage(Group.Id, newChain.ToArray()); 161 | } 162 | 163 | 164 | [Obsolete("Use SendReply instead.")] 165 | public int SendReplyEx(QMessage quote, bool addAtWhenGroup, string message) 166 | => SendReplyExAsync(quote, addAtWhenGroup, message).Result; 167 | 168 | [Obsolete("Use SendReply instead.")] 169 | public int SendReplyEx(QMessage quote, bool addAtWhenGroup, params IMessage[] chain) 170 | => SendReplyExAsync(quote, addAtWhenGroup, chain).Result; 171 | 172 | [Obsolete("Use SendReplyAsync instead.")] 173 | public Task SendReplyExAsync(QMessage quote, bool addAtWhenGroup, string message) 174 | { 175 | if (string.IsNullOrEmpty(message)) 176 | { 177 | return SendReplyExAsync(quote, addAtWhenGroup); 178 | } 179 | return SendReplyExAsync(quote, addAtWhenGroup, new PlainMessage(message)); 180 | } 181 | 182 | [Obsolete("Use SendReplyAsync instead.")] 183 | public Task SendReplyExAsync(QMessage quote, bool addAtWhenGroup, params IMessage[] chain) 184 | { 185 | if (!IsGroup) 186 | { 187 | return BotAPI.SendPrivateMessage(quote.Id, User.Id, chain); 188 | } 189 | 190 | List newChain = new(chain.Length + 2); 191 | if (addAtWhenGroup) 192 | { 193 | newChain.Add(new AtMessage(User.Id)); 194 | newChain.Add(new PlainMessage(" ")); 195 | } 196 | 197 | newChain.AddRange(chain); 198 | return BotAPI.SendGroupMessage(quote.Id, Group.Id, newChain.ToArray()); 199 | } 200 | 201 | 202 | public int SendReply(QMessage quote, string message) 203 | => SendReplyAsync(quote, message).Result; 204 | 205 | public int SendReply(QMessage quote, MessageBuilder builder) 206 | => SendReplyAsync(quote, builder.ToMessageChain()).Result; 207 | 208 | public int SendReply(QMessage quote, params IMessage[] chain) 209 | => SendReplyAsync(quote, chain).Result; 210 | 211 | public Task SendReplyAsync(QMessage quote, string message) 212 | { 213 | if (string.IsNullOrEmpty(message)) 214 | { 215 | return SendReplyAsync(quote); 216 | } 217 | return SendReplyAsync(quote, new PlainMessage(message)); 218 | } 219 | 220 | public Task SendReplyAsync(QMessage quote, MessageBuilder builder) 221 | => SendReplyAsync(quote, builder.ToMessageChain()); 222 | 223 | public Task SendReplyAsync(QMessage quote, params IMessage[] chain) 224 | { 225 | if (IsGroup) 226 | { 227 | return BotAPI.SendGroupMessage(quote.Id, Group.Id, chain); 228 | } 229 | else 230 | { 231 | return BotAPI.SendPrivateMessage(quote.Id, User.Id, chain); 232 | } 233 | } 234 | 235 | 236 | public int SendPrivate(string message) 237 | => SendPrivateAsync(new PlainMessage(message)).Result; 238 | 239 | public int SendPrivate(MessageBuilder builder) 240 | => SendPrivateAsync(builder.ToMessageChain()).Result; 241 | 242 | public int SendPrivate(params IMessage[] chain) 243 | => SendPrivateAsync(chain).Result; 244 | 245 | public Task SendPrivateAsync(string message) 246 | => SendPrivateAsync(new PlainMessage(message)); 247 | 248 | public Task SendPrivateAsync(MessageBuilder builder) 249 | => SendPrivateAsync(builder.ToMessageChain()); 250 | 251 | public Task SendPrivateAsync(params IMessage[] chain) 252 | => BotAPI.SendPrivateMessage(User.Id, chain); 253 | 254 | 255 | public int? SendImage(string path) 256 | => SendImageAsync(path).Result; 257 | 258 | public async Task SendImageAsync(string path) 259 | { 260 | var image = await BotAPI.UploadImage( 261 | (IsGroup, IsFriend || IsStranger) switch 262 | { 263 | (true, _) => UploadType.Group, 264 | (_, true) => UploadType.Friend, 265 | _ => UploadType.Temp, 266 | }, path); 267 | 268 | return await SendAsync(image); 269 | } 270 | 271 | 272 | public int SendVoice(string path) 273 | => SendVoiceAsync(path).Result; 274 | 275 | public async Task SendVoiceAsync(string path) 276 | { 277 | var voice = await BotAPI.UploadVoice(path); 278 | return await SendAsync(voice); 279 | } 280 | 281 | public void Mute(int duration) 282 | => MuteAsync(duration); 283 | 284 | public Task MuteAsync(int duration) 285 | => IsGroup 286 | ? Group.MuteAsync(User.Id, duration) 287 | : Task.CompletedTask; 288 | 289 | public void Mute(TimeSpan duration) 290 | => MuteAsync(duration); 291 | 292 | public Task MuteAsync(TimeSpan duration) 293 | => IsGroup 294 | ? Group.MuteAsync(User.Id, duration) 295 | : Task.CompletedTask; 296 | 297 | public void Unmute() 298 | => UnmuteAsync(); 299 | 300 | public Task UnmuteAsync() 301 | => IsGroup 302 | ? BotAPI.Unmute(Group.Id, User.Id) 303 | : Task.CompletedTask; 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /CocoaFramework/Models/Meeting.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System; 5 | using System.Collections; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using Maila.Cocoa.Framework.Core; 9 | using Maila.Cocoa.Framework.Models.Processing; 10 | 11 | namespace Maila.Cocoa.Framework.Models 12 | { 13 | public class Meeting 14 | { 15 | private readonly IEnumerator proc; 16 | private ListeningTarget target; 17 | private MessageReceiver? receiver; 18 | private Meeting? child; 19 | private readonly Meeting root; 20 | 21 | private TimeSpan timeout = TimeSpan.Zero; 22 | private int counter; 23 | private bool running; 24 | private bool skip; 25 | private bool finished; 26 | 27 | private readonly object _lock = new(); 28 | 29 | private Meeting(ListeningTarget target, IEnumerator proc) 30 | { 31 | this.target = target; 32 | this.proc = proc; 33 | root = this; 34 | } 35 | 36 | private Meeting(ListeningTarget target, IEnumerator proc, Meeting root) 37 | { 38 | this.target = target; 39 | this.proc = proc; 40 | this.root = root; 41 | } 42 | 43 | private LockState Run(MessageSource? src, QMessage? msg) 44 | { 45 | if (finished) 46 | { 47 | return LockState.ContinueAndRemove; 48 | } 49 | 50 | if (skip) 51 | { 52 | return LockState.Continue; 53 | } 54 | 55 | if (src is not null && !target.Pred(src)) 56 | { 57 | return LockState.Continue; 58 | } 59 | 60 | lock (_lock) 61 | { 62 | return InternalRun(src, msg); 63 | } 64 | } 65 | 66 | private LockState InternalRun(MessageSource? src, QMessage? msg) 67 | { 68 | running = true; 69 | if (child is not null) 70 | { 71 | var state = child.InternalRun(src, msg); 72 | if (state == LockState.Finished) 73 | { 74 | child = null; 75 | } 76 | else 77 | { 78 | if (state == LockState.ContinueAndRemove) 79 | { 80 | finished = true; 81 | } 82 | running = false; 83 | return state; 84 | } 85 | } 86 | 87 | if (receiver is not null) 88 | { 89 | receiver.Source = src; 90 | receiver.Message = msg; 91 | receiver.IsTimeout = src is null && msg is null; 92 | } 93 | 94 | if (proc.MoveNext()) 95 | { 96 | switch (proc.Current) 97 | { 98 | case MessageReceiver rec: 99 | { 100 | receiver = rec; 101 | var state = InternalRun(src, msg); 102 | return state; 103 | } 104 | case ListeningTarget tgt: 105 | { 106 | target = tgt; 107 | break; 108 | } 109 | case MeetingTimeout tout: 110 | { 111 | timeout = tout.Duration; 112 | var state = InternalRun(src, msg); 113 | return state; 114 | } 115 | case AsyncTask task: 116 | { 117 | skip = true; 118 | Task.Run(async () => 119 | { 120 | await task.RealTask; 121 | InternalRun(src, msg); 122 | skip = false; 123 | }); 124 | return LockState.NotFinished; 125 | } 126 | case NotFit nf: 127 | { 128 | running = false; 129 | if (!nf.Remove) 130 | { 131 | return LockState.Continue; 132 | } 133 | counter++; 134 | finished = true; 135 | return LockState.ContinueAndRemove; 136 | } 137 | case string or StringBuilder: 138 | { 139 | var retMsg = proc.Current as string ?? (proc.Current as StringBuilder)!.ToString(); 140 | src?.SendAsync(retMsg); 141 | break; 142 | } 143 | case IEnumerator or IEnumerable: 144 | { 145 | var subm = proc.Current as IEnumerator ?? (proc.Current as IEnumerable)!.GetEnumerator(); 146 | Meeting m = new(target, subm, root); 147 | var state = m.InternalRun(src, msg); 148 | if (state != LockState.Finished) 149 | { 150 | counter++; 151 | child = m; 152 | running = false; 153 | return state; 154 | } 155 | 156 | state = InternalRun(src, msg); 157 | return state; 158 | } 159 | } 160 | 161 | counter++; 162 | if (timeout > TimeSpan.Zero) 163 | { 164 | int count = counter; 165 | Task.Run(async () => 166 | { 167 | await Task.Delay(timeout); 168 | if (counter == count && !running) 169 | { 170 | root.Run(null, null); 171 | } 172 | }); 173 | } 174 | 175 | running = false; 176 | return LockState.NotFinished; 177 | } 178 | 179 | counter++; 180 | running = false; 181 | finished = true; 182 | return LockState.Finished; 183 | } 184 | 185 | public static void Start(MessageSource src, IEnumerable proc) 186 | { 187 | Start(src, proc.GetEnumerator()); 188 | } 189 | 190 | public static void Start(MessageSource src, IEnumerator proc) 191 | { 192 | Meeting m = new(ListeningTarget.FromTarget(src), proc); 193 | if (m.InternalRun(src, null) != LockState.Finished) 194 | { 195 | ModuleCore.AddLock(m.Run); 196 | } 197 | } 198 | 199 | public static void Start(ListeningTarget target, IEnumerable proc) 200 | { 201 | Start(target, proc.GetEnumerator()); 202 | } 203 | 204 | public static void Start(ListeningTarget target, IEnumerator proc) 205 | { 206 | Meeting m = new(target, proc); 207 | if (m.InternalRun(null, null) != LockState.Finished) 208 | { 209 | ModuleCore.AddLock(m.Run); 210 | } 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /CocoaFramework/Models/Processing/AsyncTask.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace Maila.Cocoa.Framework.Models.Processing 9 | { 10 | public class AsyncTask 11 | { 12 | internal Task RealTask { get; } 13 | 14 | private AsyncTask(Task realTask) 15 | { 16 | RealTask = realTask; 17 | } 18 | 19 | private static readonly AsyncTask completed = new(Task.CompletedTask); 20 | 21 | public static AsyncTask Wait(int milliseconds) 22 | { 23 | if (milliseconds <= 0) 24 | { 25 | return completed; 26 | } 27 | return new(Task.Delay(milliseconds)); 28 | } 29 | public static AsyncTask Wait(int milliseconds, CancellationToken cancellationToken) 30 | { 31 | if (milliseconds <= 0) 32 | { 33 | return completed; 34 | } 35 | return new(Task.Delay(milliseconds, cancellationToken)); 36 | } 37 | public static AsyncTask Wait(TimeSpan delay) 38 | { 39 | if (delay <= TimeSpan.Zero) 40 | { 41 | return completed; 42 | } 43 | return new(Task.Delay(delay)); 44 | } 45 | public static AsyncTask Wait(TimeSpan delay, CancellationToken cancellationToken) 46 | { 47 | if (delay <= TimeSpan.Zero) 48 | { 49 | return completed; 50 | } 51 | return new(Task.Delay(delay, cancellationToken)); 52 | } 53 | public static AsyncTask WaitUntil(DateTime time) 54 | { 55 | if (time <= DateTime.Now) 56 | { 57 | return completed; 58 | } 59 | return new(Task.Delay(time - DateTime.Now)); 60 | } 61 | public static AsyncTask WaitUntil(DateTime time, CancellationToken cancellationToken) 62 | { 63 | if (time <= DateTime.Now) 64 | { 65 | return completed; 66 | } 67 | return new(Task.Delay(time - DateTime.Now, cancellationToken)); 68 | } 69 | 70 | public static AsyncTask FromTask(Task task) 71 | { 72 | return new(task); 73 | } 74 | public static AsyncTask FromTask(Task task, out GetValue result) 75 | { 76 | GetValue getValue = new(); 77 | result = getValue; 78 | return new(Task.Run(async () => getValue.Value = await task)); 79 | } 80 | 81 | public static AsyncTask Run(Action action) 82 | { 83 | return new(Task.Run(action)); 84 | } 85 | public static AsyncTask Run(Action action, CancellationToken cancellationToken) 86 | { 87 | return new(Task.Run(action, cancellationToken)); 88 | } 89 | public static AsyncTask Run(Func function) 90 | { 91 | return new(Task.Run(function)); 92 | } 93 | public static AsyncTask Run(Func function, CancellationToken cancellationToken) 94 | { 95 | return new(Task.Run(function, cancellationToken)); 96 | } 97 | public static AsyncTask Run(Func function, out GetValue result) 98 | { 99 | GetValue getValue = new(); 100 | result = getValue; 101 | return new(Task.Run(() => getValue.Value = function())); 102 | } 103 | public static AsyncTask Run(Func> function, out GetValue result) 104 | { 105 | GetValue getValue = new(); 106 | result = getValue; 107 | return new(Task.Run(async () => getValue.Value = await function())); 108 | } 109 | public static AsyncTask Run(Func> function, out GetValue result, CancellationToken cancellationToken) 110 | { 111 | GetValue getValue = new(); 112 | result = getValue; 113 | return new(Task.Run(async () => getValue.Value = await function(), cancellationToken)); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /CocoaFramework/Models/Processing/GetValue.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | namespace Maila.Cocoa.Framework.Models.Processing 5 | { 6 | public class GetValue 7 | { 8 | public T? Value { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /CocoaFramework/Models/Processing/ListeningTarget.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System; 5 | 6 | namespace Maila.Cocoa.Framework.Models.Processing 7 | { 8 | public class ListeningTarget 9 | { 10 | internal Predicate Pred; 11 | 12 | private ListeningTarget(long? groupId, long? userId) 13 | { 14 | Pred = src => 15 | { 16 | if (src is null) 17 | { 18 | return true; 19 | } 20 | if (groupId is null && userId is null) 21 | { 22 | return true; 23 | } 24 | 25 | bool uFit = userId is null || userId == src.User.Id; 26 | bool gFit = groupId is null ? !src.IsGroup : groupId == src.Group?.Id; 27 | return gFit && uFit; 28 | }; 29 | } 30 | private ListeningTarget(Predicate pred) 31 | { 32 | Pred = pred; 33 | } 34 | 35 | public static ListeningTarget All { get; } = new(null, null); 36 | 37 | public static ListeningTarget FromGroup(long groupId) => new(groupId, null); 38 | public static ListeningTarget FromGroup(QGroup group) => new(group.Id, null); 39 | 40 | public static ListeningTarget FromUser(long userId) => new(null, userId); 41 | public static ListeningTarget FromUser(QUser user) => new(null, user.Id); 42 | 43 | public static ListeningTarget FromTarget(long groupId, long userId) => new(groupId, userId); 44 | public static ListeningTarget FromTarget(MessageSource src) => new(src.Group?.Id, src.User.Id); 45 | public static ListeningTarget CustomTarget(Predicate pred) => new(pred); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /CocoaFramework/Models/Processing/MeetingTimeout.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System; 5 | 6 | namespace Maila.Cocoa.Framework.Models.Processing 7 | { 8 | public class MeetingTimeout 9 | { 10 | internal TimeSpan Duration { get; } 11 | 12 | private MeetingTimeout(TimeSpan duration) 13 | { 14 | Duration = duration; 15 | } 16 | 17 | public static MeetingTimeout Off { get; } = new(TimeSpan.Zero); 18 | 19 | public static MeetingTimeout FromTimeSpan(TimeSpan time) 20 | { 21 | if (time <= TimeSpan.Zero) 22 | { 23 | return Off; 24 | } 25 | return new(time); 26 | } 27 | public static MeetingTimeout FromMinutes(double minutes) 28 | { 29 | if (minutes <= 0) 30 | { 31 | return Off; 32 | } 33 | return new(TimeSpan.FromMinutes(minutes)); 34 | } 35 | public static MeetingTimeout FromSeconds(double seconds) 36 | { 37 | if (seconds <= 0) 38 | { 39 | return Off; 40 | } 41 | return new(TimeSpan.FromSeconds(seconds)); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /CocoaFramework/Models/Processing/MessageReceiver.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System.Diagnostics.CodeAnalysis; 5 | 6 | namespace Maila.Cocoa.Framework.Models.Processing 7 | { 8 | public class MessageReceiver 9 | { 10 | public MessageSource? Source { get; internal set; } 11 | public QMessage? Message { get; internal set; } 12 | 13 | [MemberNotNullWhen(false, new[] { nameof(Source), nameof(Message) })] 14 | public bool IsTimeout { get; internal set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /CocoaFramework/Models/Processing/NotFit.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | namespace Maila.Cocoa.Framework.Models.Processing 5 | { 6 | public class NotFit 7 | { 8 | private NotFit() { } 9 | internal bool Remove { get; init; } 10 | 11 | public static NotFit Continue { get; } = new() { Remove = false }; 12 | public static NotFit Stop { get; } = new() { Remove = true }; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /CocoaFramework/Models/QGroup.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System; 5 | using System.Threading.Tasks; 6 | using Maila.Cocoa.Beans.API; 7 | using Maila.Cocoa.Beans.Models; 8 | using Maila.Cocoa.Beans.Models.Messages; 9 | using Maila.Cocoa.Framework.Support; 10 | 11 | namespace Maila.Cocoa.Framework.Models 12 | { 13 | public class QGroup 14 | { 15 | public long Id { get; } 16 | 17 | private GroupConfig? config; 18 | public string? Name { get => config?.Name; set => SetGroupConfig(new() { Name = value }); } 19 | public string? Announcement { get => config?.Announcement; set => SetGroupConfig(new() { Announcement = value }); } 20 | public bool? ConfessTalk { get => config?.ConfessTalk; set => SetGroupConfig(new() { ConfessTalk = value }); } 21 | public bool? AllowMemberInvite { get => config?.AllowMemberInvite; set => SetGroupConfig(new() { AllowMemberInvite = value }); } 22 | public bool? AutoApprove { get => config?.AutoApprove; set => SetGroupConfig(new() { AutoApprove = value }); } 23 | public bool? AnonymousChat { get => config?.AnonymousChat; set => SetGroupConfig(new() { AnonymousChat = value }); } 24 | 25 | private async void SetGroupConfig(GroupConfig config) 26 | { 27 | try 28 | { 29 | await BotAPI.SetGroupConfig(Id, config); 30 | config = await BotAPI.GetGroupConfig(Id); 31 | } 32 | catch (Exception e) { Console.WriteLine(e); } 33 | } 34 | 35 | public QGroup(long id) 36 | { 37 | Id = id; 38 | Task.Run(async () => 39 | { 40 | try 41 | { 42 | config = await BotAPI.GetGroupConfig(id); 43 | } 44 | catch (Exception) { } 45 | }); 46 | } 47 | 48 | public override bool Equals(object? obj) 49 | => obj is QGroup group && group.Id == Id; 50 | public override int GetHashCode() 51 | => Id.GetHashCode(); 52 | 53 | public int SendMessage(string message) 54 | => SendMessageAsync(message).Result; 55 | 56 | public int SendMessage(params IMessage[] chain) 57 | => SendMessageAsync(chain).Result; 58 | 59 | public Task SendMessageAsync(string message) 60 | => BotAPI.SendGroupMessage(Id, new PlainMessage(message)); 61 | 62 | public Task SendMessageAsync(params IMessage[] chain) 63 | => BotAPI.SendGroupMessage(Id, chain); 64 | 65 | public int SendImage(string path) 66 | => SendImageAsync(path).Result; 67 | 68 | public async Task SendImageAsync(string path) 69 | { 70 | var image = await BotAPI.UploadImage(UploadType.Group, path); 71 | return await BotAPI.SendGroupMessage(Id, image); 72 | } 73 | 74 | public int SendVoice(string path) 75 | => SendVoiceAsync(path).Result; 76 | 77 | public async Task SendVoiceAsync(string path) 78 | { 79 | var voice = await BotAPI.UploadVoice(path); 80 | return await BotAPI.SendGroupMessage(Id, voice); 81 | } 82 | 83 | public QGroupInfo? GetGroupInfo() 84 | => BotInfo.GetGroupInfo(Id); 85 | 86 | public QMemberInfo[]? GetMemberList() 87 | => BotInfo.GetMemberList(Id); 88 | 89 | public QMemberInfo? GetMemberInfo(long qqId) 90 | => BotInfo.GetMemberInfo(Id, qqId); 91 | 92 | public void Mute(long qqId, int duration) 93 | => MuteAsync(qqId, duration); 94 | 95 | public Task MuteAsync(long qqId, int duration) 96 | => BotAPI.Mute(Id, qqId, Math.Clamp(duration, 0, 2591999)); 97 | 98 | public void Mute(long qqId, TimeSpan duration) 99 | => MuteAsync(qqId, duration); 100 | 101 | public Task MuteAsync(long qqId, TimeSpan duration) 102 | => BotAPI.Mute(Id, qqId, Math.Clamp((int)duration.TotalSeconds, 0, 2591999)); 103 | 104 | public void Unmute(long qqId) 105 | => UnmuteAsync(qqId); 106 | 107 | public Task UnmuteAsync(long qqId) 108 | => BotAPI.Unmute(Id, qqId); 109 | 110 | public void MuteAll() 111 | => MuteAllAsync(); 112 | 113 | public Task MuteAllAsync() 114 | => BotAPI.MuteAll(Id); 115 | 116 | public void UnmuteAll() 117 | => UnmuteAllAsync(); 118 | 119 | public Task UnmuteAllAsync() 120 | => BotAPI.UnmuteAll(Id); 121 | 122 | public void Kick(long qqId) 123 | => KickAsync(qqId); 124 | 125 | public Task KickAsync(long qqId) 126 | => BotAPI.Kick(Id, qqId); 127 | 128 | public void Leave() 129 | => LeaveAsync(); 130 | 131 | public Task LeaveAsync() 132 | => BotAPI.Quit(Id); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /CocoaFramework/Models/QUser.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System.Threading.Tasks; 5 | using Maila.Cocoa.Beans.API; 6 | using Maila.Cocoa.Beans.Models.Messages; 7 | using Maila.Cocoa.Framework.Support; 8 | 9 | namespace Maila.Cocoa.Framework.Models 10 | { 11 | public class QUser 12 | { 13 | public long Id { get; } 14 | 15 | public UserIdentity Identity => BotAuth.GetIdentity(Id); 16 | public bool IsFriend => BotInfo.HasFriend(Id); 17 | public bool IsStranger => BotInfo.HasStranger(Id); 18 | 19 | public bool IsOwner => Identity.Fit(UserIdentity.Owner); 20 | public bool IsAdmin => Identity.Fit(UserIdentity.Admin); 21 | public bool IsDeveloper => Identity.Fit(UserIdentity.Developer); 22 | public bool IsDebugger => Identity.Fit(UserIdentity.Debugger); 23 | public bool IsOperator => Identity.Fit(UserIdentity.Operator); 24 | public bool IsStaff => Identity.Fit(UserIdentity.Staff); 25 | 26 | public QUser(long id) 27 | { 28 | Id = id; 29 | } 30 | public override bool Equals(object? obj) 31 | => obj is QUser user && user.Id == Id; 32 | public override int GetHashCode() 33 | => Id.GetHashCode(); 34 | 35 | public int SendMessage(string message) 36 | => SendMessageAsync(message).Result; 37 | 38 | public int SendMessage(params IMessage[] chain) 39 | => SendMessageAsync(chain).Result; 40 | 41 | public Task SendMessageAsync(string message) 42 | => BotAPI.SendPrivateMessage(Id, new PlainMessage(message)); 43 | 44 | public Task SendMessageAsync(params IMessage[] chain) 45 | => BotAPI.SendPrivateMessage(Id, chain); 46 | 47 | public int SendImage(string path) 48 | => SendImageAsync(path).Result; 49 | 50 | public async Task SendImageAsync(string path) 51 | { 52 | var image = await BotAPI.UploadImage(IsFriend ? UploadType.Friend : UploadType.Temp, path); 53 | return await BotAPI.SendPrivateMessage(Id, image); 54 | } 55 | 56 | public int SendVoice(string path) 57 | => SendVoiceAsync(path).Result; 58 | 59 | public async Task SendVoiceAsync(string path) 60 | { 61 | var voice = await BotAPI.UploadVoice(path); 62 | return await BotAPI.SendPrivateMessage(Id, voice); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /CocoaFramework/Models/Route/BuiltIn/RegexRoute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Reflection; 8 | using System.Text.RegularExpressions; 9 | using Maila.Cocoa.Beans.Models.Messages; 10 | using Maila.Cocoa.Framework.Support; 11 | 12 | namespace Maila.Cocoa.Framework.Models.Route.BuiltIn 13 | { 14 | internal class RegexRoute : RouteInfo 15 | { 16 | private readonly Regex regex; 17 | private readonly bool atRequired; 18 | private readonly List groupIndexes = new(); 19 | 20 | public RegexRoute(BotModuleBase module, MethodInfo route, Regex regex, bool atRequired) : base(module, route) 21 | { 22 | this.regex = regex; 23 | this.atRequired = atRequired; 24 | 25 | var groupNames = regex.GetGroupNames(); 26 | for (int i = 0; i < parameters.Length; i++) 27 | { 28 | var parameter = parameters[i]; 29 | if (parameter.HasAttribute()) 30 | { 31 | continue; 32 | } 33 | 34 | var name = parameter.Name!; 35 | if (parameter.TryGetAttribute(out var groupNameInfo)) 36 | { 37 | name = groupNameInfo.Name; 38 | } 39 | 40 | var parameterType = GetParameterType(parameters[i].ParameterType); 41 | if (parameterType != ParameterType.Unknown && groupNames.Contains(name)) 42 | { 43 | groupIndexes.Add(new() 44 | { 45 | groupNumber = regex.GroupNumberFromName(name), 46 | parameterIndex = i, 47 | parameterType = parameterType, 48 | }); 49 | } 50 | } 51 | } 52 | 53 | protected override bool IsMatch(MessageSource src, QMessage msg) 54 | { 55 | var msgText = msg.PlainText; 56 | if (atRequired) 57 | { 58 | if (!msg.GetSubMessages().Any(at => at.Target == BotAPI.BotQQ)) 59 | { 60 | return false; 61 | } 62 | 63 | msgText = msgText.StartsWith(' ') ? msgText[1..] : msgText; 64 | } 65 | 66 | return regex.IsMatch(msgText); 67 | } 68 | 69 | protected override void FillArguments(MessageSource src, QMessage msg, object?[] args) 70 | { 71 | var msgText = msg.PlainText; 72 | if (atRequired) 73 | { 74 | msgText = msgText.StartsWith(' ') ? msgText[1..] : msgText; 75 | } 76 | 77 | var match = regex.Match(msgText); 78 | foreach (var index in groupIndexes) 79 | { 80 | index.Fill(args, match); 81 | } 82 | } 83 | 84 | private static ParameterType GetParameterType(Type type) 85 | { 86 | if (type == typeof(string)) 87 | { 88 | return ParameterType.String; 89 | } 90 | 91 | if (type == typeof(string[])) 92 | { 93 | return ParameterType.StringArray; 94 | } 95 | 96 | if (type == typeof(List)) 97 | { 98 | return ParameterType.StringList; 99 | } 100 | 101 | return ParameterType.Unknown; 102 | } 103 | 104 | private enum ParameterType 105 | { 106 | String, 107 | StringArray, 108 | StringList, 109 | Unknown 110 | } 111 | 112 | private class RegexGroupIndex 113 | { 114 | public int groupNumber; 115 | public int parameterIndex; 116 | public ParameterType parameterType; 117 | 118 | public void Fill(object?[] args, Match match) 119 | { 120 | var group = match.Groups[groupNumber]; 121 | args[parameterIndex] = group.Success 122 | ? parameterType switch 123 | { 124 | ParameterType.String => group.Value, 125 | ParameterType.StringArray => group.Captures.Select(c => c.Value).ToArray(), 126 | ParameterType.StringList => group.Captures.Select(c => c.Value).ToList(), 127 | _ => null 128 | } 129 | : parameterType switch 130 | { 131 | ParameterType.String => null, 132 | ParameterType.StringArray => Array.Empty(), 133 | ParameterType.StringList => new List(), 134 | _ => null 135 | }; 136 | } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /CocoaFramework/Models/Route/BuiltIn/TextRoute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System; 5 | using System.Linq; 6 | using System.Reflection; 7 | using Maila.Cocoa.Beans.Models.Messages; 8 | using Maila.Cocoa.Framework.Support; 9 | 10 | namespace Maila.Cocoa.Framework.Models.Route.BuiltIn 11 | { 12 | internal class TextRoute : RouteInfo 13 | { 14 | private readonly string text; 15 | private readonly bool ignoreCase; 16 | private readonly bool atRequired; 17 | 18 | public TextRoute(BotModuleBase module, MethodInfo route, string text, bool ignoreCase, bool atRequired) : base(module, route) 19 | { 20 | this.text = text; 21 | this.ignoreCase = ignoreCase; 22 | this.atRequired = atRequired; 23 | } 24 | 25 | protected override bool IsMatch(MessageSource src, QMessage msg) 26 | { 27 | var msgText = msg.PlainText; 28 | if (atRequired) 29 | { 30 | if (!msg.GetSubMessages().Any(at => at.Target == BotAPI.BotQQ)) 31 | { 32 | return false; 33 | } 34 | 35 | msgText = msgText.StartsWith(' ') ? msgText[1..] : msgText; 36 | } 37 | 38 | return ignoreCase 39 | ? string.Equals(msgText, text, StringComparison.CurrentCultureIgnoreCase) 40 | : msgText == text; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /CocoaFramework/Models/Route/RouteInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System.Reflection; 5 | using System.Threading; 6 | 7 | namespace Maila.Cocoa.Framework.Models.Route 8 | { 9 | public abstract class RouteInfo 10 | { 11 | protected readonly BotModuleBase module; 12 | protected readonly MethodInfo route; 13 | protected readonly ParameterInfo[] parameters; 14 | 15 | internal SemaphoreSlim? processLock; 16 | 17 | private readonly RouteResultProcessor.Processor resultProcessor; 18 | 19 | private readonly int srcIndex = -1; 20 | private readonly int msgIndex = -1; 21 | private readonly int meetingIndex = -1; 22 | 23 | private readonly bool disabledInGroup; 24 | private readonly bool disabledInPrivate; 25 | 26 | protected RouteInfo(BotModuleBase module, MethodInfo route) 27 | { 28 | this.module = module; 29 | this.route = route; 30 | 31 | resultProcessor = RouteResultProcessor.GetProcessor(route.ReturnType); 32 | 33 | parameters = route.GetParameters(); 34 | for (int i = 0; i < parameters.Length; i++) 35 | { 36 | var parameter = parameters[i]; 37 | if (parameter.HasAttribute()) 38 | { 39 | continue; 40 | } 41 | 42 | var parameterType = parameter.ParameterType; 43 | if (srcIndex == -1 && parameterType == typeof(MessageSource)) 44 | { 45 | srcIndex = i; 46 | } 47 | if (msgIndex == -1 && parameterType == typeof(QMessage)) 48 | { 49 | msgIndex = i; 50 | } 51 | if (meetingIndex == -1 && parameterType == typeof(AsyncMeeting)) 52 | { 53 | meetingIndex = i; 54 | } 55 | 56 | if (srcIndex != -1 && msgIndex != -1 && meetingIndex != -1) 57 | { 58 | break; 59 | } 60 | } 61 | 62 | disabledInGroup = route.HasAttribute(); 63 | disabledInPrivate = route.HasAttribute(); 64 | } 65 | 66 | internal bool Run(MessageSource src, QMessage msg) 67 | { 68 | if (src.IsGroup ? disabledInGroup : disabledInPrivate) 69 | { 70 | return false; 71 | } 72 | 73 | if (!IsMatch(src, msg)) 74 | { 75 | return false; 76 | } 77 | 78 | var args = new object?[parameters.Length]; 79 | 80 | if (srcIndex > -1) 81 | { 82 | args[srcIndex] = src; 83 | } 84 | if (msgIndex > -1) 85 | { 86 | args[msgIndex] = msg; 87 | } 88 | if (meetingIndex > -1) 89 | { 90 | args[meetingIndex] = new AsyncMeeting(src); 91 | } 92 | 93 | FillArguments(src, msg, args); 94 | 95 | try 96 | { 97 | processLock?.Wait(); 98 | 99 | var result = route.Invoke(module, args); 100 | return resultProcessor(src, msg, result); 101 | } 102 | finally 103 | { 104 | processLock?.Release(); 105 | } 106 | } 107 | 108 | protected abstract bool IsMatch(MessageSource src, QMessage msg); 109 | protected virtual void FillArguments(MessageSource src, QMessage msg, object?[] args) { } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /CocoaFramework/Models/Route/RouteResultProcessor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace Maila.Cocoa.Framework.Models.Route 11 | { 12 | public static class RouteResultProcessor 13 | { 14 | internal delegate bool Processor(MessageSource src, QMessage msg, object? returnValue); 15 | public delegate bool Processor(MessageSource src, QMessage msg, T returnValue); 16 | 17 | private static readonly Dictionary processors = new(); 18 | 19 | static RouteResultProcessor() 20 | { 21 | processors[typeof(void)] = static (_, _, _) => true; 22 | processors[typeof(Task)] = static (_, _, _) => true; 23 | 24 | RegistProcessor(static (_, _, result) => result); 25 | RegistProcessor(static (src, _, result) => 26 | { 27 | if (string.IsNullOrEmpty(result)) 28 | { 29 | return false; 30 | } 31 | else 32 | { 33 | src.Send(result); 34 | return true; 35 | } 36 | }); 37 | RegistProcessor(static (src, _, result) => 38 | { 39 | Meeting.Start(src, result); 40 | return true; 41 | }); 42 | RegistProcessor(static (src, _, result) => 43 | { 44 | Meeting.Start(src, result); 45 | return true; 46 | }); 47 | RegistProcessor(static (src, _, result) => 48 | { 49 | if (result.Length > 0) 50 | { 51 | src.SendAsync(result.ToString()); 52 | return true; 53 | } 54 | else 55 | { 56 | return false; 57 | } 58 | }); 59 | RegistProcessor(static (src, _, result) => 60 | { 61 | src.Send(result); 62 | return true; 63 | }); 64 | } 65 | 66 | private static Processor Wrap(Processor func) 67 | => (src, msg, returnVal) => returnVal is T result && func(src, msg, result); 68 | 69 | public static void RegistProcessor(Processor func) 70 | { 71 | processors[typeof(T)] = Wrap(func); 72 | processors[typeof(Task)] = Wrap>((src, msg, task) => 73 | { 74 | Task.Run(async () => func(src, msg, await task)); 75 | return true; 76 | }); 77 | } 78 | 79 | internal static Processor GetProcessor(Type returnType) 80 | { 81 | if (processors.TryGetValue(returnType, out var processor)) 82 | { 83 | return processor; 84 | } 85 | 86 | if (returnType.IsValueType) 87 | { 88 | return (_, _, returnValue) => 89 | { 90 | try 91 | { 92 | var defaultValue = Activator.CreateInstance(returnType); 93 | var isDefaultValue = returnValue?.Equals(defaultValue) ?? false; 94 | return !isDefaultValue; 95 | } 96 | catch 97 | { 98 | return false; 99 | } 100 | }; 101 | } 102 | else 103 | { 104 | return static (_, _, returnValue) => returnValue != null; 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /CocoaFramework/QMessage.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System; 5 | using System.Collections.Immutable; 6 | using System.Diagnostics.CodeAnalysis; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | using Maila.Cocoa.Beans.Models.Messages; 10 | using Maila.Cocoa.Framework.Support; 11 | 12 | namespace Maila.Cocoa.Framework 13 | { 14 | public class QMessage 15 | { 16 | public ImmutableArray Chain { get; } 17 | public int Id { get; } 18 | public DateTime Time { get; } 19 | public string PlainText { get; } 20 | 21 | public QMessage(IMessage[] chain) 22 | { 23 | if (chain is null || chain.Length < 2 || chain[0] is not SourceMessage sm) 24 | { 25 | throw new ArgumentException("Invalid message chain."); 26 | } 27 | 28 | Id = sm.Id; 29 | Time = DateTimeOffset.FromUnixTimeSeconds(sm.Time).LocalDateTime; 30 | Chain = ImmutableArray.Create(chain, 1, chain.Length - 1); 31 | PlainText = string.Concat(chain.Select(m => (m as PlainMessage)?.Text)); 32 | } 33 | 34 | public T[] GetSubMessages() where T : IMessage 35 | => Chain.OfType() 36 | .ToArray(); 37 | 38 | public override string ToString() 39 | => PlainText; 40 | 41 | [return: NotNullIfNotNull("msg")] 42 | public static implicit operator string?(QMessage? msg) 43 | => msg?.PlainText; 44 | 45 | public void Recall() 46 | => RecallAsync(); 47 | 48 | public Task RecallAsync() 49 | => BotAPI.Recall(Id); 50 | 51 | public void SetEssence() 52 | => SetEssenceAsync(); 53 | 54 | public Task SetEssenceAsync() 55 | => BotAPI.SetEssence(Id); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /CocoaFramework/Support/BotAuth.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Reflection; 7 | 8 | namespace Maila.Cocoa.Framework.Support 9 | { 10 | public static class BotAuth 11 | { 12 | private static Dictionary identities = new(); 13 | 14 | internal static void Init() 15 | { 16 | DataHosting.AddOptimizeEnabledHosting( 17 | typeof(BotAuth).GetField(nameof(identities), BindingFlags.Static | BindingFlags.NonPublic)!, 18 | null, 19 | $"BotAuth/{BotAPI.BotQQ}"); 20 | } 21 | 22 | internal static void Reset() 23 | { 24 | identities = new(); 25 | } 26 | 27 | /// 28 | /// Get the user's identity. 29 | /// 30 | public static UserIdentity GetIdentity(long qqId) 31 | => identities.GetValueOrDefault(qqId, UserIdentity.User); 32 | 33 | /// 34 | /// Get all user's identities. 35 | /// 36 | public static KeyValuePair[] GetStoredIdentity() 37 | => identities.ToArray(); 38 | 39 | /// 40 | /// Get all filtered user's identities. 41 | /// 42 | public static KeyValuePair[] GetStoredIdentity(UserIdentity filter) 43 | => identities.Where(p => p.Value.Fit(filter)).ToArray(); 44 | 45 | /// 46 | /// Set the user's identity. 47 | /// 48 | public static void SetIdentity(long qqId, UserIdentity identity) 49 | => identities[qqId] = identity; 50 | 51 | /// 52 | /// Append specified identity to the user. 53 | /// 54 | public static UserIdentity AddIdentity(long qqId, UserIdentity identity) 55 | => identities[qqId] = identities.GetValueOrDefault(qqId, UserIdentity.User) | identity; 56 | 57 | /// 58 | /// Remove specified identity. 59 | /// 60 | public static UserIdentity RemoveIdentity(long qqId, UserIdentity identity) 61 | => identities[qqId] = identities.GetValueOrDefault(qqId, UserIdentity.User) & ~identity; 62 | 63 | /// 64 | /// Set the user's identity to . 65 | /// 66 | public static bool ClearIdentity(long qqId) 67 | { 68 | if (identities.ContainsKey(qqId)) 69 | { 70 | identities.Remove(qqId); 71 | return true; 72 | } 73 | return false; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /CocoaFramework/Support/BotInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | using Maila.Cocoa.Beans.Models; 9 | 10 | namespace Maila.Cocoa.Framework.Support 11 | { 12 | public static class BotInfo 13 | { 14 | private static Dictionary? groups; 15 | private static Dictionary>? members; 16 | private static Dictionary? friends; 17 | private static readonly Dictionary strangers = new(); 18 | 19 | private static readonly TimeSpan CredibleTime = TimeSpan.FromMinutes(5); 20 | private static DateTime friendsLastSync = DateTime.MinValue; 21 | private static DateTime groupsLastSync = DateTime.MinValue; 22 | 23 | internal static void Reset() 24 | { 25 | groups = null; 26 | members = null; 27 | friends = null; 28 | strangers.Clear(); 29 | } 30 | 31 | internal static void UpdateFriend(QFriendInfo friend) 32 | { 33 | if (friends is null) 34 | { 35 | _ = ReloadFriends(); 36 | return; 37 | } 38 | 39 | friends[friend.Id] = friend; 40 | } 41 | 42 | internal static void UpdateMember(QMemberInfo member) 43 | { 44 | if (groups is null || members is null) 45 | { 46 | _ = ReloadAllGroupMembers(); 47 | return; 48 | } 49 | 50 | if (!members.TryGetValue(member.Group.Id, out var groupMembers)) 51 | { 52 | _ = ReloadGroupMembers(member.Group.Id); 53 | return; 54 | } 55 | 56 | groupMembers[member.Id] = member; 57 | } 58 | 59 | public static async Task ReloadAll() 60 | { 61 | Task f = ReloadFriends(); 62 | Task g = ReloadAllGroupMembers(); 63 | 64 | await f; 65 | await g; 66 | } 67 | 68 | public static async Task ReloadAllGroupMembers() 69 | { 70 | Dictionary groups = new(); 71 | Dictionary> members = new(); 72 | foreach (var info in await BotAPI.GetGroupList()) 73 | { 74 | groups[info.Id] = info; 75 | members[info.Id] = (await BotAPI.GetMemberList(info.Id)).ToDictionary(m => m.Id); 76 | } 77 | 78 | BotInfo.groups = groups; 79 | BotInfo.members = members; 80 | groupsLastSync = DateTime.Now; 81 | } 82 | 83 | public static async Task ReloadGroupMembers(long groupId) 84 | { 85 | if (groups is null || members is null) 86 | { 87 | await ReloadAll(); 88 | return true; 89 | } 90 | 91 | if (members.ContainsKey(groupId)) 92 | { 93 | members[groupId] = (await BotAPI.GetMemberList(groupId)).ToDictionary(m => m.Id); 94 | return true; 95 | } 96 | 97 | if ((await BotAPI.GetGroupList()).FirstOrDefault(i => i.Id == groupId) is not { } gInfo) 98 | { 99 | return false; 100 | } 101 | 102 | groups[groupId] = gInfo; 103 | members[groupId] = (await BotAPI.GetMemberList(groupId)).ToDictionary(m => m.Id); 104 | return true; 105 | } 106 | 107 | public static async Task ReloadFriends() 108 | { 109 | friends = (await BotAPI.GetFriendList()).ToDictionary(f => f.Id); 110 | friendsLastSync = DateTime.Now; 111 | } 112 | 113 | public static bool HasGroup(long groupId) 114 | { 115 | if (DateTime.Now - groupsLastSync > CredibleTime) 116 | { 117 | ReloadAllGroupMembers().Wait(); 118 | } 119 | return groups?.ContainsKey(groupId) ?? false; 120 | } 121 | 122 | public static QGroupInfo[]? GetGroupList() 123 | { 124 | if (DateTime.Now - groupsLastSync > CredibleTime) 125 | { 126 | ReloadAllGroupMembers().Wait(); 127 | } 128 | return groups?.Values.ToArray(); 129 | } 130 | 131 | public static QGroupInfo? GetGroupInfo(long groupId) 132 | { 133 | if (DateTime.Now - groupsLastSync > CredibleTime) 134 | { 135 | ReloadAllGroupMembers().Wait(); 136 | } 137 | return groups?.GetValueOrDefault(groupId); 138 | } 139 | 140 | public static QMemberInfo[]? GetMemberList(long groupId) 141 | { 142 | if (DateTime.Now - groupsLastSync > CredibleTime) 143 | { 144 | ReloadAllGroupMembers().Wait(); 145 | } 146 | return members?.GetValueOrDefault(groupId)?.Select(p => p.Value).ToArray(); 147 | } 148 | 149 | public static QMemberInfo? GetMemberInfo(long groupId, long memberId) 150 | { 151 | if (DateTime.Now - groupsLastSync > CredibleTime) 152 | { 153 | ReloadAllGroupMembers().Wait(); 154 | } 155 | return members?.GetValueOrDefault(groupId)?.GetValueOrDefault(memberId); 156 | } 157 | 158 | public static bool HasFriend(long qqId) 159 | { 160 | if (DateTime.Now - friendsLastSync > CredibleTime) 161 | { 162 | ReloadFriends().Wait(); 163 | } 164 | return friends?.ContainsKey(qqId) ?? false; 165 | } 166 | 167 | public static QFriendInfo[]? GetFriendList() 168 | { 169 | if (DateTime.Now - friendsLastSync > CredibleTime) 170 | { 171 | ReloadFriends().Wait(); 172 | } 173 | return friends?.Values.ToArray(); 174 | } 175 | 176 | public static QFriendInfo? GetFriendInfo(long qqId) 177 | { 178 | if (DateTime.Now - friendsLastSync > CredibleTime) 179 | { 180 | ReloadFriends().Wait(); 181 | } 182 | return friends?.GetValueOrDefault(qqId); 183 | } 184 | 185 | public static long[] GetTempPath(long qqId) 186 | { 187 | if (DateTime.Now - groupsLastSync > CredibleTime) 188 | { 189 | ReloadAllGroupMembers().Wait(); 190 | } 191 | return members?.Where(p => p.Value.ContainsKey(qqId)).Select(p => p.Key).ToArray() ?? Array.Empty(); 192 | } 193 | 194 | public static void RegistStranger(QStrangerInfo strangerInfo) 195 | { 196 | strangers[strangerInfo.Id] = strangerInfo; 197 | } 198 | 199 | public static bool HasStranger(long qqId) 200 | { 201 | return strangers.ContainsKey(qqId); 202 | } 203 | 204 | public static QStrangerInfo[] GetStrangerList() 205 | { 206 | return strangers.Values.ToArray(); 207 | } 208 | 209 | public static QStrangerInfo? GetStrangerInfo(long qqId) 210 | { 211 | return strangers.GetValueOrDefault(qqId); 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /CocoaFramework/Support/BotReg.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System.Collections.Concurrent; 5 | using System.Linq; 6 | using System.Reflection; 7 | 8 | namespace Maila.Cocoa.Framework.Support 9 | { 10 | public static class BotReg 11 | { 12 | private static ConcurrentDictionary data = new(); 13 | 14 | internal static void Init() 15 | { 16 | DataHosting.AddOptimizeEnabledHosting( 17 | typeof(BotReg).GetField("data", BindingFlags.Static | BindingFlags.NonPublic)!, 18 | null, 19 | "BotReg"); 20 | } 21 | 22 | internal static void Reset() 23 | { 24 | data = new(); 25 | } 26 | 27 | public static bool ContainsKey(string key) 28 | { 29 | return data.ContainsKey(key); 30 | } 31 | 32 | public static string[] GetKeys() 33 | { 34 | return data.Keys.ToArray(); 35 | } 36 | 37 | public static string[] GetKeys(string path) 38 | { 39 | return data.Keys 40 | .Where(k => k.StartsWith(path)) 41 | .Select(k => k[(path.EndsWith('/') ? path.Length : path.Length + 1)..]) 42 | .ToArray(); 43 | } 44 | 45 | public static string GetString(string key, string defaultVal = "") 46 | { 47 | return data.ContainsKey(key) ? data[key] : defaultVal; 48 | } 49 | 50 | public static void SetString(string key, string val) 51 | { 52 | data[key] = val; 53 | } 54 | 55 | public static int GetInt(string key, int defaultVal = 0) 56 | { 57 | if (data.ContainsKey(key)) 58 | { 59 | return int.TryParse(data[key], out int val) ? val : defaultVal; 60 | } 61 | 62 | return defaultVal; 63 | } 64 | 65 | public static void SetInt(string key, int val) 66 | { 67 | data[key] = val.ToString(); 68 | } 69 | 70 | public static long GetLong(string key, long defaultVal = 0) 71 | { 72 | if (data.ContainsKey(key)) 73 | { 74 | return long.TryParse(data[key], out long val) ? val : defaultVal; 75 | } 76 | 77 | return defaultVal; 78 | } 79 | 80 | public static void SetLong(string key, long val) 81 | { 82 | data[key] = val.ToString(); 83 | } 84 | 85 | public static float GetFloat(string key, float defaultVal = 0) 86 | { 87 | if (data.ContainsKey(key)) 88 | { 89 | return float.TryParse(data[key], out float val) ? val : defaultVal; 90 | } 91 | 92 | return defaultVal; 93 | } 94 | 95 | public static void SetFloat(string key, float val) 96 | { 97 | data[key] = val.ToString(); 98 | } 99 | 100 | public static double GetDouble(string key, double defaultVal = 0) 101 | { 102 | if (data.ContainsKey(key)) 103 | { 104 | return double.TryParse(data[key], out double val) ? val : defaultVal; 105 | } 106 | 107 | return defaultVal; 108 | } 109 | 110 | public static void SetDouble(string key, double val) 111 | { 112 | data[key] = val.ToString(); 113 | } 114 | 115 | public static bool GetBool(string key, bool defaultVal = false) 116 | { 117 | if (data.ContainsKey(key)) 118 | { 119 | return bool.TryParse(data[key], out bool val) ? val : defaultVal; 120 | } 121 | 122 | return defaultVal; 123 | } 124 | 125 | public static void SetBool(string key, bool val) 126 | { 127 | data[key] = val.ToString(); 128 | } 129 | 130 | public static bool Remove(string key) 131 | { 132 | return data.TryRemove(key, out _); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /CocoaFramework/Support/DataHosting.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Reflection; 9 | using System.Security.Cryptography; 10 | using System.Text; 11 | using System.Threading; 12 | using System.Threading.Tasks; 13 | using Newtonsoft.Json; 14 | 15 | namespace Maila.Cocoa.Framework.Support 16 | { 17 | public static class DataHosting 18 | { 19 | private class HostingInfo 20 | { 21 | private readonly FieldInfo field; 22 | private readonly object? instance; 23 | private readonly string fileName; 24 | private readonly string filePath; 25 | private readonly string folderPath; 26 | private readonly bool optim; 27 | private byte[] lastHash = Array.Empty(); 28 | private DateTime lastSave = DateTime.MinValue; 29 | 30 | public HostingInfo(FieldInfo field, object? instance, string fileName, bool optim) 31 | { 32 | this.field = field; 33 | this.instance = instance; 34 | this.fileName = fileName; 35 | this.optim = optim; 36 | 37 | filePath = $"{DataManager.DataRoot}{fileName}.json"; 38 | folderPath = Path.GetDirectoryName(filePath) ?? DataManager.DataRoot; 39 | } 40 | 41 | private int _lock; 42 | 43 | public async Task Sync() 44 | { 45 | if (Interlocked.Exchange(ref _lock, 1) == 1) 46 | { 47 | return; 48 | } 49 | 50 | string current = JsonConvert.SerializeObject(field.GetValue(instance)); 51 | byte[] currentHash = MD5.HashData(Encoding.UTF8.GetBytes(current)); 52 | 53 | if (!File.Exists(filePath)) 54 | { 55 | if (optim && current == "{}") 56 | { 57 | Optimize(); 58 | } 59 | else 60 | { 61 | DataManager.SaveData(fileName, field.GetValue(instance)); 62 | } 63 | 64 | lastHash = currentHash; 65 | } 66 | else if (File.GetLastWriteTimeUtc(filePath) > lastSave) 67 | { 68 | field.SetValue(instance, await DataManager.LoadData(fileName, field.FieldType)); 69 | 70 | try 71 | { 72 | string last = JsonConvert.SerializeObject(field.GetValue(instance)); 73 | lastHash = MD5.HashData(Encoding.UTF8.GetBytes(last)); 74 | } 75 | catch 76 | { 77 | DataManager.SaveData(fileName, field.GetValue(instance)); 78 | } 79 | } 80 | else if (optim && current == "{}") 81 | { 82 | Optimize(); 83 | } 84 | else if (!currentHash.SequenceEqual(lastHash)) 85 | { 86 | DataManager.SaveData(fileName, field.GetValue(instance)); 87 | lastHash = currentHash; 88 | } 89 | 90 | lastSave = DateTime.UtcNow; 91 | _lock = 0; 92 | } 93 | 94 | private void Optimize() 95 | { 96 | if (File.Exists(filePath)) 97 | { 98 | File.Delete(filePath); 99 | } 100 | 101 | if (Directory.Exists(folderPath) 102 | && Directory.GetFiles(folderPath).Length == 0 103 | && Directory.GetDirectories(folderPath).Length == 0) 104 | { 105 | Directory.Delete(folderPath); 106 | } 107 | } 108 | } 109 | 110 | private static readonly List hostingInfos = new(); 111 | private static CancellationTokenSource? _hosting; 112 | private static bool stopHosting; 113 | 114 | internal static void AddHosting(FieldInfo field, object? instance, string name) 115 | { 116 | hostingInfos.Add(new(field, instance, name, false)); 117 | } 118 | 119 | internal static void AddOptimizeEnabledHosting(FieldInfo field, object? instance, string name) 120 | { 121 | hostingInfos.Add(new(field, instance, name, true)); 122 | } 123 | 124 | internal static async Task SyncAll() 125 | { 126 | foreach (var h in hostingInfos) 127 | { 128 | await h.Sync(); 129 | } 130 | } 131 | 132 | internal static async void StartHosting(TimeSpan delay) 133 | { 134 | if (delay.TotalSeconds < 1) 135 | { 136 | await SyncAll(); 137 | return; 138 | } 139 | 140 | if (Interlocked.CompareExchange(ref _hosting, new(), null) is not null) 141 | { 142 | throw new("Duplicated Calling"); 143 | } 144 | 145 | while (!stopHosting) 146 | { 147 | await SyncAll(); 148 | try 149 | { 150 | await Task.Delay(delay, _hosting.Token); 151 | } 152 | catch (TaskCanceledException) 153 | { 154 | if (!stopHosting) 155 | { 156 | _hosting = new(); 157 | } 158 | } 159 | } 160 | 161 | stopHosting = false; 162 | } 163 | 164 | internal static async Task StopHosting() 165 | { 166 | if (_hosting is null) 167 | { 168 | await SyncAll(); 169 | return; 170 | } 171 | stopHosting = true; 172 | _hosting.Cancel(); 173 | _hosting = null; 174 | 175 | while (stopHosting) 176 | { 177 | await Task.Delay(10); 178 | } 179 | await SyncAll(); 180 | hostingInfos.Clear(); 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /CocoaFramework/Support/DataManager.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System; 5 | using System.Collections.Concurrent; 6 | using System.IO; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using Newtonsoft.Json; 10 | 11 | namespace Maila.Cocoa.Framework.Support 12 | { 13 | public static class DataManager 14 | { 15 | public static readonly string DataRoot = "data/"; 16 | 17 | internal static bool SavingData => !savingStatus.IsEmpty; 18 | 19 | private static readonly ConcurrentDictionary savingStatus = new(); 20 | private static readonly ConcurrentDictionary savingStatusLock = new(); 21 | 22 | public static async void SaveData(string name, object? obj, bool indented = true) 23 | { 24 | var statusLock = savingStatusLock.GetOrAdd(name, _ => new(1)); 25 | await statusLock.WaitAsync(); 26 | 27 | if (savingStatus.TryGetValue(name, out var status)) 28 | { 29 | savingStatus[name] = (true, obj); 30 | statusLock.Release(); 31 | return; 32 | } 33 | else 34 | { 35 | savingStatus[name] = (false, null); 36 | } 37 | 38 | statusLock.Release(); 39 | 40 | var path = $"{DataRoot}{name}.json"; 41 | var directory = Path.GetDirectoryName(path); 42 | if (directory == null) 43 | { 44 | // Error: bad save path 45 | savingStatus.TryRemove(name, out _); 46 | return; 47 | } 48 | 49 | if (!Directory.Exists(directory)) 50 | { 51 | Directory.CreateDirectory(directory); 52 | } 53 | 54 | var formatting = indented ? Formatting.Indented : Formatting.None; 55 | await File.WriteAllTextAsync(path, JsonConvert.SerializeObject(obj, formatting)); 56 | 57 | await statusLock.WaitAsync(); 58 | while (savingStatus.TryGetValue(name, out status) && status.valueUpdated) 59 | { 60 | savingStatus[name] = (false, null); 61 | statusLock.Release(); 62 | 63 | await File.WriteAllTextAsync(path, JsonConvert.SerializeObject(status.value, formatting)); 64 | 65 | await statusLock.WaitAsync(); 66 | } 67 | 68 | savingStatus.TryRemove(name, out _); 69 | statusLock.Release(); 70 | } 71 | 72 | public static async Task LoadData(string name) 73 | { 74 | while (savingStatus.ContainsKey(name)) 75 | { 76 | await Task.Delay(10); 77 | } 78 | 79 | return File.Exists($"{DataRoot}{name}.json") 80 | ? JsonConvert.DeserializeObject(await File.ReadAllTextAsync($"{DataRoot}{name}.json")) 81 | : default; 82 | } 83 | 84 | public static async Task LoadData(string name, Type type) 85 | { 86 | while (savingStatus.ContainsKey(name)) 87 | { 88 | await Task.Delay(10); 89 | } 90 | 91 | return File.Exists($"{DataRoot}{name}.json") 92 | ? JsonConvert.DeserializeObject(await File.ReadAllTextAsync($"{DataRoot}{name}.json"), type) 93 | : null; 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /CocoaFramework/UserIdentity.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maila. All rights reserved. 2 | // Licensed under the GNU AGPLv3 3 | 4 | using System; 5 | 6 | namespace Maila.Cocoa.Framework 7 | { 8 | [Flags] 9 | public enum UserIdentity 10 | { 11 | User 12 | = 0b0, 13 | Admin 14 | = 0b1, 15 | Owner 16 | = 0b10, 17 | Developer 18 | = 0b100, 19 | Debugger 20 | = 0b1000, 21 | Operator 22 | = 0b10000, 23 | Staff 24 | = 0b100000, 25 | Custom1 26 | = 0b1000000, 27 | Custom2 28 | = 0b10000000, 29 | Custom3 30 | = 0b100000000, 31 | Custom4 32 | = 0b1000000000, 33 | Custom5 34 | = 0b10000000000, 35 | Custom6 36 | = 0b100000000000, 37 | Custom7 38 | = 0b1000000000000, 39 | Custom8 40 | = 0b10000000000000, 41 | Custom9 42 | = 0b100000000000000, 43 | 44 | SU 45 | = 0b111111 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Docs/Manual/API.md: -------------------------------------------------------------------------------- 1 |

Cocoa Framework API

2 |
3 | 4 | ## [Startup](./API/Startup/index.md) 5 | - [BotStartup](./API/Startup/BotStartup.md) 6 | - [BotStartupConfig](./API/Startup/BotStartupConfig.md) 7 | 8 |
9 | 10 | ## [Attributes](./API/Attributes.md) 11 | 12 |
13 | 14 | ## [Core](./API/Core/index.md) 15 | - [UserIdentity](./API/Core/UserIdentity.md) 16 | - [Module](./API/Core/Module.md) 17 | - [Middleware](./API/Core/Middleware.md) 18 | 19 |
20 | 21 | ## [Meeting](./API/Meeting/index.md) 22 | - [MessageReceiver](./API/Meeting/MessageReceiver.md) 23 | - [ListeningTarget](./API/Meeting/ListeningTarget.md) 24 | - [MeetingTimeout](./API/Meeting/MeetingTimeout.md) 25 | - [NotFit](./API/Meeting/NotFit.md) 26 | - [AsyncTask](./API/Meeting/AsyncTask.md) 27 | - [GetValue](./API/Meeting/GetValue.md) 28 | 29 |
30 | 31 | ## [Support](./API/Support/index.md) 32 | - [BotAPI](./API/Support/BotAPI.md) 33 | - [BotAuth](./API/Support/BotAuth.md) 34 | - [BotInfo](./API/Support/BotInfo.md) 35 | - [BotReg](./API/Support/BotReg.md) 36 | - [DataManager](./API/Support/DataManager.md) -------------------------------------------------------------------------------- /Docs/Manual/API/Attributes.md: -------------------------------------------------------------------------------- 1 | # Attributes 2 | 3 | ## DisabledAttribute 4 | 指示 Cocoa Framework 应忽视此内容。 5 | 可指定于:`类`、`方法`、`字段`、`参数` 6 | 7 |
8 | 9 | ## BotModuleAttribute 10 | 指示当前类是一个 Module。 11 | 可指定于:`类` 12 | 13 |
14 | 15 | ## IdentityRequirementsAttribute 16 | 指示执行当前 Module 或功能的身份要求。 17 | 可指定于:`类`、`方法` 18 | 可多次指定 19 | 20 |
21 | 22 | ## DisableInGroupAttribute 23 | 指示当前 Module 或功能不可用于群聊。 24 | 可指定于:`类`、`方法` 25 | 26 |
27 | 28 | ## DisableInPrivateAttribute 29 | 指示当前 Module 或功能不可用于私聊。 30 | 可指定于:`类`、`方法` 31 | 32 |
33 | 34 | ## ThreadSafeAttribute 35 | 指示当前方法是线程安全的,Cocoa Framework 会对这些方法进行异步调用。 36 | 可指定于:`方法` 37 | 38 |
39 | 40 | ## HostingAttribute 41 | 指示当前字段是托管数据字段。由于平台限制,无法托管静态只读字段。 42 | 可指定于:`字段` 43 | 44 |
45 | 46 | ## TextRouteAttribute 47 | 指示当前方法可被文本路由。 48 | 可指定于:`字段` 49 | 可多次指定 50 | 51 |
52 | 53 | ## RegexRouteAttribute 54 | 指示当前方法可被正则路由。 55 | 可指定于:`字段` 56 | 可多次指定 57 | 58 |
59 | 60 | ## GroupNameAttribute 61 | 指示当前参数将映射到指定名字的正则组。 62 | 可指定于:`参数` 63 | 64 |
65 | 66 | ## MemoryOnlyAttribute 67 | 指示当前 AutoData 参数为临时自动数据,仅存储于内存中 68 | 可指定于:`参数` 69 | 70 |
71 | 72 | ## SharedFromAttribute 73 | 指示当前 AutoData 将共享来自另一 Module 的数据 -------------------------------------------------------------------------------- /Docs/Manual/API/Core/Middleware.md: -------------------------------------------------------------------------------- 1 | # Middleware 2 | 3 | Middleware 用于对消息进行进行预处理,执行顺序有严格的要求,因此由开发者手动添加,最终执行顺序与添加顺序一致。 4 | 5 |
6 | 7 | ## MiddlewareCore 类 8 | 命名空间:Maila.Cocoa.Framework.Core 9 | 10 |
11 | 12 | 管理 Middleware 的核心类 13 | ```C# 14 | public static class MiddlewareCore 15 | ``` 16 | 17 | ### 属性 18 | - Middlewares 19 | > 当前所有被加载的 Middleware 20 | > ```C# 21 | > public static ImmutableArray Middlewares { get; } 22 | > ``` 23 | 24 |
25 | 26 | ## BotMiddlewareBase 类 27 | 命名空间:Maila.Cocoa.Framework 28 | 29 |
30 | 31 | Middleware 的基类,所有 Middleware 应派生自此类。 32 | ```C# 33 | public abstract class BotMiddlewareBase 34 | ``` 35 | 36 | ### 方法 37 | - Init 38 | > 初始化时被调用 39 | > ```C# 40 | > protected virtual void Init(); 41 | > ``` 42 | - Destroy 43 | > 断开连接时被调用 44 | > ```C# 45 | > protected virtual void Destroy(); 46 | > ``` 47 | - OnMessage 48 | > 收到消息时被调用 49 | > ```C# 50 | > protected virtual void OnMessage(MessageSource src, QMessage msg, Action next) 51 | > ``` 52 | > #### 参数 53 | > `src` MessageSource 54 | > 消息来源 55 | > `msg` QMessage 56 | > 消息内容 57 | > `next` Action\ 58 | > 下一个 Middleware 的 OnMessage 方法,如果允许消息继续传递需要调用此方法,如需更改消息来源或消息内容可以直接把新的内容作为 `next` 的参数。注意,`next` 在一次执行中最多允许调用一次,建议在调用 `next` 后直接使用 return 结束 59 | - OnSendMessage 60 | > 发送消息时被调用 61 | > ```C# 62 | > protected virtual bool OnSendMessage(ref long id, ref bool isGroup, ref IMessage[] chain, ref int? quote); 63 | > ``` 64 | > #### 参数 65 | > `id` ref long 66 | > 发送目标 67 | > `isGroup` ref bool 68 | > 目标为群聊 69 | > `chain` ref IMessage[] 70 | > 要发送的消息链 71 | > `quote` ref int? 72 | > 要回复的消息 Id,不回复时为 null 73 | > #### 返回值 74 | > bool 75 | > 是否同意发送 -------------------------------------------------------------------------------- /Docs/Manual/API/Core/Module.md: -------------------------------------------------------------------------------- 1 | # Module 2 | 3 | Module 进行具体处理,一般情况下应各司其职,使运行结果不受 Module 运行顺序的影响。因此 Module 由 Cocoa Framework 自动搜索和添加。但 Cocoa Framework 也提供了 Module 间的优先级功能,以便应对特殊情况。 4 | 5 |
6 | 7 | ## ModuleCore 类 8 | 命名空间:Maila.Cocoa.Framework.Core 9 | 10 |
11 | 12 | 管理 Module 的核心类 13 | ```C# 14 | public static class ModuleCore 15 | ``` 16 | 17 | ### 属性 18 | - Modules 19 | > 当前所有被加载的 Modules 20 | > ```C# 21 | > public static ImmutableArray Modules { get; } 22 | > ``` 23 | 24 | ### 方法 25 | - AddLock 26 | > 添加消息锁 27 | > 28 | > ```C# 29 | > public static void AddLock(Func lockRun); 30 | > public static void AddLock(Func lockRun, Predicate predicate); 31 | > public static void AddLock(Func lockRun, ListeningTarget target); 32 | > public static void AddLock(Func lockRun, MessageSource src); 33 | > public static void AddLock(Func lockRun, Predicate predicate, TimeSpan timeout, Action? onTimeout = null); 34 | > public static void AddLock(Func lockRun, ListeningTarget target, TimeSpan timeout, Action? onTimeout = null); 35 | > public static void AddLock(Func lockRun, MessageSource src, TimeSpan timeout, Action? onTimeout = null); 36 | > ``` 37 | > #### 参数 38 | > `lockRun` Func\ 39 | > 消息锁运行方法 40 | > `predicate` Predicate\ 41 | > 消息锁运行条件 42 | > `target` ListeningTarget 43 | > 消息锁监听目标,仅目标符合时消息锁才会被调用 44 | > `src` MessageSource 45 | > 消息锁监听源,仅消息源一致时消息锁才会被调用 46 | > `timeout` TimeSpan 47 | > 超时时间,消息锁被添加后经过指定时间仍未被调用称为超时,超时后消息锁会被自动移除 48 | > `onTimeout` Action 49 | > 超时回调,超时后会被调用 50 | > 51 | > #### 示例 52 | > ```C# 53 | > [TextRoute("你好")] 54 | > public static void Hello(MessageSource src) 55 | > { 56 | > src.Send("你好!你的名字是?"); 57 | > ModuleCore.AddLock((_src, _msg) => 58 | > { 59 | > _src.Send($"你好,{_msg.PlainText}!"); 60 | > return LockState.Finished; 61 | > }, src); 62 | > } 63 | > 64 | > // <= 你好 65 | > // => 你好!你的名字是? 66 | > // <= Chino 67 | > // => 你好,Chino! 68 | > ``` 69 | 70 |
71 | 72 | ## BotModuleBase 类 73 | 命名空间:Maila.Cocoa.Framework 74 | 75 |
76 | 77 | Module 的基类,所有 Module 应派生自此类。 78 | ```C# 79 | public abstract class BotModuleBase 80 | ``` 81 | 82 | ### 属性 83 | - Name 84 | > Module 名,匿名 Module 的本属性值为 null 85 | > ```C# 86 | > public string? Name { get; } 87 | > ``` 88 | - Priority 89 | > 优先顺序,数值越大越晚被执行 90 | > ```C# 91 | > public int Priority { get; } 92 | > ``` 93 | - EnableInGroup 94 | > 在群聊中是否可用 95 | > ```C# 96 | > public bool EnableInGroup { get; } 97 | > ``` 98 | - EnableInPrivate 99 | > 在私聊时是否可用,私聊包含好友消息和临时消息 100 | > ```C# 101 | > public bool EnableInPrivate { get; } 102 | > ``` 103 | - IsAnonymous 104 | > 是否为匿名 Module 105 | > ```C# 106 | > public bool IsAnonymous { get; } 107 | > ``` 108 | - Enabled 109 | > Module 是否被启用 110 | > ```C# 111 | > public bool Enabled { get; set; } 112 | > ``` 113 | 114 | ### 方法 115 | - Init 116 | > 初始化时被调用 117 | > ```C# 118 | > protected virtual void Init(); 119 | > ``` 120 | - Destroy 121 | > 断开连接时被调用 122 | > ```C# 123 | > protected virtual void Destroy(); 124 | > ``` 125 | - OnMessage 126 | > 收到消息时被调用 127 | > ```C# 128 | > protected virtual bool OnMessage(MessageSource src, QMessage msg); 129 | > ``` 130 | > #### 参数 131 | > `src` MessageSource 132 | > 消息来源 133 | > `msg` QMessage 134 | > 消息内容 135 | > 136 | > #### 返回值 137 | > bool 138 | > 消息是否被处理 139 | - OnMessageFinished 140 | > 消息处理完后被调用 141 | > ```C# 142 | > protected virtual void OnMessageFinished(MessageSource src, QMessage msg, MessageSource origSrc, QMessage origMsg, bool processed, BotModuleBase? processModule); 143 | > ``` 144 | > #### 参数 145 | > `src` MessageSource 146 | > 消息来源 147 | > `msg` QMessage 148 | > 消息内容 149 | > `origSrc` MessageSource 150 | > 被 Middleware 处理前的消息来源 151 | > `origMsg` QMessage 152 | > 被 Middleware 处理前的消息内容 153 | > `processed` bool 154 | > 消息是否被处理 155 | > `processModule` BotModuleBase? 156 | > 处理该消息的 Module,如果未被处理则为 null 157 | -------------------------------------------------------------------------------- /Docs/Manual/API/Core/UserIdentity.md: -------------------------------------------------------------------------------- 1 | # UserIdentity 枚举 2 | 命名空间:Maila.Cocoa.Framework 3 | 4 |
5 | 6 | 标识用户身份。 7 | 此枚举有一个 FlagsAttribute 特性,允许按位组合成员值。 8 | ```C# 9 | [System.Flags] 10 | public enum UserIdentity 11 | 12 | ``` 13 | 14 | ## 字段 15 | | | | | 16 | | - | - | - | 17 | | User | 0 | | 18 | | Admin | 1 | | 19 | | Owner | 2 | | 20 | | Developer | 4 | | 21 | | Debugger | 8 | | 22 | | Operator | 16 | | 23 | | Staff | 32 | | 24 | | Custom1 | 64 | | 25 | | Custom2 | 128 | | 26 | | Custom3 | 256 | | 27 | | Custom4 | 512 | | 28 | | Custom5 | 1024 | | 29 | | Custom6 | 2048 | | 30 | | Custom7 | 4096 | | 31 | | Custom8 | 8192 | | 32 | | Custom9 | 16384 | | 33 | | SU | 63 | 超级用户,包含自定义身份以外的全部身份 | 34 | -------------------------------------------------------------------------------- /Docs/Manual/API/Core/index.md: -------------------------------------------------------------------------------- 1 | # Core 2 | 3 | Cocoa Framework 的核心是 Middleware 和 Module,主要负责对消息的处理。 4 | 5 |
6 | 7 | - [UserIdentity](./UserIdentity.md) 8 | - [Module](./Module.md) 9 | - [Middleware](./Middleware.md) -------------------------------------------------------------------------------- /Docs/Manual/API/Meeting/AsyncTask.md: -------------------------------------------------------------------------------- 1 | # AsyncTask 类 2 | 命名空间:Maila.Cocoa.Framework.Models.Processing 3 | 4 |
5 | 6 | 用于在 Meeting 中执行异步任务。请注意,异步任务执行过程中不会阻塞消息,因此在 Meeting 等待异步任务的过程中可能由用户误操作导致重复建立 Meeting,请自行避免。 7 | ```C# 8 | public class AsyncTask 9 | ``` 10 | 11 |
12 | 13 | ## 方法 14 | - Wait 15 | > 暂停执行 16 | > ```C# 17 | > public static AsyncTask Wait(int milliseconds); 18 | > public static AsyncTask Wait(int milliseconds, CancellationToken cancellationToken); 19 | > public static AsyncTask Wait(TimeSpan delay); 20 | > public static AsyncTask Wait(TimeSpan delay, CancellationToken cancellationToken); 21 | > ``` 22 | > 23 | > ### 参数 24 | > `milliseconds` int 25 | > 暂停时长(毫秒) 26 | > `delay` TimeSpan 27 | > 暂停时长 28 | > `cancellationToken` CancellationToken 29 | > 用于取消执行的 Token 30 | - WaitUntil 31 | > 在指定时间之前暂停执行 32 | > ```C# 33 | > public static AsyncTask WaitUntil(DateTime time); 34 | > public static AsyncTask WaitUntil(DateTime time, CancellationToken cancellationToken); 35 | > ``` 36 | > 37 | > ### 参数 38 | > `time` DateTime 39 | > 结束时间 40 | > `cancellationToken` CancellationToken 41 | > 用于取消执行的 Token 42 | - Run 43 | > 执行指定的异步任务 44 | > ```C# 45 | > public static AsyncTask Run(Action action); 46 | > public static AsyncTask Run(Action action, CancellationToken cancellationToken); 47 | > public static AsyncTask Run(Func function); 48 | > public static AsyncTask Run(Func function, CancellationToken cancellationToken); 49 | > public static AsyncTask Run(Func function, out GetValue result); 50 | > public static AsyncTask Run(Func> func, out GetValue result); 51 | > public static AsyncTask Run(Func> func, out GetValue result, CancellationToken cancellationToken); 52 | > ``` 53 | > 54 | > ### 参数 55 | > `action` Action 56 | > 要异步执行的同步任务 57 | > `function` Func\ 58 | > 要执行的异步任务 59 | > `function` Func\ 60 | > 要异步执行的同步函数 61 | > `function` Func\> 62 | > 要执行的异步函数 63 | > `result` out GetValue\ 64 | > 用于获取函数返回值的对象 65 | > `cancellationToken` CancellationToken 66 | > 用于取消执行的 Token -------------------------------------------------------------------------------- /Docs/Manual/API/Meeting/GetValue.md: -------------------------------------------------------------------------------- 1 | # GetValue 类 2 | 命名空间:Maila.Cocoa.Framework.Models.Processing 3 | 4 |
5 | 6 | 用于在 Meeting 间传输数据。 7 | ```C# 8 | public class GetValue 9 | ``` 10 | 11 |
12 | 13 | ## 字段 14 | - `Value` T? 15 | > 要传输的数据 -------------------------------------------------------------------------------- /Docs/Manual/API/Meeting/ListeningTarget.md: -------------------------------------------------------------------------------- 1 | # ListeningTarget 类 2 | 命名空间:Maila.Cocoa.Framework.Models.Processing 3 | 4 |
5 | 6 | 用于更换监听目标。 7 | ```C# 8 | public class ListeningTarget 9 | ``` 10 | 11 |
12 | 13 | ## 属性 14 | - All 15 | > 表示监听全部内容 16 | > ```C# 17 | > public static ListeningTarget All { get; } 18 | > ``` 19 | 20 |
21 | 22 | ## 方法 23 | - FromGroup 24 | > 设置监听目标为某个群 25 | > ```C# 26 | > public static ListeningTarget FromGroup(long groupId); 27 | > public static ListeningTarget FromGroup(QGroup group); 28 | > ``` 29 | > 30 | > ### 参数 31 | > `groupId` long 32 | > 要监听的群号 33 | > `group` QGroup 34 | > 要监听的群 35 | - FromUser 36 | > 设置监听目标为某个用户 37 | > ```C# 38 | > public static ListeningTarget FromUser(long userId); 39 | > public static ListeningTarget FromUser(QUser user); 40 | > ``` 41 | > 42 | > ### 参数 43 | > `userId` long 44 | > 要监听的 QQ 号 45 | > `user` QUser 46 | > 要监听的用户 47 | - FromTarget 48 | > 指定具体监听目标 49 | > ```C# 50 | > public static ListeningTarget FromTarget(long groupId, long userId); 51 | > public static ListeningTarget FromTarget(MessageSource src); 52 | > ``` 53 | > 54 | > ### 参数 55 | > `groupId` long 56 | > 要监听的群号 57 | > `userId` long 58 | > 要监听的 QQ 号 59 | > `src` MessageSource 60 | > 要监听的消息源 61 | - CustomTarget 62 | > 自定义监听目标 63 | > ```C# 64 | > public static ListeningTarget CustomTarget(Predicate pred); 65 | > ``` 66 | > 67 | > ### 参数 68 | > `pred` Predicate\ 69 | > 目标的判定规则 70 | -------------------------------------------------------------------------------- /Docs/Manual/API/Meeting/MeetingTimeout.md: -------------------------------------------------------------------------------- 1 | # MeetingTimeout 类 2 | 命名空间:Maila.Cocoa.Framework.Models.Processing 3 | 4 |
5 | 6 | 用于设置超时时长。 7 | ```C# 8 | public class MeetingTimeout 9 | ``` 10 | 11 |
12 | 13 | ## 属性 14 | - Off 15 | > 表示关闭超时 16 | > ```C# 17 | > public static MeetingTimeout Off { get; } 18 | > ``` 19 | 20 |
21 | 22 | ## 方法 23 | - FromTimeSpan 24 | > 根据给定的 TimeSpan 设置超时时长 25 | > ```C# 26 | > public static MeetingTimeout FromTimeSpan(TimeSpan time); 27 | > ``` 28 | > 29 | > ### 参数 30 | > `time` TimeSpan 31 | > 超时时长 32 | - FromMinutes 33 | > 根据给定的分钟数设置超时时长 34 | > ```C# 35 | > public static MeetingTimeout FromMinutes(double minutes); 36 | > ``` 37 | > 38 | > ### 参数 39 | > `minutes` double 40 | > 超时时长(分钟) 41 | - FromSeconds 42 | > 根据给定的秒钟数设置超时时长 43 | > ```C# 44 | > public static MeetingTimeout FromSeconds(double seconds); 45 | > ``` 46 | > 47 | > ### 参数 48 | > `seconds` double 49 | > 超时时长(秒钟) -------------------------------------------------------------------------------- /Docs/Manual/API/Meeting/MessageReceiver.md: -------------------------------------------------------------------------------- 1 | # MessageReceiver 类 2 | 命名空间:Maila.Cocoa.Framework.Models.Processing 3 | 4 |
5 | 6 | 消息接收器,用于在 Meeting 中接收用户发送的消息。 7 | ```C# 8 | public class MessageReceiver 9 | ``` 10 | 11 |
12 | 13 | ## 字段 14 | - `Source` MessageSource? 15 | > 消息来源,超时情况下值为 null 16 | - `Message` QMessage? 17 | > 消息内容,超时情况下值为 null 18 | - `IsTimeout` bool 19 | > 是否为超时 -------------------------------------------------------------------------------- /Docs/Manual/API/Meeting/NotFit.md: -------------------------------------------------------------------------------- 1 | # NotFit 类 2 | 命名空间:Maila.Cocoa.Framework.Models.Processing 3 | 4 |
5 | 6 | 表示接收到的消息不符合要求,消息应继续传递。 7 | ```C# 8 | public class NotFit 9 | ``` 10 | 11 |
12 | 13 | ## 属性 14 | - Continue 15 | > 表示 Meeting 应继续运行 16 | > ```C# 17 | > public static NotFit Continue { get; } 18 | > ``` 19 | - Stop 20 | > 表示 Meeting 应停止运行 21 | > ```C# 22 | > public static NotFit Stop { get; } 23 | > ``` -------------------------------------------------------------------------------- /Docs/Manual/API/Meeting/index.md: -------------------------------------------------------------------------------- 1 | # Meeting 2 | 3 | Meeting 是对一段连续的消息处理过程的抽象,可用于实现对话功能。 4 | 5 |
6 | 7 | ## 开始 Meeting 8 | 可以使用 Meeting.Start 开启一次 Meeting,也可以通过路由自动启动 9 | 10 |
11 | 12 | ## yield return 13 | yield return 将作为 Meeting 向管理器传递状态的方式 14 | - 返回值类型 15 | - MessageReceiver:设置接收消息的接收器,此次返回不会中断执行 16 | - ListeningTarget:设置消息的监听源,此次返回会中断执行,直到有来自监听目标的消息 17 | - MeetingTimeout:设置 Meeting 的超时时长,超时后枚举器将会收到一次超时消息。设为 MeetingTimeout.Off 以关闭超时。此次返回不会中断执行 18 | - NotFit:使管理器返回 LockState.Continue 或 LockState.ContinueAndRemove。详见 MessageLock 19 | - string 或 StringBuilder:此次返回会中断执行,并向来源发送返回的内容 20 | - AsyncTask:执行异步任务,此次返回会在异步任务完成后继续执行 21 | - IEnumerator 或 IEnumerable:将给定的枚举器作为子 Meeting,此枚举器的 Next 方法会被立即调用。借助 GetValue 可实现 Meeting 间通信 22 | - null:此次返回会中断执行,直到有来自监听目标的消息 23 | 24 |
25 | 26 | ## ProcessingModels 27 | - [MessageReceiver](./MessageReceiver.md) 28 | - [ListeningTarget](./ListeningTarget.md) 29 | - [MeetingTimeout](./MeetingTimeout.md) 30 | - [NotFit](./NotFit.md) 31 | - [AsyncTask](./AsyncTask.md) 32 | - [GetValue](./GetValue.md) -------------------------------------------------------------------------------- /Docs/Manual/API/Startup/BotStartup.md: -------------------------------------------------------------------------------- 1 | # BotStartup 类 2 | 命名空间:Maila.Cocoa.Framework 3 | 4 |
5 | 6 | 提供运行状态管理的相关方法。 7 | ```C# 8 | public static class BotStartup 9 | ``` 10 | 11 |
12 | 13 | ## 属性 14 | - Connected 15 | > 表示是否已连接到 Mirai 16 | > ```C# 17 | > public static bool Connected { get; } 18 | > ``` 19 | 20 |
21 | 22 | ## 方法 23 | - ConnectAndInit 24 | > 连接 Mirai 并初始化 25 | > ```C# 26 | > public static Task ConnectAndInit(BotStartupConfig config); 27 | > ``` 28 | > 29 | > ### 参数 30 | > `config` [BotStartupConfig](./BotStartupConfig.md) 31 | > 启动信息 32 | > 33 | > ### 返回值 34 | > bool 35 | > 表示是否初始化成功 36 | - DisconnectAndSaveData 37 | > 断开连接并保存数据 38 | > ```C# 39 | > public static Task DisconnectAndSaveData(); 40 | > ``` 41 | - Reconnect 42 | > 重新连接并重新加载模块 43 | > ```C# 44 | > public static Task Reconnect(); 45 | > ``` -------------------------------------------------------------------------------- /Docs/Manual/API/Startup/BotStartupConfig.md: -------------------------------------------------------------------------------- 1 | # BotStartupConfig 类 2 | 命名空间:Maila.Cocoa.Framework 3 | 4 |
5 | 6 | 用于配置启动信息。 7 | ```C# 8 | public class BotStartupConfig 9 | ``` 10 | 11 |
12 | 13 | ## 构造函数 14 | - BotStartupConfig(string, long, string) 15 | > 初始化 BotStartupConfig 类的新实例,默认端口为 80 16 | > ```C# 17 | > public BotStartupConfig(string verifyKey, long qqId, string host); 18 | > ``` 19 | > 20 | > ### 参数 21 | > `verifyKey` string 22 | > 连接密钥 23 | > `qqId` long 24 | > 机器人的 QQ 号 25 | > `host` string 26 | > mirai-api-http 的地址 27 | - BotStartupConfig(string, long, int) 28 | > 初始化 BotStartupConfig 类的新实例,默认 mirai-api-http 的地址为 127.0.0.1 29 | > ```C# 30 | > public BotStartupConfig(string verifyKey, long qqId, int port); 31 | > ``` 32 | > 33 | > ### 参数 34 | > `verifyKey` string 35 | > 连接密钥 36 | > `qqId` long 37 | > 机器人的 QQ 号 38 | > `port` int 39 | > 端口 40 | - BotStartupConfig(string, long, string, int) 41 | > 初始化 BotStartupConfig 类的新实例,默认 mirai-api-http 的地址为 127.0.0.1,端口为 8080 42 | > ```C# 43 | > public BotStartupConfig(string verifyKey, long qqId, string host = "127.0.0.1", int port = 8080); 44 | > ``` 45 | > 46 | > ### 参数 47 | > `verifyKey` string 48 | > 连接密钥 49 | > `qqId` long 50 | > 机器人的 QQ 号 51 | > `host` string 52 | > mirai-api-http 的地址 53 | > `port` int 54 | > 端口 55 | 56 | ## 字段 57 | - `host` string 58 | > mirai-api-http 的地址 59 | - `port` int 60 | > 端口 61 | - `verifyKey` string 62 | > 连接密钥 63 | - `qqId` long 64 | > 机器人的 QQ 号 65 | - `autoSave` TimeSpan 66 | > 数据托管的自动保存间隔 67 | 68 |
69 | 70 | ## 属性 71 | - Assemblies 72 | > 包含 Module 的程序集列表 73 | > ```C# 74 | > public List Assemblies { get; } 75 | > ``` 76 | 77 |
78 | 79 | ## 方法 80 | - AddMiddleware 81 | > 添加 Middleware 82 | > ```C# 83 | > public BotStartupConfig AddMiddleware() where T : BotMiddlewareBase; 84 | > public BotStartupConfig AddMiddleware(Type type); 85 | > ``` 86 | > 87 | > ### 参数 88 | > `type` Type 89 | > Middleware 类 90 | > 91 | > ### 返回值 92 | > BotStartupConfig 93 | > 当前 BotStartupConfig 94 | - AddAssembly 95 | > 添加包含 Module 的程序集。入口程序集会被自动添加,请勿重复添加 96 | > ```C# 97 | > public BotStartupConfig AddAssembly(Assembly assem); 98 | > ``` 99 | > 100 | > ### 参数 101 | > `assem` Assembly 102 | > 要添加的程序集 103 | > 104 | > ### 返回值 105 | > BotStartupConfig 106 | > 当前 BotStartupConfig -------------------------------------------------------------------------------- /Docs/Manual/API/Startup/index.md: -------------------------------------------------------------------------------- 1 | # Startup 2 | 3 | Startup 是管理运行状态的部分,负责配置启动信息、启动、停止等功能。 4 | 5 |
6 | 7 | - [BotStartup](./BotStartup.md) 8 | - [BotStartupConfig](./BotStartupConfig.md) -------------------------------------------------------------------------------- /Docs/Manual/API/Support/BotAPI.md: -------------------------------------------------------------------------------- 1 | # BotAPI 类 2 | 命名空间:Maila.Cocoa.Framework.Support 3 | 4 |
5 | 6 | 对 mirai-api-http 提供的 API 的封装。 7 | ```C# 8 | public static class BotAPI 9 | ``` -------------------------------------------------------------------------------- /Docs/Manual/API/Support/BotAuth.md: -------------------------------------------------------------------------------- 1 | # BotAuth 类 2 | 命名空间:Maila.Cocoa.Framework.Support 3 | 4 |
5 | 6 | 提供用户身份和权限管理的相关功能。 7 | ```C# 8 | public static class BotAuth 9 | ``` -------------------------------------------------------------------------------- /Docs/Manual/API/Support/BotInfo.md: -------------------------------------------------------------------------------- 1 | # BotInfo 类 2 | 命名空间:Maila.Cocoa.Framework.Support 3 | 4 |
5 | 6 | 维护机器人的好友和群信息。 7 | ```C# 8 | public static class BotInfo 9 | ``` 10 | -------------------------------------------------------------------------------- /Docs/Manual/API/Support/BotReg.md: -------------------------------------------------------------------------------- 1 | # BotReg 类 2 | 命名空间:Maila.Cocoa.Framework.Support 3 | 4 |
5 | 6 | 机器人注册表。 7 | ```C# 8 | public static class BotReg 9 | ``` 10 | -------------------------------------------------------------------------------- /Docs/Manual/API/Support/DataManager.md: -------------------------------------------------------------------------------- 1 | # DataManager 类 2 | 命名空间:Maila.Cocoa.Framework.Support 3 | 4 |
5 | 6 | 提供数据管理相关功能。 7 | ```C# 8 | public static class DataManager 9 | ``` 10 | -------------------------------------------------------------------------------- /Docs/Manual/API/Support/index.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | Support 是对核心功能的补充,辅助核心功能的实现。 4 | 5 |
6 | 7 | - [BotAPI](./BotAPI.md) 8 | - [BotAuth](./BotAuth.md) 9 | - [BotInfo](./BotInfo.md) 10 | - [BotReg](./BotReg.md) 11 | - [DataManager](./DataManager.md) -------------------------------------------------------------------------------- /Docs/Manual/AsyncMeeting.md: -------------------------------------------------------------------------------- 1 | # AsyncMeeting 2 | 3 | AsyncMeeting 是 [Meeting](./Meeting.md) 的异步实现形式。 4 | 5 |
6 | 7 | ## 示例 8 | 本示例实现最简单的加法计算分步式引导。为了方便理解,本代码不包含非法输入判断。 9 | 10 | ```C# 11 | [TextRoute("加法")] 12 | [TextRoute("add")] 13 | public static async Task Proc(AsyncMeeting am) 14 | { 15 | am.Send("请输入第一个数"); 16 | var result = await am.Wait(); 17 | int a = int.Parse(result!.Message); // 未设置超时,result 不为空 18 | 19 | am.Send("请输入第二个数"); 20 | result = await am.Wait(); 21 | int b = int.Parse(result!.Message); 22 | 23 | // var result = await am.SendAndWait("请输入第一个数"); 24 | // int a = int.Parse(result!.Message); 25 | // 26 | // result = await am.SendAndWait("请输入第二个数"); 27 | // int b = int.Parse(result!.Message); 28 | 29 | return $"结果是 {a + b}"; 30 | } 31 | 32 | // <= 加法 33 | // => 请输入第一个数 34 | // <= 1 35 | // => 请输入第二个数 36 | // <= 2 37 | // => 结果是 3 38 | ``` -------------------------------------------------------------------------------- /Docs/Manual/AutoData.md: -------------------------------------------------------------------------------- 1 | # AutoData 2 | 3 | AutoData 是一种自动数据托管方案,可以根据消息来源提供不同的数据。AutoData 包括三种类型,即 UserAutoData<>、GroupAutoData<>、SourceAutoData<>,分别以用户、群和消息来源(相当于 QQ 中的窗口)作为区分数据的依据。在路由方法的参数列表中添加这些类型的参数,Cocoa Framework 便会提供对应的实例,处理过程中可以对这些实例的 Value 属性进行自由读写。更改后的 Value 会被自动保存至硬盘,以保证程序重启后数据仍然存在。如果不需要保存至硬盘,可以为对应的参数添加 MemoryOnlyAttribute 特性。 4 | 5 |
6 | 7 | ## 示例 8 | ```C# 9 | [RegexRoute("笔记 (?.+)")] 10 | public static string TakeNotes(string notes, UserAutoData paper) 11 | { 12 | paper.Value = notes; 13 | return "写好了!"; 14 | } 15 | 16 | [TextRoute("查看笔记")] 17 | public static string ViewNotes(UserAutoData paper) 18 | { 19 | if (paper.Value is null) 20 | { 21 | return "您没有记下任何内容"; 22 | } 23 | return paper.Value; 24 | } 25 | 26 | // user1 <= 查看笔记 27 | // => 您没有记下任何内容 28 | // user1 <= 笔记 测试 29 | // => 写好了! 30 | // user1 <= 查看笔记 31 | // => 测试 32 | // user2 <= 笔记 test 33 | // => 写好了! 34 | // user2 <= 查看笔记 35 | // => test 36 | // user1 <= 查看笔记 37 | // => 测试 38 | ``` 39 | 40 |
41 | 42 | ## 注解 43 | - 作用域 44 | - AutoData 目前仅可用于路由方法 45 | - AutoData 之间的数据互通性 46 | - 默认同一 Module 之内、参数名和参数类型一致、生命周期(是否保存至硬盘)一致的 AutoData 之间数据互通。如果需要实现 Module 之间的数据互通,请使用 SharedFrom 特性。 47 | - 性能 48 | - 本功能依赖于 DataManager 的数据托管功能,因此不推荐用于存储过于庞大的数据。不存储至硬盘的 AutoData 没有此限制 -------------------------------------------------------------------------------- /Docs/Manual/BotEventHandler.md: -------------------------------------------------------------------------------- 1 | # BotEventHandler 2 | 3 | BotEventHandler 用于处理事件。 4 | 5 |
6 | 7 | ## 示例 8 | 9 | ### BotEventHandler 10 | ```C# 11 | class BotEventHandler : BotEventHandlerBase 12 | { 13 | // Bot 事件处理器 14 | // 方法仅允许包含一个参数,且参数类型对应 Maila.Cocoa.Beans.Models.Events 中的事件类型 15 | // 方法名没有限制,但建议与事件名相关 16 | // 可以通过添加 DisabledAttribute 来禁用事件处理器 17 | void OnMemberJoin(MemberJoinEvent evt) 18 | { 19 | BotAPI.SendGroupMessage(evt.Member.Group.Id, "欢迎加入群聊!"); 20 | } 21 | 22 | // 其他事件处理器 23 | protected override void OnException(Exception e) 24 | { 25 | Console.WriteLine(e); 26 | } 27 | } 28 | ``` 29 | 30 | ### 启动代码 31 | ```C# 32 | /*BotStartupConfig*/ config.AddEventHandler(new BotEventHandler()); 33 | ``` 34 | 35 | ### 实现效果 36 | 当有成员加入群聊时,OnMemberJoin 会被调用,发送欢迎信息。当发生异常时,OnException 会被调用,输出异常信息。 -------------------------------------------------------------------------------- /Docs/Manual/CustomRoute.md: -------------------------------------------------------------------------------- 1 | # 自定义路由 2 | 3 | Cocoa Framework 支持自定义路由,以便于定制特殊的消息处理逻辑。 4 | 5 |
6 | 7 | ## 示例 8 | 本示例实现限定于特定群聊的路由。相较于普通的 TextRoute,添加了对群聊的判断。为便于理解,本示例不支持 TextRoute 的 IgnoreCase 与 AtRequired 功能,且仅支持设置一个群。 9 | 10 | ```C# 11 | // 继承 RouteInfo 类,实现对消息的处理 12 | public class GroupSpecifiedTextRoute : RouteInfo 13 | { 14 | private readonly string text; 15 | private readonly long groupId; 16 | 17 | public GroupSpecifiedTextRoute(BotModuleBase module, MethodInfo route, string text, long groupId) : base(module, route) 18 | { 19 | this.text = text; 20 | this.groupId = groupId; 21 | } 22 | 23 | protected override bool IsMatch(MessageSource src, QMessage msg) 24 | { 25 | return src.Group?.Id == groupId && msg.PlainText == text; 26 | } 27 | } 28 | 29 | // 继承 RouteAttribute 类,实现对应的路由特性 30 | public sealed class GroupSpecifiedTextRouteAttribute : RouteAttribute 31 | { 32 | public string Text { get; } 33 | public long GroupId { get; } 34 | 35 | public GroupSpecifiedTextRouteAttribute(string text, long groupId) 36 | { 37 | Text = text; 38 | GroupId = groupId; 39 | } 40 | 41 | public override RouteInfo GetRouteInfo(BotModuleBase module, MethodInfo route) 42 | { 43 | return new GroupSpecifiedTextRoute(module, route, Text, GroupId); 44 | } 45 | } 46 | 47 | // 使用自定义路由 48 | [BotModule] 49 | public class Demo : BotModuleBase 50 | { 51 | // 在群 123456 中收到“ping”时回复“pong” 52 | [GroupSpecifiedTextRoute("ping", 123456)] 53 | public static void Run(MessageSource src) 54 | { 55 | src.Send("pong"); 56 | } 57 | } 58 | ``` -------------------------------------------------------------------------------- /Docs/Manual/Data.md: -------------------------------------------------------------------------------- 1 | # 数据存储 2 | 3 | 可以使用 DataManager.SaveData 和 DataManager.LoadData 进行数据的存储和读取,也可以在 Module 和 Middleware 中为字段添加 Hosting 特性以自动读取和保存数据。 4 | 此类方式仅限轻量数据的存储,如配置信息、用户数据等。完整消息记录等庞大的数据继续使用此方式可能造成过大的性能消耗,此类数据建议使用数据库存储。 -------------------------------------------------------------------------------- /Docs/Manual/Meeting.md: -------------------------------------------------------------------------------- 1 | # Meeting 2 | 3 | Meeting 是对一段连续的消息处理过程的抽象,可用于实现对话功能。支持单人对话(如复杂功能的分步式引导)和多人会话(如多人游戏)场景。 4 | 5 |
6 | 7 | ## 示例 8 | 本示例实现最简单的加法计算分步式引导。为了方便理解,本代码不包含非法输入判断。 9 | 10 | ```C# 11 | [TextRoute("加法")] 12 | [TextRoute("add")] 13 | public IEnumerator Proc(MessageSource src) 14 | { 15 | MessageReceiver receiver = new(); 16 | yield return receiver; 17 | 18 | src.Send("请输入第一个数"); 19 | yield return null; 20 | int a = int.Parse(receiver.Message.PlainText); 21 | 22 | src.Send("请输入第二个数"); 23 | yield return null; 24 | int b = int.Parse(receiver.Message.PlainText); 25 | 26 | // yield return "请输入第一个数"; 27 | // int a = int.Parse(receiver.Message.PlainText); 28 | // 29 | // yield return "请输入第二个数"; 30 | // int b = int.Parse(receiver.Message.PlainText); 31 | 32 | src.Send($"结果是 {a + b}"); 33 | } 34 | 35 | // <= 加法 36 | // => 请输入第一个数 37 | // <= 1 38 | // => 请输入第二个数 39 | // <= 2 40 | // => 结果是 3 41 | ``` -------------------------------------------------------------------------------- /Docs/Manual/Permission.md: -------------------------------------------------------------------------------- 1 | # 权限管理 2 | 3 | Cocoa Framework 采了基于身份的权限管理机制。 4 | 5 |
6 | 7 | ## 用户身份 8 | 每个用户可同时拥有多个身份,在程序内使用 enum UserIdentity 记录。UserIdentity 带有 Flags 特性,意味着您可以使用按位或运算符(|)叠加多个身份。 9 | 10 |
11 | 12 | ## 身份授予和检验 13 | BotAuth 提供了身份相关的功能,这些是较为常用的方法: 14 | - public static UserIdentity GetIdentity(long qqId); 15 | > 获取用户身份 16 | - public static void SetIdentity(long qqId, UserIdentity identity); 17 | > 设置用户身份 18 | - public static UserIdentity AddIdentity(long qqId, UserIdentity identity); 19 | > 追加用户身份 20 | - public static UserIdentity RemoveIdentity(long qqId, UserIdentity identity); 21 | > 移除用户身份 22 | 23 |
24 | 25 | ## 群成员身份 26 | enum GroupPermission 记录用户的群成员身份,包括成员(MEMBER)、管理员(ADMINISTRATOR)和群主(OWNER)。可以从 MessageSource.Permission 获取。 27 | 28 |
29 | 30 | ## Cocoa Framework 内置的身份校验功能 31 | 可以在 Module 对应的类和消息处理方法前添加 IdentityRequirementsAttribute 特性以限制对相关功能的访问。 32 | ```C# 33 | using Maila.Cocoa.Framework; 34 | using Maila.Cocoa.Beans.Models; 35 | 36 | // 仅 Owner 可使用 RunA 和 RunB 37 | [BotModule] 38 | [IdentityRequirements(UserIdentity.Owner)] 39 | public class Demo1 : BotModuleBase 40 | { 41 | [TextRoute("a")] 42 | public static void RunA() 43 | { 44 | // ... 45 | } 46 | [TextRoute("b")] 47 | public static void RunB() 48 | { 49 | // ... 50 | } 51 | } 52 | 53 | [BotModule] 54 | public class Demo2 : BotModuleBase 55 | { 56 | // 所有人均可使用 57 | [TextRoute("a")] 58 | public static void RunA() 59 | { 60 | // ... 61 | } 62 | 63 | // 仅 Owner 可使用 64 | [TextRoute("b")] 65 | [IdentityRequirements(UserIdentity.Owner)] 66 | public static void RunB() 67 | { 68 | // ... 69 | } 70 | 71 | // 仅 Owner 和 Admin 可使用 72 | [TextRoute("c")] 73 | [IdentityRequirements(UserIdentity.Owner)] 74 | [IdentityRequirements(UserIdentity.Admin)] 75 | public static void RunC() 76 | { 77 | // ... 78 | } 79 | 80 | // 仅同时为 Owner 和 Admin 的用户可使用 81 | [TextRoute("d")] 82 | [IdentityRequirements(UserIdentity.Owner | UserIdentity.Admin)] 83 | public static void RunD() 84 | { 85 | // ... 86 | } 87 | 88 | // 仅群管理员及以上(群主)可使用 89 | [TextRoute("e")] 90 | [IdentityRequirements(GroupPermission.ADMINISTRATOR)] 91 | public static void RunE() 92 | { 93 | // ... 94 | } 95 | } 96 | ``` -------------------------------------------------------------------------------- /Docs/Manual/Route.md: -------------------------------------------------------------------------------- 1 | # 路由 2 | 3 | 路由是一种方便消息分类的机制,通过给定路由条件,实现自动解析和自动调用,相比于手动解析更为便捷。 4 | 5 |
6 | 7 | ## 特性 8 | 特性用于标记入口,需添加于入口方法前 9 | - TextRouteAttribute 10 | > 文本路由,如消息与提供的文本一致,则进行调用 11 | - RegexRouteAttribute 12 | > 正则路由,如消息符合正则表达式,则进行调用,并自动将匹配到的组填充到同名参数中 13 | - GroupNameAttribute 14 | > 用于指定正则路由填充时参数对应的组名 15 | 16 |
17 | 18 | ## 入口方法 19 | 20 | - 参数 21 | - 参数可以任意填写,除下述情况的参数都将被传入默认值或 null 22 | - 第一个类型为 MessageSource 的参数将被传入消息的来源 23 | - 第一个类型为 QMessage 的参数将被传入消息的内容 24 | - 参数名为正则表达式中的组名且类型为 string 的参数将被传入该组匹配到的字符串,如果该组会进行多次匹配(如 (?\abc)+)则会传入匹配到的最后一个字符串。在 TextRoute 中无效 25 | - 参数名为正则表达式中的组名且类型为 string[] 或 List\ 的参数将被传入该组匹配到的全部字符串。在 TextRoute 中无效 26 | - 类型为 UserAutoData、GroupAutoData、SourceAutoData 的参数将根据消息来源提供对应的数据。详见 [AutoData](./AutoData.md) 27 | - 类型为 MessageInfo 和 AsyncMeeting 时,将被传入对应的实例 28 | 29 | - 返回值 30 | - 入口方法的返回值可以是任意类型 31 | - 如果为 void 表示一旦被调用就代表消息被处理 32 | - 如果为 bool 类型表示消息是否被处理 33 | - 如果为 string、StringBuilder 或 MessageBuilder 类型且不为空将自动向来源发送对应文本,否则表示消息未被处理 34 | - 如果为 IEnumerator 或 IEnumerable 会被自动添加为 [Meeting](./Meeting.md) 35 | - 如果为 Task,则调用时消息会被立即标记为处理。如果 Task 存在返回值,返回值会在 Task 完成后按照其余规则进行处理 36 | - 如果为其他值类型,返回结果不为默认值将代表消息被处理 37 | - 如果为其他引用类型,返回结果不为 null 将代表消息被处理 38 | 39 |
40 | 41 | ## 示例 42 | ```C# 43 | using Maila.Cocoa.Framework; 44 | 45 | [BotModule] 46 | public class Demo : BotModuleBase 47 | { 48 | // 收到“test1”时回复“ok” 49 | [TextRoute("test1")] 50 | public static void Run1(MessageSource src) 51 | { 52 | src.Send("ok"); 53 | } 54 | 55 | // 收到“test2”时回复“ok” 56 | [TextRoute("test2")] 57 | public static string Run2() 58 | { 59 | return "ok"; 60 | } 61 | 62 | // 收到“你好abc”时回复“我不叫abc” 63 | [RegexRoute("你好(?.+)")] 64 | public static string Run3(string name) 65 | { 66 | return "我不叫" + name; 67 | } 68 | 69 | // 收到“你好abc”时回复“我不叫abc”的另一种实现方式 70 | [RegexRoute("你好(?.+)")] 71 | public static string Run3([GroupName("name")] string wrongName) 72 | { 73 | return "我不叫" + wrongName; 74 | } 75 | } 76 | 77 | ``` -------------------------------------------------------------------------------- /Docs/Samples/Blacklist.md: -------------------------------------------------------------------------------- 1 | # 黑名单 2 | 3 | ## Middleware 4 | ```C# 5 | using System; 6 | using System.Collections.Generic; 7 | using Maila.Cocoa.Framework; 8 | 9 | public class Blacklist : BotMiddlewareBase 10 | { 11 | [Hosting] 12 | public static List blacklist = new(); 13 | 14 | protected override void OnMessage(MessageSource src, QMessage msg, Action next) 15 | { 16 | if (!blacklist.Contains(src.User.Id)) 17 | { 18 | next(src, msg); 19 | } 20 | } 21 | } 22 | ``` 23 | 24 |
25 | 26 | ## 启动代码 27 | ```C# 28 | /*BotStartupConfig*/ config.AddMiddleware(); 29 | ``` 30 | 31 |
32 | 33 | ## 将 12345678 添加到黑名单 34 | ```C# 35 | Blacklist.blacklist.Add(12345678); 36 | ``` -------------------------------------------------------------------------------- /Docs/Samples/Cocode.md: -------------------------------------------------------------------------------- 1 | # Cocode 2 | 3 | 通过文本表达非文本内容,类似于 CQCode / MiraiCode,但比它们更简单 4 | 5 | 示例: 6 | `@123` => `[艾特123]` 7 | `Hello, @123!` => `Hello, [艾特123]!` 8 | `@+` => `[艾特全体成员]` 9 | `@@` => `@` 10 | `#i./img.png:` => `[图片,路径为"./img.png"]` 11 | `#f123` => `[表情,id为123]` 12 | `##` => `#` 13 | 14 | ## Middleware 15 | ```C# 16 | using Maila.Cocoa.Beans.API; 17 | using Maila.Cocoa.Beans.Models.Messages; 18 | using Maila.Cocoa.Framework; 19 | using Maila.Cocoa.Framework.Support; 20 | using System.Collections.Generic; 21 | using System.Text; 22 | 23 | public class Cocode : BotMiddlewareBase 24 | { 25 | protected override bool OnSendMessage(ref long id, ref bool isGroup, ref IMessage[] chain, ref int? quote) 26 | { 27 | if (chain.Length != 1 || chain[0] is not PlainMessage msg) 28 | { 29 | return true; 30 | } 31 | 32 | List newChain = new(); 33 | StringBuilder sb = new(); 34 | for (int i = 0; i < msg.Text.Length; i++) 35 | { 36 | switch (msg.Text[i]) 37 | { 38 | case '@': 39 | if (i + 1 == msg.Text.Length || msg.Text[i + 1] == '@') 40 | { 41 | sb.Append('@'); 42 | i++; 43 | continue; 44 | } 45 | if (msg.Text[i + 1] == '+') 46 | { 47 | if (sb.Length > 0) 48 | { 49 | newChain.Add(new PlainMessage(sb.ToString())); 50 | sb.Clear(); 51 | } 52 | newChain.Add(AtAllMessage.Instance); 53 | i++; 54 | continue; 55 | } 56 | if (msg.Text[i + 1] is >= '0' and <= '9') 57 | { 58 | if (sb.Length > 0) 59 | { 60 | newChain.Add(new PlainMessage(sb.ToString())); 61 | sb.Clear(); 62 | } 63 | for (i++; i < msg.Text.Length && msg.Text[i] is >= '0' and <= '9'; i++) 64 | { 65 | sb.Append(msg.Text[i]); 66 | } 67 | newChain.Add(new AtMessage(long.Parse(sb.ToString()))); 68 | sb.Clear(); 69 | i--; 70 | } 71 | else 72 | { 73 | sb.Append('@'); 74 | } 75 | break; 76 | case '#': 77 | if (i + 1 == msg.Text.Length || msg.Text[i + 1] == '#') 78 | { 79 | sb.Append('#'); 80 | i++; 81 | continue; 82 | } 83 | if (msg.Text[i + 1] == 'f') 84 | { 85 | if (sb.Length > 0) 86 | { 87 | newChain.Add(new PlainMessage(sb.ToString())); 88 | sb.Clear(); 89 | } 90 | for (i += 2; i < msg.Text.Length && msg.Text[i] is >= '0' and <= '9'; i++) 91 | { 92 | sb.Append(msg.Text[i]); 93 | } 94 | newChain.Add(new FaceMessage(int.Parse(sb.ToString()))); 95 | sb.Clear(); 96 | i--; 97 | continue; 98 | } 99 | if (msg.Text[i + 1] == 'i') 100 | { 101 | if (sb.Length > 0) 102 | { 103 | newChain.Add(new PlainMessage(sb.ToString())); 104 | sb.Clear(); 105 | } 106 | for (i += 2; i < msg.Text.Length && msg.Text[i] != ':'; i++) 107 | { 108 | sb.Append(msg.Text[i]); 109 | } 110 | var image = BotAPI.UploadImage(isGroup ? UploadType.Group : 111 | (BotInfo.HasFriend(id) ? UploadType.Friend : UploadType.Temp), 112 | sb.ToString()).Result; 113 | newChain.Add(image); 114 | sb.Clear(); 115 | } 116 | else 117 | { 118 | sb.Append('#'); 119 | } 120 | break; 121 | default: 122 | sb.Append(msg.Text[i]); 123 | break; 124 | } 125 | } 126 | 127 | if (sb.Length > 0) 128 | { 129 | newChain.Add(new PlainMessage(sb.ToString())); 130 | } 131 | chain = newChain.ToArray(); 132 | return true; 133 | } 134 | } 135 | 136 | ``` 137 | 138 |
139 | 140 | ## 启动代码 141 | ```C# 142 | /*BotStartupConfig*/ config.AddMiddleware(); 143 | ``` 144 | 145 |
146 | 147 | ## 逻辑代码 148 | ```C# 149 | [TextRoute("hello cocoa")] 150 | public static string Run(MessageSource src) 151 | => $"Hello, @{src.User.Id}#f21"; 152 | ``` -------------------------------------------------------------------------------- /Docs/Samples/Repeater.md: -------------------------------------------------------------------------------- 1 | # 复读机 2 | 3 | ## 重复全部消息 4 | ```C# 5 | using Maila.Cocoa.Framework; 6 | 7 | [BotModule("Repeater")] 8 | public class Repeater : BotModuleBase 9 | { 10 | protected override bool OnMessage(MessageSource src, QMessage msg) 11 | { 12 | src.Send(msg.PlainText); 13 | return true; 14 | } 15 | } 16 | 17 | // <= abc 18 | // => abc 19 | ``` 20 | 21 |
22 | 23 | ## 实现 echo 指令 24 | ```C# 25 | using Maila.Cocoa.Framework; 26 | 27 | [BotModule("Repeater")] 28 | public class Repeater : BotModuleBase 29 | { 30 | // 手动发送: 31 | [RegexRoute("^/echo (?.+)")] 32 | public static void Echo(MessageSource src, string content) 33 | { 34 | src.Send(content); 35 | } 36 | 37 | // 自动发送: 38 | // [RegexRoute("^/echo (?.+)")] 39 | // public static string Echo(string content) 40 | // { 41 | // return content; 42 | // } 43 | 44 | // 简化版自动发送: 45 | // [RegexRoute("^/echo (?.+)")] 46 | // public static string Echo(string content) => content; 47 | } 48 | 49 | // <= /echo abcd 50 | // => abcd 51 | ``` -------------------------------------------------------------------------------- /Docs/Tutorial/CreateModule.md: -------------------------------------------------------------------------------- 1 | # 创建 Module 2 | 3 | 通过本教程,您将学会如何创建一个 Module 4 | 5 |
6 | 7 | 1. 创建类 8 | ```C# 9 | public class Demo 10 | { 11 | } 12 | ``` 13 | 14 |
15 | 16 | 2. 继承父类 17 | ```C# 18 | using Maila.Cocoa.Framework; 19 | 20 | public class Demo : BotModuleBase 21 | { 22 | } 23 | ``` 24 | 25 |
26 | 27 | 3. 添加特性 28 | ```C# 29 | using Maila.Cocoa.Framework; 30 | 31 | [BotModule] 32 | public class Demo : BotModuleBase 33 | { 34 | } 35 | ``` 36 | 37 |
38 | 39 | 至此,一个不包含功能但会被正常加载的 Module 创建完成。 40 | 下一篇:[使用路由](./Route.md) -------------------------------------------------------------------------------- /Docs/Tutorial/Hellococoa.md: -------------------------------------------------------------------------------- 1 | # 你好,Cocoa! 2 | 3 | 通过本教程,您将学会如何通过 Cocoa Framework 实现最简单的应答机器人 4 | 5 |
6 | 7 | ## 在此之前 8 | - [启动 Mirai](https://github.com/mamoe/mirai/blob/dev/docs/UserManual.md) 并 [安装 mirai-api-http](https://github.com/mamoe/mirai/blob/dev/docs/UserManual.md#%E5%A6%82%E4%BD%95%E5%AE%89%E8%A3%85%E5%AE%98%E6%96%B9%E6%8F%92%E4%BB%B6),如果使用的是 mirai-api-http 2.x 版本需手动启用 WebSocket 9 | - 安装 [Visual Studio](https://visualstudio.microsoft.com/zh-hans/) 或其他 IDE 10 | 11 |
12 | 13 | ## 新建项目 14 | 1. 启动 Visual Studio,创建新项目 15 | 2. 选择控制台应用程序,继续 16 | 3. 输入项目名,选择项目存放位置,继续 17 | 4. 目标框架选择 .NET 6.0(一般已默认选择),创建 18 | 19 |
20 | 21 | ## 添加引用 22 | 1. 在创建的项目上右键,点击“管理 Nuget 包” 23 | 2. 进入“浏览”选项卡,搜索 Maila.Cocoa.Framework 或 maila 24 | 3. 选中搜索结果中的 Maila.Cocoa.Framework,点击右侧界面中的“安装” 25 | 26 |
27 | 28 | ## 编写启动代码 29 | 将默认创建的 Program.cs 中的全部代码删除,替换为以下代码 30 | ```C# 31 | using System; 32 | using Maila.Cocoa.Framework; 33 | 34 | BotStartupConfig config = new("YourVerifyKey", 12345678); // 启动配置,请将 YourVerifyKey 改为您的 VerifyKey,12345678 改为机器人的 QQ 号 35 | var succeed = await BotStartup.ConnectAndInit(config); // 连接 Mirai 并初始化 36 | if (succeed) // 如果连接成功 37 | { 38 | Console.WriteLine("Startup OK"); // 提示连接成功 39 | while (Console.ReadLine() != "exit"); // 在用户往控制台输入“exit”前持续运行 40 | await BotStartup.DisconnectAndSaveData(); // 断开连接 41 | } 42 | else // 否则 43 | { 44 | Console.WriteLine("Failed"); // 提示连接失败 45 | } 46 | ``` 47 | 48 |
49 | 50 | ## 实现简单应答 51 | 新建 Hello.cs 文件,并输入以下代码 52 | ```C# 53 | using Maila.Cocoa.Framework; 54 | 55 | [BotModule] 56 | public class Hello : BotModuleBase 57 | { 58 | [TextRoute("hello cocoa")] // 收到“hello cocoa”时调用此方法 59 | public static void Run(MessageSource src) 60 | { 61 | src.Send("Hi!"); // 向消息来源发送“Hi!” 62 | } 63 | } 64 | ``` 65 | 66 |
67 | 68 | ## 完成 69 | 运行程序,进入 QQ 测试功能 70 | 71 |
72 | 73 | ## 备注 74 | 如果希望主动发送消息,例如在启动时向管理员发送通知,可以通过 BotAPI.SendFriendMessage 或 BotAPI.SendGroupMessage 实现。其他与机器人相关的 API 也位于 BotAPI 类中。 75 | ```C# 76 | BotAPI.SendFriendMessage(123456, new PlainMessage("message")); 77 | ``` 78 | -------------------------------------------------------------------------------- /Docs/Tutorial/Overview.md: -------------------------------------------------------------------------------- 1 | # Cocoa Framewrok 概述 2 | 3 | 本教程将介绍 Cocoa Framework 的一些基础概念。 4 | 5 |
6 | 7 | ## 消息的组成 8 | 一条消息由来源和内容组成。来源是指发送者的 QQ 号、是否为群聊、群号等信息,它们被封装在 MessageSource 类中。内容被封装在 QMessage 类中,通过消息链表示。由于来源和内容被分开存储,因此在某些时候可以只获取来源或者消息。 9 | 10 |
11 | 12 | ## 消息链 13 | QQ 消息经常出现文字和表情、图片等非文本内容存在于同一气泡内的情况,因此使用 IMessage 数组表示消息的内容和顺序。例如一条文本+表情+文本的消息用消息链表示就是 { PlainMessage, FaceMessage, PlainMessage }。需要注意的是,mirai 原始消息链的第一个项是 SourceMessage,而 QMessage 会自动解析 SourceMessage 中包含的信息,并作为属性提供。因此 QMessage 中的消息链不包含 SourceMessage。 14 | 15 |
16 | 17 | ## Middleware 18 | Cocoa Framework 接收到消息后,会先交由 Middleware 进行预处理。此时 Middleware 可以截断消息,也可以更改消息的发送者和内容。同时发送消息前也会交由 Middleware 进行预处理,此时 Middleware 可以取消发送,也可以更改发送的接收者和内容。 19 | 20 |
21 | 22 | ## Module 23 | 消息由 Middleware 处理完后会交由 Module 处理。Module 是机器人具体功能的载体,推荐使用路由进行处理。消息处理完后也会将消息、原始消息、处理情况再次交由 Module 进行后处理。 24 | 25 |
26 | 27 | ## 路由 28 | 路由可以自动匹配消息内容、进行简单的消息解析并调用相关处理方法,包含 TextRoute 和 RegexRoute 两种,区别在于前者为普通的文字匹配,后者为基于正则表达式的文本匹配。同时路由还提供自动参数匹配、自动数据等特色功能。 -------------------------------------------------------------------------------- /Docs/Tutorial/Route.md: -------------------------------------------------------------------------------- 1 | # 使用路由 2 | 3 | 路由可以自动匹配接收到的消息并调用您指定的方法。通过本教程,您将学会如何使用路由 4 | 5 |
6 | 7 | ## 创建最基础的路由 8 | 1. 路由仅在 Module 中可用,因此你需要先创建一个 Module 9 | ```C# 10 | using Maila.Cocoa.Framework; 11 | 12 | [BotModule] 13 | public class Demo : BotModuleBase 14 | { 15 | } 16 | ``` 17 | 18 |
19 | 20 | 2. 一个路由包含特性和入口方法 21 | ```C# 22 | using Maila.Cocoa.Framework; 23 | 24 | [BotModule] 25 | public class Demo : BotModuleBase 26 | { 27 | [TextRoute("hello")] // 特性 28 | public void Hello() // 入口方法 29 | { 30 | } 31 | } 32 | ``` 33 | TextRoute 是文本路由。在本例中,如果机器人收到 "hello" 将会调用 Hello() 方法。同时还有 RegexRoute,当机器人收到符合提供的正则表达式的消息时将会调用相应的入口方法。 34 | 35 |
36 | 37 | 3. 参数列表 38 | ```C# 39 | using Maila.Cocoa.Framework; 40 | 41 | [BotModule] 42 | public class Demo : BotModuleBase 43 | { 44 | [TextRoute("hello")] 45 | public void Hello(MessageSource src) // 参数列表 46 | { 47 | } 48 | } 49 | ``` 50 | 您可以按照自己的喜好添加参数。参数的顺序可以随意更换,Cocoa Framework 会自动按照参数的类型和名字传入您需要的内容。 51 | MessageSource 包含消息来源,您可以借助它轻松地回复消息。QMessage 包含消息的具体内容。对于其他支持的类型,请参考 [路由](../Manual/Route.md#入口方法)。 52 | 53 |
54 | 55 | 4. 可选的返回值 56 | ```C# 57 | using Maila.Cocoa.Framework; 58 | 59 | [BotModule] 60 | public class Demo : BotModuleBase 61 | { 62 | // 手动发送消息 63 | // [TextRoute("hello")] 64 | // public void Hello(MessageSource src) 65 | // { 66 | // src.Send("Hello!"); 67 | // } 68 | 69 | // 通过返回值发送消息 70 | [TextRoute("hello")] 71 | public string Hello(MessageSource src) 72 | { 73 | return "Hello!"; 74 | } 75 | } 76 | ``` 77 | 您可以通过返回值来发送消息。如果您不想发送消息,可以返回 null。当然,Cocoa Framework 也支持其他的返回值类型,请参考 [路由](../Manual/Route.md#入口方法)。 78 | 79 |
80 | 81 | 5. RegexRoute 的参数自动匹配 82 | ```C# 83 | using Maila.Cocoa.Framework; 84 | 85 | [BotModule] 86 | public class Demo : BotModuleBase 87 | { 88 | [DisableInGroup] 89 | [RegexRoute("^hello (?[a-zA-Z]+)$")] 90 | public string Hello(MessageSource src, string name) 91 | { 92 | if (name == "cocoa") 93 | { 94 | return "Hello!"; 95 | } 96 | else 97 | { 98 | return $"My name is cocoa, not {name}!"; 99 | } 100 | } 101 | } 102 | ``` 103 | RegexRoute 可以根据正则表达式中的组名将对应的内容传入同名参数。在此处的例子中,如果消息内容为 "hello cocoa",则会将 "cocoa" 传入 name 参数,此时机器人将回复 "Hello!"。但如果消息内容为 "hello coco",机器人将会生气地回复 "My name is cocoa, not coco!"。 104 | 你应该已经注意到了,这里我额外添加了一个 DisableInGroup 特性,它将禁止此功能在群聊中使用,以防被意外调用。当然,类似的也有 DisableInPrivate 特性禁止在私聊中使用、Disabled 特性禁止在一切时候使用。Disabled 特性同时还能用于模块、重写方法、托管字段和一些属性。 -------------------------------------------------------------------------------- /Docs/Whatsnew/NewFeatures.md: -------------------------------------------------------------------------------- 1 | # Cocoa Framework 2 中的新特性 2 | 3 | ## 基于身份的权限管理系统 4 | --- 5 | 可以为用户指定身份,然后通过身份管理用户对功能的访问。详见 [权限管理](../Manual/Permission.md) 6 | 7 |
8 | 9 | ## 开放事件侦听接口 10 | --- 11 | 允许添加对消息事件(群聊消息、好友消息、临时消息)以外事件的处理 12 | 13 |
14 | 15 | ## 新增特性(Attribute) 16 | --- 17 | - IdentityRequirementsAttribute 18 | > 用于指定身份要求,可以重复添加。每个要求的判断标准为“全部”,即用户要拥有所要求的全部身份;要求间的关系为“任意”,即满足任意要求即可。详见 [权限管理](../Manual/Permission.md) 19 | > 可用于类和方法,仅在 Module 类、路由方法和 OnMessage 方法中有效 20 | - DisableInGroupAttribute 和 DisableInPrivateAttribute 21 | > 用于禁止指定的功能在群或私聊(包括好友消息和临时消息)环境下使用 22 | > 可用于类和方法,仅在 Module 类、路由方法和 OnMessage 方法中有效 23 | - GroupNameAttribute 24 | > 用于指定变量所映射的组。详见 [路由](../Manual/Route.md) 25 | > 可用于参数,仅在正则路由方法的参数中有效 26 | 27 |
28 | 29 | ## 数据托管逻辑更改为同步 30 | --- 31 | 被托管的字段会对应于一个数据文件,之前在程序运行过程中对此数据文件的直接修改都会被覆盖,现在则会将手动修改的内容与字段的数据进行合并。但除了修改静态数据,一般不推荐手动修改数据文件。详见 [数据存储](../Manual/Data.md) 32 | 33 |
34 | 35 | ## 增强 Middleware 处理 36 | --- 37 | ```C# 38 | // protected virtual bool OnMessage(ref MessageSource src, ref QMessage msg); 39 | protected virtual void OnMessage(MessageSource src, QMessage msg, Action next); 40 | ``` 41 | 这意味着 Middleware 不再受到 ref 的限制,可以使用 async/await 更为方便地进行异步处理 42 | 43 |
44 | 45 | ## AutoData 46 | --- 47 | 基于消息来源提供不同的数据,可以极大简化对简单数据的管理。详见 [AutoData](../Manual/AutoData.md) 48 | 49 |
50 | 51 | ## AsyncMeeting 52 | 用更加自然的方式实现对话。详见 [AsyncMeeting](../Manual/AsyncMeeting.md) 53 | -------------------------------------------------------------------------------- /Docs/Whatsnew/UpdateLog.md: -------------------------------------------------------------------------------- 1 |

更新日志

2 | 3 | # **2.1.8.3** 4 | *released October 3, 2023* 5 | 6 | ## 新特性 7 | - 支持自定义 Route 8 | 9 | ## 修复 10 | - 更新依赖,以修复 OOB write 漏洞(CVE-2023-4863) 11 | 12 |
13 | 14 | # **2.1.8.2** 15 | *released March 5, 2023* 16 | 17 | ## 修复 18 | - 修复 DataManager 有时不会保存数据的问题 19 | 20 |
21 | 22 | # **2.1.8.1** 23 | *released January 14, 2023* 24 | 25 | ## 优化 26 | - 性能优化 27 | 28 |
29 | 30 | # **2.1.8** 31 | *released December 25, 2022* 32 | 33 | ## 修复 34 | - 修复 MemberCardChangeEvent 无法接收的问题 35 | 36 | ## 优化 37 | - 允许将 null QMessage 转换为 string?(仍然为 null) 38 | 39 | ## 新特性 40 | - 新增 AsyncMeeting,通过异步更为直观地实现对话。使用 async/await 在任何地方获取收到的消息 41 | - 支持接收陌生人消息 42 | - 支持收发 MiraiCodeMessage 43 | - Route 参数列表支持 MessageInfo 和 AsyncMeeting。返回值支持 MessageBuilder 和 Task\<> 44 | 45 |
46 | 47 | # **2.1.7** 48 | *released November 19, 2022* 49 | 50 | ## 修复 51 | - 解决 AtAllMessage 和 MusicShareMessage 无法发送的问题 52 | 53 | ## 变更 54 | - 更新目标框架至 .NET 6.0 55 | 56 | ## 新特性 57 | - 新增 MessageBuilder 类,用于更方便地构建消息链 58 | 59 |
60 | 61 | # **2.1.6.6** 62 | *released October 31, 2022* 63 | 64 | ## 优化 65 | - 发送图片功能支持 Linux 平台 66 | 67 |
68 | 69 | # **2.1.6.5** 70 | *released October 26, 2022* 71 | 72 | ## 修复 73 | - 更新依赖,以修复远程代码执行漏洞(CVE-2021-26701) 74 | 75 |
76 | 77 | # **2.1.6.4** 78 | *released June 8, 2022* 79 | 80 | ## 优化 81 | - 代码质量与性能提升 82 | 83 | ## 变更 84 | - DataManager.DataPath 更名为 DataManager.DataRoot 85 | 86 |
87 | 88 | # **2.1.6.3** 89 | *released May 31, 2022* 90 | 91 | ## 优化 92 | - 进一步优化 nullable 静态分析 93 | 94 |
95 | 96 | # **2.1.6.2** 97 | *released May 31, 2022* 98 | 99 | ## 优化 100 | - 优化 nullable 静态分析 101 | 102 |
103 | 104 | # **2.1.6.1** 105 | *released May 7, 2022* 106 | 107 | ## 修复 108 | - 修复使用新版 mirai 时无法接收 FileMessage 的问题 109 | 110 |
111 | 112 | # **2.1.6** 113 | *released March 26, 2022* 114 | 115 | ## 变更 116 | - 重命名及拆分部分方法 117 | - 移除 BotAPI 中用于事件绑定的属性 118 | 119 | ## 新特性 120 | - 新增 BotEventHandler,用于处理事件 121 | 122 |
123 | 124 | # **2.1.5.5** 125 | *released March 22, 2022* 126 | 127 | ## 修复 128 | - 修复使用新版 mirai 时无法接收 NudgeEvent 的问题 129 | 130 |
131 | 132 | # **2.1.5.4** 133 | *released March 20, 2022* 134 | 135 | ## 新特性 136 | - AutoData 新增 SharedFrom 特性,用于在 Module 间共享数据 137 | 138 |
139 | 140 | # **2.1.5.3** 141 | *released March 4, 2022* 142 | 143 | ## 修复 144 | - 修复获取群成员列表时可能出现下标越界的问题 145 | 146 |
147 | 148 | # **2.1.5.2** 149 | *released December 28, 2021* 150 | 151 | ## 修复 152 | - Fixed a bug that might take up a lot of disk IO.(问就是不会说中文了 153 | 154 |
155 | 156 | # **2.1.5.1** 157 | *released December 10, 2021* 158 | 159 | ## 变动 160 | - 优化数据存储逻辑,不包含实际数据的文件将被自动移除,以优化 data 文件夹的空间占用 161 | 162 | ## 新特性 163 | - BotInfo 新增 GetGroupList 和 GetFriendList 方法,用于获取群列表和好友列表 164 | 165 |
166 | 167 | # **2.1.5** 168 | *released November 21, 2021* 169 | 170 | ## 变动 171 | - 移除 AtRoute 172 | 173 | ## 新特性 174 | - RegexRoute 和 TextRoute 追加 AtRequired 属性,用于替代 AtRoute 175 | - QGroup 新增 GetMemberList 方法和群设置相关属性 176 | - QMessage 新增 Recall 和 SetEssence 方法 177 | 178 |
179 | 180 | # **2.1.4.4** 181 | *released November 18, 2021* 182 | 183 | ## 修复 184 | - 修复无法获取群设置的问题 185 | - 修复无法将禁言时长设置为三天以上的问题 186 | 187 |
188 | 189 | # **2.1.4.3** 190 | *released November 14, 2021* 191 | 192 | ## 修复 193 | - 修复无法通过 AtRoute 捕获单个 At 的问题 194 | 195 |
196 | 197 | # **2.1.4.2** 198 | *released October 27, 2021* 199 | 200 | ## 修复 201 | - 修复无法接收和发送转发消息的问题 202 | 203 |
204 | 205 | # **2.1.4.1** 206 | *released October 18, 2021* 207 | 208 | ## 修复 209 | - 修复语音无法发送的问题 210 | 211 |
212 | 213 | # **2.1.4** 214 | *released October 11, 2021* 215 | 216 | ## 新特性 217 | - 新增 AtRoute 218 | - Middleware 新增 OnSendMessage 219 | - BotInfo 支持自动刷新 220 | 221 |
222 | 223 | # **2.1.3.3** 224 | *released September 1, 2021* 225 | 226 | ## 变动 227 | - 调整 ListeningTarget 的判定逻辑 228 | 229 |
230 | 231 | # **2.1.3.2** 232 | *released August 10, 2021* 233 | 234 | ## 修复 235 | - 修复 Message Lock 执行完成后不会结束处理的问题 236 | 237 | ## 变动 238 | - 异步路由和 Module 中的异步覆写方法会被认为是线程安全的,即使没有添加 ThreadSafe 特性 239 | 240 |
241 | 242 | # **2.1.3.1** 243 | *released August 1, 2021* 244 | 245 | ## 修复 246 | - 修复 GroupAutoData 无法读取和保存的问题 247 | 248 |
249 | 250 | # **2.1.3** 251 | *released August 1, 2021* 252 | 253 | ## 请注意! 254 | 此版本对数据存储路径中标识符的计算方式进行了更改,以解决更新后数据丢失的问题。部分数据需要手动进行调整。 255 | 256 | 涉及到的内容: 257 | - `data/ModuleData` 文件夹 258 | - `data/MiddlewareData` 文件夹 259 | - `data/ModuleData/*/UserAutoData.json` 文件 260 | - `data/ModuleData/*/GroupAutoData.json` 文件 261 | - `data/ModuleData/*/SourceAutoData.json` 文件 262 | 263 | 例外: 264 | - 未使用 AutoData 功能的 Module 可以忽略以 AutoData.json 结尾的文件 265 | - 未使用数据托管和 AutoData 功能的 Module 和 Middleware 可以忽略对应的数据文件夹 266 | 267 | 推荐的调整方式: 268 | - 文件夹 269 | 1. 备份并删除相关文件夹 270 | 2. 更新 Cocoa Framework 并至少启动一次机器人程序 271 | 3. 检查文件夹,会发现文件夹名的后缀相较之前出现变化 272 | 4. 将旧文件夹中的文件移动到对应的新文件夹中 273 | 5. 如果您使用过 2.1.2 版本或 2.1.2.1 版本,之前的 Middleware 数据会被错误地放置到 ModuleData 文件夹中。当前版本已经修复了这个问题,但您需要手动把它们恢复到 MiddlewareData 文件夹中。 274 | - 文件 275 | 1. 有些文件内容为空(仅包含一对大括号),这些文件您可以直接忽略 276 | 2. 包含数据的文件需要您自行比较和修改,替换工具可以减轻一部分工作量 277 | 278 | ## 修复 279 | - 修复 Middleware 的数据被放置到 ModuleData 文件夹中的问题 280 | 281 | ## 变动 282 | - 发送消息相关方法的返回值和部分参数更改为不可为空 283 | 284 | ## 新特性 285 | - ListeningTarget 支持自定义监听目标 286 | 287 |
288 | 289 | # **2.1.2.1** 290 | *released July 26, 2021* 291 | 292 | ## 修复 293 | - 修复包含 string[] 和 List\ 类型参数的 RegexRoute 无法被调用的问题 294 | 295 |
296 | 297 | # **2.1.2** 298 | *released July 25, 2021* 299 | 300 | ## 新特性 301 | 302 | - 新增 AutoData,用于简化对简单数据的管理 303 | 304 |
305 | 306 | # **2.1.1** 307 | *released July 24, 2021* 308 | 309 | ## 变动 310 | 311 | - Meeting 中修改超时的方式由返回 TimeSpan 更改为 MeetingTimeout 312 | - 优化 Route 在返回 void 时的执行逻辑 313 | 314 | ## 新特性 315 | 316 | - 新增 AsyncTask,用于在 Meeting 中执行异步任务 317 | 318 |
319 | 320 | # **2.1.0** 321 | *released July 23, 2021* 322 | 323 | ## 修复 324 | 325 | - 解决 Middleware 无法更改消息内容的一系列相关问题 326 | 327 | ## 变动 328 | 329 | - QMessage 中的 Message 类全部更改为 IMessage 接口,涉及构造函数和成员: 330 | - QMessage(**IMessage**[] chain) 331 | - ImmutableArray<**IMessage**> Chain 332 | - T[] GetSubMessages\() where T : **IMessage** 333 | 334 | ## 新特性 335 | 336 | - 添加对 mirai-api-http 2.x 版本的支持 337 | 338 | 339 |
340 | 341 | # **2.0.0** 342 | *released July 14, 2021* 343 | 344 | ## 变动 345 | 346 | - 更改依赖 347 | > Mirai-CSharp => CocoaBeans 348 | - 调整命名空间规划 349 | - 移除 Service 和 Component 350 | - 移除 Module 的内置黑白名单,Module 的群聊可用性默认为可用 351 | - Middleware 改由框架进行实例化 352 | - Middleware 的 OnMessage() 方法由返回 bool 更改为调用 next() 方法 353 | - 数据托管功能逻辑由自动保存更改为同步 354 | - 异常更改为由用户处理 355 | 356 | ## 新特性 357 | 358 | - 支持添加多个程序集 359 | - 支持软件内重启 360 | - 支持匿名 Module,简化 BotModule 的构造函数 361 | - 新增基于身份的权限管理系统 362 | - 增加新特性(Attribute) 363 | - 允许正则路由的方法指定变量所映射的组 364 | - 允许添加对消息事件(群聊消息、好友消息、临时消息)以外事件的处理 -------------------------------------------------------------------------------- /Docs/index.md: -------------------------------------------------------------------------------- 1 |

Cocoa Framework 文档

2 |
3 | 4 | ## 教程 5 | - [你好,Cocoa!](./Tutorial/Hellococoa.md) 6 | - [Cocoa Framewrok 概述](./Tutorial/Overview.md) 7 | - [创建 Module](./Tutorial/CreateModule.md) 8 | - [使用路由](./Tutorial/Route.md) 9 | - 咕咕咕…… 10 | 11 |
12 | 13 | ## 案例参考 14 | - [复读机](./Samples/Repeater.md) 15 | - [黑名单](./Samples/Blacklist.md) 16 | - [Cocode](./Samples/Cocode.md) 17 | 18 |
19 | 20 | ## 新增功能与变化 21 | - [Cocoa Framework 2 中的新特性](./Whatsnew/NewFeatures.md) 22 | - [更新日志](./Whatsnew/UpdateLog.md) 23 | 24 |
25 | 26 | ## 用户手册 27 | - [Cocoa Framework API](./Manual/API.md) 28 | - [路由](./Manual/Route.md) 29 | - [自定义路由](./Manual/CustomRoute.md) 30 | - [AutoData](./Manual/AutoData.md) 31 | - [Meeting](./Manual/Meeting.md) 32 | - [AsyncMeeting](./Manual/AsyncMeeting.md) 33 | - [BotEventHandler](./Manual/BotEventHandler.md) 34 | - [数据存储](./Manual/Data.md) 35 | - [权限管理](./Manual/Permission.md) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | LOGO 3 | 4 | Cocoa Framework 2 5 |

6 | 7 |
8 | 9 | [![NuGet version (Maila.Cocoa.Framework)](https://img.shields.io/nuget/v/Maila.Cocoa.Framework.svg?style=flat)](https://www.nuget.org/packages/Maila.Cocoa.Framework/) 10 | 11 | Cocoa Framework 是一个基于 mirai 的 QQ 机器人开发框架,致力于降低 QQ 机器人的开发难度,使更多人能定制属于自己的 QQ 机器人 12 | 13 | 项目名称来源于 14 | [Koi](https://zh.moegirl.org.cn/Koi) 15 | 作品 16 | [《请问您今天要来点兔子吗?》](https://zh.moegirl.org.cn/%E8%AF%B7%E9%97%AE%E6%82%A8%E4%BB%8A%E5%A4%A9%E8%A6%81%E6%9D%A5%E7%82%B9%E5%85%94%E5%AD%90%E5%90%97) 17 | 中的 18 | [保登心爱](https://zh.moegirl.org.cn/%E4%BF%9D%E7%99%BB%E5%BF%83%E7%88%B1) 19 | 20 |
21 |
22 | 23 | # 开始使用 24 | 25 | 尝试快速入门教程: 26 | - [你好,Cocoa!](./Docs/Tutorial/Hellococoa.md) 27 | 28 | 查看常见功能的案例: 29 | - [复读机](./Docs/Samples/Repeater.md) 30 | - [Cocode](./Docs/Samples/Cocode.md) 31 | 32 | 教程、API 文档和更多案例详见 [Cocoa Framework 文档](./Docs/index.md) 33 | 34 |
35 | 36 | # 相较于 1.x 版本的区别 37 | 38 | Cocoa Framework 2 在继承了 1.x 版本核心思路的同时进行了完全的重构,尽可能地使开发者只需考虑功能本身。同时,与 mirai-api-http 的通讯也改为使用 Cocoa Beans,便于及时跟进 mirai 的新特性。 39 | 更多新特性请参阅 [Cocoa Framework 2 中的新特性](./Docs/Whatsnew/NewFeatures.md) 和 [更新日志](./Docs/Whatsnew/UpdateLog.md) 40 | 41 |
42 | 43 | # 联系我们 44 | 您可以直接通过 Issue 向我们提供反馈。 45 | 如果希望与项目开发者和其他用户交流,欢迎加入 QQ 群(766230870) 46 | 如果希望加入我们,欢迎联系上述交流群群主。 -------------------------------------------------------------------------------- /README_nuget.md: -------------------------------------------------------------------------------- 1 | # Cocoa Framework 2 2 | 3 | Cocoa Framework 是一个基于 mirai 的 QQ 机器人开发框架,致力于降低 QQ 机器人的开发难度,使更多人能定制属于自己的 QQ 机器人 4 | 5 | 项目名称来源于 [Koi](https://zh.moegirl.org.cn/Koi) 作品 [《请问您今天要来点兔子吗?》](https://zh.moegirl.org.cn/%E8%AF%B7%E9%97%AE%E6%82%A8%E4%BB%8A%E5%A4%A9%E8%A6%81%E6%9D%A5%E7%82%B9%E5%85%94%E5%AD%90%E5%90%97) 中的 [保登心爱](https://zh.moegirl.org.cn/%E4%BF%9D%E7%99%BB%E5%BF%83%E7%88%B1) 6 | 7 | # 开始使用 8 | [Cocoa Framework 文档](https://github.com/Miyakowww/CocoaFramework2/blob/master/Docs/index.md) 提供了一些用于快速上手的教程与 API 文档,可以帮助您了解和使用 Cocoa Framework。 -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Miyakowww/CocoaFramework2/50332faa75bc726aec6a2bc96d3cdc7002b97ae2/logo.png --------------------------------------------------------------------------------