├── .gitattributes ├── .gitignore ├── .gitmodules ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── KanonBot.sln ├── LICENSE ├── QUICKSTART.md ├── README.md ├── Tests ├── Misc.cs ├── OSU.cs ├── TestFiles │ ├── ImageHelper示例文件.png │ ├── ImageHelper示例文件.txt │ ├── Kakichoco - Zan'ei (Lasse) [Illusion].osu │ ├── beatmap.json │ ├── cqmessage.json │ ├── gulidmsg.json │ ├── head.gif │ ├── info.png │ ├── ppplus.json │ ├── score.json │ ├── scoretest.png │ └── user.json ├── Tests.csproj └── Usings.cs ├── convert.sh ├── dependencies └── osu-pp │ ├── OsuPP.cs │ ├── WorkingBeatmap.cs │ ├── osu-pp.csproj │ ├── osu-pp.sln │ └── resources │ ├── 1028484.osu │ ├── 1638954.osu │ ├── 2118524.osu │ ├── 2785319.osu │ └── 657916.osu ├── res └── mail_desu_life_mailaddr_verify_template.txt └── src ├── .editorconfig ├── API ├── OSU │ ├── API.cs │ ├── Extensions.cs │ ├── Mode.cs │ └── Models │ │ ├── Beatmap.cs │ │ ├── BeatmapAttributes.cs │ │ ├── BeatmapSearch.cs │ │ ├── Beatmapset.cs │ │ ├── Leagcy.cs │ │ ├── Medal.cs │ │ ├── Mods.cs │ │ ├── PPlusData.cs │ │ ├── Score.cs │ │ ├── ScoreLazer.cs │ │ └── User.cs ├── OpenAI │ └── API.cs ├── PPYSB │ ├── API.cs │ ├── Converter.cs │ ├── Extensions.cs │ ├── Mode.cs │ └── Models │ │ ├── Beatmap.cs │ │ ├── Privileges.cs │ │ ├── Response.cs │ │ ├── Score.cs │ │ └── User.cs └── oss.cs ├── Command ├── BotCmdHelper.cs └── Universal.cs ├── Config.cs ├── Error.cs ├── Event.cs ├── FodyWeavers.xml ├── GlobalUsings.cs ├── KanonBot.csproj ├── Mail.cs ├── Message ├── AtSegment.cs ├── Chain.cs ├── EmojiSegment.cs ├── IMsgSegment.cs ├── ImageSegment.cs ├── RawSegment.cs └── TextSegment.cs ├── OsuPerformance ├── Oppai.cs ├── OsuPP.cs ├── PPInfo.cs ├── RosuPP.cs ├── SBRosuPP.cs └── UniversalCalculator.cs ├── Program.cs ├── Properties └── launchSettings.json ├── Serializer.cs ├── Utils ├── Avatar.cs ├── Beatmap.cs ├── ConcurrentDictionaryExtensions.cs ├── File.cs ├── Image.cs ├── Mail.cs ├── Math.cs ├── Random.cs ├── Reflection.cs ├── Time.cs └── Utils.cs ├── database ├── Database.cs └── Models.cs ├── drivers ├── Discord │ ├── API.cs │ ├── Driver.cs │ └── Message.cs ├── Drivers.cs ├── FakeSocket.cs ├── KOOK │ ├── API.CS │ ├── Driver.cs │ ├── Enums.cs │ ├── Message.cs │ └── Models.cs ├── OneBot │ ├── API.cs │ ├── Driver.cs │ ├── Driver │ │ ├── Client.cs │ │ └── Server.cs │ ├── Enums.cs │ ├── Message.cs │ └── Models.cs ├── QQGuild │ ├── API.cs │ ├── Driver.cs │ ├── Enums.cs │ ├── Message.cs │ └── Models.cs └── Target.cs ├── functions ├── Accounts.cs ├── Bot │ ├── cat.cs │ ├── help.cs │ ├── ping.cs │ ├── su.cs │ └── sudo.cs └── osu │ ├── GeneralUpdate.cs │ ├── badge.cs │ ├── best_performance.cs │ ├── bplist.cs │ ├── get.cs │ ├── getbg.cs │ ├── info.cs │ ├── leeway.cs │ ├── leeway_calculator.cs │ ├── ppvs.cs │ ├── recent.cs │ ├── recentlist.cs │ ├── score.cs │ ├── search.cs │ ├── seasonalpass.cs │ ├── set.cs │ ├── todaybp.cs │ └── update.cs └── image ├── LegacyDraw.cs ├── OsuInfoPanelV2.cs ├── OsuScorePanelV3.cs ├── Processor.cs └── ScoreList.cs /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "rosu-pp-ffi"] 2 | path = dependencies/rosu-pp-ffi 3 | url = https://github.com/fantasyzhjk/rosu-pp-ffi 4 | [submodule "sb-pp-ffi"] 5 | path = dependencies/sb-pp-ffi 6 | url = https://github.com/fantasyzhjk/rosu-pp-ffi 7 | [submodule "OppaiSharp"] 8 | path = dependencies/OppaiSharp 9 | url = https://github.com/fantasyzhjk/OppaiSharp 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | // Use IntelliSense to find out which attributes exist for C# debugging 6 | // Use hover for the description of the existing attributes 7 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/src/bin/Debug/net7.0/KanonBot.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}", 16 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console 17 | "console": "integratedTerminal", 18 | "stopAtEntry": false 19 | }, 20 | { 21 | "name": ".NET Core Attach", 22 | "type": "coreclr", 23 | "request": "attach" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/src/KanonBot.csproj", 11 | "/property:GenerateFullPaths=true", 12 | ], 13 | "problemMatcher": "$msCompile", 14 | }, 15 | { 16 | "label": "test", 17 | "command": "dotnet", 18 | "type": "process", 19 | "args": [ 20 | "test", 21 | "${workspaceFolder}/Tests/Tests.csproj", 22 | "--logger", 23 | "console;verbosity=detailed", 24 | "--filter", 25 | "${input:testFilter}", 26 | ], 27 | "problemMatcher": "$msCompile", 28 | 29 | }, 30 | { 31 | "label": "publish self-contained", 32 | "command": "dotnet", 33 | "type": "process", 34 | "args": [ 35 | "publish", 36 | "${workspaceFolder}/src/KanonBot.csproj", 37 | "--configuration", 38 | "Release", 39 | "/property:GenerateFullPaths=true", 40 | ], 41 | "problemMatcher": "$msCompile" 42 | }, 43 | { 44 | "label": "publish", 45 | "command": "dotnet", 46 | "type": "process", 47 | "args": [ 48 | "publish", 49 | "${workspaceFolder}/src/KanonBot.csproj", 50 | "--configuration", 51 | "Release", 52 | "--self-contained", 53 | "false", 54 | "/property:GenerateFullPaths=true", 55 | ], 56 | "problemMatcher": "$msCompile" 57 | }, 58 | { 59 | "label": "watch", 60 | "command": "dotnet", 61 | "type": "process", 62 | "args": [ 63 | "watch", 64 | "run", 65 | "--project", 66 | "${workspaceFolder}/src/KanonBot.csproj" 67 | ], 68 | "problemMatcher": "$msCompile" 69 | } 70 | ], 71 | "inputs": [ 72 | { 73 | "id": "testFilter", 74 | "description": "请输入要测试的过滤器", 75 | "default": "", 76 | "type": "promptString" 77 | }, 78 | ] 79 | } -------------------------------------------------------------------------------- /KanonBot.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KanonBot", "src\KanonBot.csproj", "{8CA995FE-0476-4976-8B3B-0F896E6F05C2}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{53976824-B5EF-451E-A1A4-3131AD12B139}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(SolutionProperties) = preSolution 16 | HideSolutionNode = FALSE 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {8CA995FE-0476-4976-8B3B-0F896E6F05C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {8CA995FE-0476-4976-8B3B-0F896E6F05C2}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {8CA995FE-0476-4976-8B3B-0F896E6F05C2}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {8CA995FE-0476-4976-8B3B-0F896E6F05C2}.Release|Any CPU.Build.0 = Release|Any CPU 23 | {53976824-B5EF-451E-A1A4-3131AD12B139}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {53976824-B5EF-451E-A1A4-3131AD12B139}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {53976824-B5EF-451E-A1A4-3131AD12B139}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {53976824-B5EF-451E-A1A4-3131AD12B139}.Release|Any CPU.Build.0 = Release|Any CPU 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /QUICKSTART.md: -------------------------------------------------------------------------------- 1 | # QuickStart 2 | 3 | install rust & c#(mono) 4 | 5 | # test 6 | 7 | ```bash 8 | dotnet test --logger "console;verbosity=detailed" 9 | ``` 10 | 11 | # run 12 | 13 | ```bash 14 | dotnet run --project .\src\KanonBot.csproj 15 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KanonBot 2 | rewrite for obsolete projects. 3 | 4 | 对于旧版本kanonbot的重构。 5 | 6 | 有任何问题、建议请提交issue。 7 | 8 | 欢迎任何人添加新功能,在提交pull request前请先提交issue说明信息。 9 | 10 | The following repositories were used 11 | https://github.com/RoanH/osu-BonusPP 12 | https://github.com/MaxOhn/rosu-pp 13 | -------------------------------------------------------------------------------- /Tests/Misc.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Text.RegularExpressions; 3 | using System.Reflection; 4 | using System.ComponentModel; 5 | using API = KanonBot.API; 6 | using KanonBot.Serializer; 7 | using KanonBot.Drivers; 8 | using KanonBot; 9 | using Newtonsoft.Json.Linq; 10 | using Msg = KanonBot.Message; 11 | using Img = KanonBot.Image; 12 | using SixLabors.ImageSharp; 13 | 14 | namespace Tests; 15 | 16 | public class Misc 17 | { 18 | private readonly ITestOutputHelper Output; 19 | public Misc(ITestOutputHelper Output) 20 | { 21 | this.Output = Output; 22 | var configPath = "./config.toml"; 23 | if (File.Exists(configPath)) 24 | { 25 | Config.inner = Config.load(configPath); 26 | } 27 | else 28 | { 29 | System.IO.Directory.SetCurrentDirectory("../../../../"); 30 | Config.inner = Config.load(configPath); 31 | } 32 | } 33 | 34 | [Fact] 35 | public void Kaiheila() 36 | { 37 | var req = new KanonBot.Drivers.Kook.Models.MessageCreate 38 | { 39 | MessageType = KanonBot.Drivers.Kook.Enums.MessageType.Text, 40 | TargetId = "123", 41 | Content = "hi" 42 | }; 43 | Output.WriteLine(Json.Serialize(req)); 44 | } 45 | 46 | [Fact] 47 | public void UtilsTest() 48 | { 49 | Assert.Equal("osu", Utils.GetObjectDescription(API.OSU.Mode.OSU)); 50 | Output.WriteLine(Utils.ForStarDifficulty(1.25).ToString()); 51 | Output.WriteLine(Utils.ForStarDifficulty(2).ToString()); 52 | Output.WriteLine(Utils.ForStarDifficulty(2.5).ToString()); 53 | Output.WriteLine(Utils.ForStarDifficulty(3).ToString()); 54 | Output.WriteLine(Utils.ForStarDifficulty(3.5).ToString()); 55 | } 56 | 57 | [Fact] 58 | public void MsgChain() 59 | { 60 | var c = new Msg.Chain().msg("hello").image("C:\\hello.png", Msg.ImageSegment.Type.Url).msg("test\nhaha"); 61 | c.Add(new Msg.RawSegment("Test", new JObject { { "test", "test" } })); 62 | Assert.True(c.StartsWith("he")); 63 | Assert.False(c.StartsWith("!")); 64 | c = new Msg.Chain().at("zhjk", Platform.OneBot); 65 | Assert.True(c.StartsWith(new Msg.AtSegment("zhjk", Platform.OneBot))); 66 | 67 | var c1 = OneBot.Message.Build(c); 68 | Assert.Equal("[{\"type\":\"at\",\"data\":{\"qq\":\"zhjk\"}}]", Json.Serialize(c1)); 69 | var c2 = OneBot.Message.Parse(c1); 70 | Assert.Equal("", c2.ToString()); 71 | } 72 | 73 | // [Fact] 74 | // public void Mail() 75 | // { 76 | // // 邮件测试 77 | // KanonBot.Mail.MailStruct ms = new() 78 | // { 79 | // MailTo = new string[] { "deleted" }, 80 | // Subject = "你好!", 81 | // Body = "你好!这是一封来自猫猫的测试邮件!" 82 | // }; 83 | // KanonBot.Mail.Send(ms); 84 | // } 85 | 86 | } 87 | 88 | -------------------------------------------------------------------------------- /Tests/TestFiles/ImageHelper示例文件.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/desu-life/Bot/5a2da042c63e98a8d36976971c9a0739d95ee399/Tests/TestFiles/ImageHelper示例文件.png -------------------------------------------------------------------------------- /Tests/TestFiles/ImageHelper示例文件.txt: -------------------------------------------------------------------------------- 1 | Image -n baseimage -p new --size 500x500 2 | Image -n rawimg -p ./TestFiles/head.gif --size 100x100 3 | Draw -d baseimage -s rawimg -a 1 -p 0x0 4 | Round -n rawimg -s 100x100 -r 25 5 | Draw -d baseimage -s rawimg -a 1 -p 120x0 6 | DrawText -n baseimage -t "你好 世界!" -f "HarmonyOS_Sans_SC/HarmonyOS_Sans_SC_Medium.ttf" -s 24 -c "#f47920" -a left -p 0x130 7 | DrawText -n baseimage -t "Hello World!" -f "HarmonyOS_Sans_SC/HarmonyOS_Sans_SC_Medium.ttf" -s 24 -c "#f47920" -a left -p 25x154 8 | Image -n sampletext -p new -s 500x500 9 | DrawText -n sampletext -t "sample" -f "Torus-SemiBold.ttf" -s 64 -c "#ffffff" -a left -p 0x200 10 | Draw -d baseimage -s sampletext -a 0.3 -p 50x0 -------------------------------------------------------------------------------- /Tests/TestFiles/cqmessage.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "data": { 3 | "file": "14ab9b0d77f331eeaece7d591d200e22.image", 4 | "subType": "1", 5 | "url": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 6 | }, 7 | "type": "image" 8 | }, { 9 | "data": { 10 | "text": "test" 11 | }, 12 | "type": "text" 13 | }, { 14 | "data": { 15 | "file": "497af7e6c32d47536215904c4b4dc775.image", 16 | "subType": "1", 17 | "url": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 18 | }, 19 | "type": "image" 20 | }, { 21 | "data": { 22 | "text": "tessst" 23 | }, 24 | "type": "text" 25 | }] -------------------------------------------------------------------------------- /Tests/TestFiles/gulidmsg.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "avatar": "http://txx", 4 | "bot": false, 5 | "id": "xxxxxxxxxxxxx", 6 | "username": "xx" 7 | }, 8 | "channel_id": "xxxxxxxxxx", 9 | "content": "<@!xxxxxx> ", 10 | "guild_id": "xxxxxx", 11 | "id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 12 | "member": { 13 | "joined_at": "2022-04-21T12:47:37+08:00", 14 | "nick": "xxxxxxxxxxxx", 15 | "roles": ["2"] 16 | }, 17 | "mentions": [{ 18 | "avatar": "xxxxxxxxxxxx", 19 | "bot": true, 20 | "id": "xxxxxxxxxxxxxx", 21 | "username": "xxxxxxxxxxxxxxxx" 22 | }], 23 | "seq": 1, 24 | "seq_in_channel": "1", 25 | "timestamp": "2022-05-02T12:10:28+08:00" 26 | } -------------------------------------------------------------------------------- /Tests/TestFiles/head.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/desu-life/Bot/5a2da042c63e98a8d36976971c9a0739d95ee399/Tests/TestFiles/head.gif -------------------------------------------------------------------------------- /Tests/TestFiles/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/desu-life/Bot/5a2da042c63e98a8d36976971c9a0739d95ee399/Tests/TestFiles/info.png -------------------------------------------------------------------------------- /Tests/TestFiles/ppplus.json: -------------------------------------------------------------------------------- 1 | { 2 | "user_data": { 3 | "Rank": 75801, 4 | "CountryRank": 2760, 5 | "UserID": 9037287, 6 | "UserName": "Zh_Jk", 7 | "CountryCode": "CN", 8 | "PerformanceTotal": 4194.7376367012, 9 | "AimTotal": 2044.1327624658525, 10 | "JumpAimTotal": 1965.457680467264, 11 | "FlowAimTotal": 919.7710856644018, 12 | "PrecisionTotal": 451.2494446885271, 13 | "SpeedTotal": 1242.4057510518437, 14 | "StaminaTotal": 1042.3792683244214, 15 | "AccuracyTotal": 787.5575628564072, 16 | "AccuracyPercentTotal": 95.73530578613281, 17 | "PlayCount": 32459, 18 | "CountRankSS": 3, 19 | "CountRankS": 69 20 | }, 21 | "user_performances": { 22 | "total": [{ 23 | "SetID": 886368, 24 | "Artist": "hanser", 25 | "Title": "No title", 26 | "Version": "Sotarks' Sky Dream", 27 | "MaxCombo": 454, 28 | "UserID": 9037287, 29 | "BeatmapID": 1854022, 30 | "Total": 235.23690606765143, 31 | "Aim": 143.5167089026433, 32 | "JumpAim": 143.09149916701796, 33 | "FlowAim": 0.41644703516658615, 34 | "Precision": 27.104322133490186, 35 | "Speed": 62.117866579736315, 36 | "Stamina": 38.19972938714756, 37 | "HigherSpeed": 62.117866579736315, 38 | "Accuracy": 21.234379912081405, 39 | "Count300": 314, 40 | "Count100": 46, 41 | "Count50": 0, 42 | "Misses": 0, 43 | "AccuracyPercent": 0.9148148148148149, 44 | "Combo": 454, 45 | "EnabledMods": 8, 46 | "Rank": "A", 47 | "Date": "2019-10-02 07:06:29" 48 | } 49 | ] 50 | } 51 | } -------------------------------------------------------------------------------- /Tests/TestFiles/score.json: -------------------------------------------------------------------------------- 1 | { 2 | "accuracy": 0.9894665307509344, 3 | "best_id": 3894191295, 4 | "created_at": "2021-10-04T14:28:24+08:00", 5 | "id": 3894191295, 6 | "max_combo": 1185, 7 | "mode": "osu", 8 | "mode_int": 0, 9 | "mods": [ 10 | "HD" 11 | ], 12 | "passed": true, 13 | "perfect": true, 14 | "pp": 254.811, 15 | "rank": "SH", 16 | "replay": true, 17 | "score": 37856429, 18 | "statistics": { 19 | "count_100": 13, 20 | "count_300": 966, 21 | "count_50": 2, 22 | "count_geki": 136, 23 | "count_katu": 9, 24 | "count_miss": 0 25 | }, 26 | "user_id": 9037287, 27 | "current_user_attributes": { 28 | "pin": null 29 | }, 30 | "beatmap": { 31 | "beatmapset_id": 53249, 32 | "difficulty_rating": 5.59, 33 | "id": 162405, 34 | "mode": "osu", 35 | "status": "ranked", 36 | "total_length": 199, 37 | "user_id": 381444, 38 | "version": "Another", 39 | "accuracy": 8, 40 | "ar": 9, 41 | "bpm": 174.5, 42 | "convert": false, 43 | "count_circles": 798, 44 | "count_sliders": 183, 45 | "count_spinners": 0, 46 | "cs": 4, 47 | "deleted_at": null, 48 | "drain": 7, 49 | "hit_length": 188, 50 | "is_scoreable": true, 51 | "last_updated": "2014-11-17T17:39:23+08:00", 52 | "mode_int": 0, 53 | "passcount": 74389, 54 | "playcount": 1064794, 55 | "ranked": 1, 56 | "url": "https://osu.ppy.sh/beatmaps/162405", 57 | "checksum": "3623b9d6bb704e32e8034ed72f38d940" 58 | }, 59 | "beatmapset": { 60 | "artist": "Traktion", 61 | "artist_unicode": "Traktion", 62 | "covers": { 63 | "cover": "https://assets.ppy.sh/beatmaps/53249/covers/cover.jpg?1622056964", 64 | "cover@2x": "https://assets.ppy.sh/beatmaps/53249/covers/cover@2x.jpg?1622056964", 65 | "card": "https://assets.ppy.sh/beatmaps/53249/covers/card.jpg?1622056964", 66 | "card@2x": "https://assets.ppy.sh/beatmaps/53249/covers/card@2x.jpg?1622056964", 67 | "list": "https://assets.ppy.sh/beatmaps/53249/covers/list.jpg?1622056964", 68 | "list@2x": "https://assets.ppy.sh/beatmaps/53249/covers/list@2x.jpg?1622056964", 69 | "slimcover": "https://assets.ppy.sh/beatmaps/53249/covers/slimcover.jpg?1622056964", 70 | "slimcover@2x": "https://assets.ppy.sh/beatmaps/53249/covers/slimcover@2x.jpg?1622056964" 71 | }, 72 | "creator": "galvenize", 73 | "favourite_count": 1042, 74 | "hype": null, 75 | "id": 53249, 76 | "nsfw": false, 77 | "offset": 0, 78 | "play_count": 1850295, 79 | "preview_url": "//b.ppy.sh/preview/53249.mp3", 80 | "source": "", 81 | "spotlight": false, 82 | "status": "ranked", 83 | "title": "Mission ASCII", 84 | "title_unicode": "Mission ASCII", 85 | "track_id": null, 86 | "user_id": 381444, 87 | "video": false 88 | }, 89 | "user": { 90 | "avatar_url": "https://a.ppy.sh/9037287?1650110149.gif", 91 | "country_code": "CN", 92 | "default_group": "default", 93 | "id": 9037287, 94 | "is_active": true, 95 | "is_bot": false, 96 | "is_deleted": false, 97 | "is_online": false, 98 | "is_supporter": false, 99 | "last_visit": "2022-07-07T14:15:00+08:00", 100 | "pm_friends_only": false, 101 | "profile_colour": null, 102 | "username": "Zh_Jk" 103 | }, 104 | "weight": { 105 | "percentage": 100, 106 | "pp": 254.811 107 | } 108 | } -------------------------------------------------------------------------------- /Tests/TestFiles/scoretest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/desu-life/Bot/5a2da042c63e98a8d36976971c9a0739d95ee399/Tests/TestFiles/scoretest.png -------------------------------------------------------------------------------- /Tests/Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | all 17 | 18 | 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | all 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Tests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; 2 | global using Xunit.Abstractions; 3 | -------------------------------------------------------------------------------- /convert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #将UTF-8带BOM编码的文件转化为UTF-8无BOM格式 3 | if [[ -z "$1" ]];then 4 | echo '用法:./rmbom.sh [folder | file]' 5 | echo '将UTF-8编码的文件转化为UTF-8无BOM格式' 6 | exit 1 7 | fi 8 | 9 | 10 | path=$1 11 | find $path -type f -name "*" -print | xargs -i sed -i '1 s/^\xef\xbb\xbf//' {} 12 | echo "Convert finish" 13 | -------------------------------------------------------------------------------- /dependencies/osu-pp/OsuPP.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.Json; 5 | using System.Threading.Tasks; 6 | using osu.Game.Beatmaps.Legacy; 7 | using osu.Game.Rulesets.Osu; 8 | using osu.Game.Rulesets.Osu.Difficulty; 9 | using osu.Game.Rulesets.Osu.Objects; 10 | using osu.Game.Rulesets.Scoring; 11 | using osu.Game.Scoring; 12 | using osu.Framework.Audio.Track; 13 | using osu.Framework.Graphics.Textures; 14 | using osu.Game.Beatmaps; 15 | using osu.Game.Beatmaps.Formats; 16 | using osu.Game.IO; 17 | using osu.Game.Rulesets; 18 | using osu.Game.Skinning; 19 | using System; 20 | using System.Diagnostics; 21 | using System.Threading; 22 | using Newtonsoft.Json; 23 | using osu.Game.Rulesets.Mods; 24 | using osu.Game.Rulesets.Taiko; 25 | using osu.Game.Rulesets.Catch; 26 | using osu.Game.Rulesets.Mania; 27 | 28 | namespace OsuPP; 29 | 30 | public static class Utils { 31 | public static Ruleset? ParseRuleset(int id) { 32 | return id switch 33 | { 34 | 0 => new OsuRuleset(), 35 | 1 => new TaikoRuleset(), 36 | 2 => new CatchRuleset(), 37 | 3 => new ManiaRuleset(), 38 | _ => null 39 | }; 40 | } 41 | } 42 | 43 | public class OsuMods { 44 | public required osu.Game.Rulesets.Mods.Mod[] Mods { get; set; } 45 | 46 | public static OsuMods? FromJson(Ruleset ruleset, string json) { 47 | var mods = JsonConvert.DeserializeObject(json); 48 | if (mods is null) { 49 | return null; 50 | } else{ 51 | return new OsuMods { Mods = mods.Select(x => x.ToMod(ruleset)).ToArray() }; 52 | } 53 | } 54 | } 55 | 56 | 57 | public class Calculater { 58 | public required WorkingBeatmap beatmap { get; set; } 59 | public required OsuMods? mods { get; set; } 60 | public required Ruleset ruleset { get; set; } 61 | public required osu.Game.Rulesets.Difficulty.DifficultyAttributes? difficultyAttributes { get; set; } 62 | 63 | public double? accuracy { get; set; } 64 | public uint? combo { get; set; } 65 | public uint? N300 { get; set; } 66 | public uint? N100 { get; set; } 67 | public uint? N50 { get; set; } 68 | public uint? NKatu { get; set; } 69 | public uint? NGeki { get; set; } 70 | public uint? NMiss { get; set; } 71 | public uint? SliderTailHit { get; set; } 72 | public uint? SliderTickMiss { get; set; } 73 | 74 | public static Calculater New(Ruleset ruleset, WorkingBeatmap beatmap) { 75 | return new Calculater { 76 | beatmap = beatmap, 77 | mods = null, 78 | ruleset = ruleset, 79 | difficultyAttributes = null 80 | }; 81 | } 82 | 83 | public osu.Game.Rulesets.Mods.Mod[]? GetMods() { 84 | if (mods is not null) { 85 | return mods.Mods; 86 | } else { 87 | return null; 88 | } 89 | } 90 | 91 | public void Mods(string json) { 92 | mods = OsuMods.FromJson(ruleset, json); 93 | } 94 | 95 | public osu.Game.Rulesets.Difficulty.DifficultyAttributes CalculateDifficulty() { 96 | var difficultyCalculator = ruleset.CreateDifficultyCalculator(beatmap); 97 | 98 | if (mods is not null) { 99 | difficultyAttributes = difficultyCalculator.Calculate(mods.Mods); 100 | } else { 101 | difficultyAttributes = difficultyCalculator.Calculate(); 102 | } 103 | 104 | return difficultyAttributes; 105 | } 106 | 107 | public osu.Game.Beatmaps.BeatmapDifficulty CalculateBeatmap() { 108 | var playable_beatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, GetMods()); 109 | return playable_beatmap.Difficulty; 110 | } 111 | 112 | public osu.Game.Rulesets.Difficulty.PerformanceAttributes Calculate() { 113 | 114 | if (difficultyAttributes is null) { 115 | CalculateDifficulty(); 116 | } 117 | 118 | var scoreInfo = new ScoreInfo(beatmap.BeatmapInfo, ruleset.RulesetInfo, null); 119 | 120 | if (mods is not null) { 121 | scoreInfo.Mods = mods.Mods; 122 | } 123 | 124 | var statistics = new Dictionary(); 125 | 126 | if (ruleset is CatchRuleset) { 127 | if (N100 is not null) { 128 | statistics[HitResult.LargeTickHit] = (int)N100; 129 | } 130 | 131 | if (N50 is not null) { 132 | statistics[HitResult.SmallTickHit] = (int)N50; 133 | } 134 | 135 | if (NKatu is not null) { 136 | statistics[HitResult.SmallTickMiss] = (int)NKatu; 137 | } 138 | } else { 139 | if (N100 is not null) { 140 | statistics[HitResult.Ok] = (int)N100; 141 | } 142 | 143 | if (N50 is not null) { 144 | statistics[HitResult.Meh] = (int)N50; 145 | } 146 | 147 | if (NKatu is not null) { 148 | statistics[HitResult.Good] = (int)NKatu; 149 | } 150 | } 151 | 152 | if (ruleset is OsuRuleset) { 153 | if (SliderTailHit is not null) { 154 | statistics[HitResult.SliderTailHit] = (int)SliderTailHit; 155 | } 156 | 157 | if (SliderTickMiss is not null) { 158 | statistics[HitResult.LargeTickMiss] = (int)SliderTickMiss; 159 | } 160 | } 161 | 162 | if (N300 is not null) { 163 | statistics[HitResult.Great] = (int)N300; 164 | } 165 | 166 | if (NGeki is not null) { 167 | statistics[HitResult.Perfect] = (int)NGeki; 168 | } 169 | 170 | if (NMiss is not null) { 171 | statistics[HitResult.Miss] = (int)NMiss; 172 | } 173 | 174 | if (combo is not null) { 175 | scoreInfo.MaxCombo = (int)combo; 176 | } 177 | 178 | if (accuracy is not null) { 179 | scoreInfo.Accuracy = accuracy.Value / 100.0; 180 | } 181 | 182 | 183 | scoreInfo.Statistics = statistics; 184 | 185 | var ppcalc = ruleset.CreatePerformanceCalculator()!; 186 | return ppcalc.Calculate(scoreInfo, difficultyAttributes!); 187 | } 188 | } -------------------------------------------------------------------------------- /dependencies/osu-pp/WorkingBeatmap.cs: -------------------------------------------------------------------------------- 1 | using osu.Game.Rulesets.Osu.Objects; 2 | using osu.Game.Rulesets.Scoring; 3 | using osu.Game.Scoring; 4 | using osu.Framework.Audio.Track; 5 | using osu.Framework.Graphics.Textures; 6 | using osu.Game.Beatmaps; 7 | using osu.Game.Beatmaps.Formats; 8 | using osu.Game.IO; 9 | using osu.Game.Rulesets; 10 | using osu.Game.Skinning; 11 | 12 | namespace OsuPP; 13 | 14 | public class CalculatorWorkingBeatmap : WorkingBeatmap 15 | { 16 | private readonly Beatmap _beatmap; 17 | 18 | public CalculatorWorkingBeatmap(Stream beatmapStream) : this(ReadFromStream(beatmapStream)) { } 19 | public CalculatorWorkingBeatmap(byte[] b) : this(ReadFromBytes(b)) { } 20 | 21 | private CalculatorWorkingBeatmap(Beatmap beatmap) : base(beatmap.BeatmapInfo, null) 22 | { 23 | _beatmap = beatmap; 24 | _beatmap.BeatmapInfo.Ruleset = Utils.ParseRuleset(beatmap.BeatmapInfo.Ruleset.OnlineID)!.RulesetInfo; 25 | } 26 | 27 | static Beatmap ReadFromStream(Stream stream) 28 | { 29 | using var reader = new LineBufferedReader(stream); 30 | return Decoder.GetDecoder(reader).Decode(reader); 31 | } 32 | 33 | static Beatmap ReadFromBytes(byte[] bytes) { 34 | using var stream = new MemoryStream(bytes); 35 | return ReadFromStream(stream); 36 | } 37 | 38 | protected override IBeatmap GetBeatmap() => _beatmap; 39 | public override Texture? GetBackground() => null; 40 | protected override Track? GetBeatmapTrack() => null; 41 | protected override ISkin? GetSkin() => null; 42 | public override Stream? GetStream(string storagePath) => null; 43 | } 44 | -------------------------------------------------------------------------------- /dependencies/osu-pp/osu-pp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | OsuPP 6 | enable 7 | enable 8 | true 9 | en-US 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /dependencies/osu-pp/osu-pp.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.002.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu-pp", "osu-pp.csproj", "{C0A1235D-CD0B-40E7-AEB2-88BCA8A0A284}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {C0A1235D-CD0B-40E7-AEB2-88BCA8A0A284}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {C0A1235D-CD0B-40E7-AEB2-88BCA8A0A284}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {C0A1235D-CD0B-40E7-AEB2-88BCA8A0A284}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {C0A1235D-CD0B-40E7-AEB2-88BCA8A0A284}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {5F6E1886-AFBA-4923-900E-C03852110BD7} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /res/mail_desu_life_mailaddr_verify_template.txt: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
您正在验证 「desu.life」 的邮箱账户
7 |
8 |
9 |
10 | 11 | 您好, 12 | 13 |
14 |

您正在验证的邮箱账户为:{{{{mailaddress}}}}

15 |

如您未进行此项操作,请忽略并删除此邮件。

16 |


点此查看KanonBot的使用文档

17 |

此链接有效期为2小时,请您在收到本邮件后尽快验证您的邮箱,以免链接过期。

18 |
19 | 20 | 21 | 22 | 33 | 34 | 35 |
23 | 24 | 25 | 26 | 29 | 30 | 31 |
27 | 前往验证 28 |
32 |
36 |
37 |
38 |
39 |
40 |
-------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.cs] 4 | 5 | dotnet_diagnostic.IDE0044.severity = none 6 | dotnet_diagnostic.IDE1006.severity = none 7 | dotnet_diagnostic.IDE0059.severity = none 8 | dotnet_diagnostic.IDE0051.severity = none 9 | dotnet_diagnostic.IDE0060.severity = none 10 | dotnet_diagnostic.IDE0042.severity = none 11 | dotnet_diagnostic.CA1507.severity = none 12 | dotnet_diagnostic.CA2211.severity = none 13 | dotnet_diagnostic.CA2208.severity = none 14 | dotnet_diagnostic.CA1822.severity = none -------------------------------------------------------------------------------- /src/API/OSU/Extensions.cs: -------------------------------------------------------------------------------- 1 | namespace KanonBot.API.OSU; 2 | 3 | public static class OSUExtensions 4 | { 5 | public static string ToStr(this UserScoreType type) 6 | { 7 | return type switch 8 | { 9 | UserScoreType.Firsts => "firsts", 10 | UserScoreType.Recent => "recent", 11 | UserScoreType.Best => "best", 12 | _ => throw new ArgumentOutOfRangeException(), 13 | }; 14 | } 15 | 16 | public static string ToStr(this Mode mode) 17 | { 18 | return mode switch 19 | { 20 | Mode.OSU => "osu", 21 | Mode.Taiko => "taiko", 22 | Mode.Fruits => "fruits", 23 | Mode.Mania => "mania", 24 | _ => throw new ArgumentOutOfRangeException(), 25 | }; 26 | } 27 | 28 | public static string ToDisplay(this Mode mode) 29 | { 30 | return mode switch 31 | { 32 | Mode.OSU => "osu!standard", 33 | Mode.Taiko => "osu!taiko", 34 | Mode.Fruits => "osu!catch", 35 | Mode.Mania => "osu!mania", 36 | _ => throw new ArgumentOutOfRangeException(), 37 | }; 38 | } 39 | 40 | public static int ToNum(this Mode mode) 41 | { 42 | return mode switch 43 | { 44 | Mode.OSU => 0, 45 | Mode.Taiko => 1, 46 | Mode.Fruits => 2, 47 | Mode.Mania => 3, 48 | _ => throw new ArgumentOutOfRangeException() 49 | }; 50 | } 51 | 52 | public static Mode? ToMode(this string value) 53 | { 54 | value = value.ToLower(); // 大写字符转小写 55 | return value switch 56 | { 57 | "osu" => Mode.OSU, 58 | "taiko" => Mode.Taiko, 59 | "fruits" => Mode.Fruits, 60 | "mania" => Mode.Mania, 61 | _ => null 62 | }; 63 | } 64 | 65 | public static Mode? ParseMode(this string value) 66 | { 67 | value = value.ToLower(); // 大写字符转小写 68 | return value switch 69 | { 70 | "0" or "osu" or "std" => Mode.OSU, 71 | "1" or "taiko" or "tko" => Mode.Taiko, 72 | "2" or "fruits" or "catch" or "ctb" => Mode.Fruits, 73 | "3" or "mania" or "m" => Mode.Mania, 74 | _ => null 75 | }; 76 | } 77 | 78 | public static Mode? ToMode(this int value) 79 | { 80 | return value switch 81 | { 82 | 0 => Mode.OSU, 83 | 1 => Mode.Taiko, 84 | 2 => Mode.Fruits, 85 | 3 => Mode.Mania, 86 | _ => null 87 | }; 88 | } 89 | 90 | } -------------------------------------------------------------------------------- /src/API/OSU/Mode.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS8618 // 非null 字段未初始化 2 | using System.ComponentModel; 3 | 4 | namespace KanonBot.API.OSU; 5 | 6 | [DefaultValue(OSU)] // 解析失败就osu 7 | public enum Mode 8 | { 9 | [Description("osu")] 10 | OSU, 11 | 12 | [Description("taiko")] 13 | Taiko, 14 | 15 | [Description("fruits")] 16 | Fruits, 17 | 18 | [Description("mania")] 19 | Mania, 20 | } 21 | -------------------------------------------------------------------------------- /src/API/OSU/Models/Beatmap.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS8618 // 非null 字段未初始化 2 | using KanonBot.Serializer; 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Linq; 5 | using NullValueHandling = Newtonsoft.Json.NullValueHandling; 6 | using System.ComponentModel; 7 | 8 | namespace KanonBot.API.OSU; 9 | 10 | public partial class Models 11 | { 12 | public class BeatmapList { 13 | 14 | [JsonProperty(PropertyName = "beatmaps")] 15 | public Beatmap[] Beatmaps { get; set; } 16 | } 17 | 18 | public class Beatmap 19 | { 20 | [JsonProperty(PropertyName = "beatmapset_id")] 21 | public long BeatmapsetId { get; set; } 22 | 23 | [JsonProperty(PropertyName = "difficulty_rating")] 24 | public double DifficultyRating { get; set; } 25 | 26 | [JsonProperty(PropertyName = "id")] 27 | public long BeatmapId { get; set; } 28 | 29 | [JsonProperty(PropertyName = "mode")] 30 | [JsonConverter(typeof(JsonEnumConverter))] 31 | public Mode Mode { get; set; } 32 | 33 | [JsonProperty(PropertyName = "status")] 34 | [JsonConverter(typeof(JsonEnumConverter))] 35 | public Status Status { get; set; } 36 | 37 | [JsonProperty(PropertyName = "total_length")] 38 | public uint TotalLength { get; set; } 39 | 40 | [JsonProperty(PropertyName = "user_id")] 41 | public long UserId { get; set; } 42 | 43 | [JsonProperty(PropertyName = "version")] 44 | public string Version { get; set; } 45 | 46 | // Accuracy = OD 47 | 48 | [JsonProperty(PropertyName = "accuracy")] 49 | public double OD { get; set; } 50 | 51 | [JsonProperty(PropertyName = "ar")] 52 | public double AR { get; set; } 53 | 54 | [JsonProperty(PropertyName = "bpm", NullValueHandling = NullValueHandling.Ignore)] 55 | public double? BPM { get; set; } 56 | 57 | [JsonProperty(PropertyName = "convert")] 58 | public bool Convert { get; set; } 59 | 60 | [JsonProperty(PropertyName = "count_circles")] 61 | public long CountCircles { get; set; } 62 | 63 | [JsonProperty(PropertyName = "count_sliders")] 64 | public long CountSliders { get; set; } 65 | 66 | [JsonProperty(PropertyName = "count_spinners")] 67 | public long CountSpinners { get; set; } 68 | 69 | [JsonProperty(PropertyName = "cs")] 70 | public double CS { get; set; } 71 | 72 | [JsonProperty(PropertyName = "deleted_at", NullValueHandling = NullValueHandling.Ignore)] 73 | public DateTimeOffset? DeletedAt { get; set; } 74 | 75 | [JsonProperty(PropertyName = "drain")] 76 | public double HPDrain { get; set; } 77 | 78 | [JsonProperty(PropertyName = "hit_length")] 79 | public long HitLength { get; set; } 80 | 81 | [JsonProperty(PropertyName = "is_scoreable")] 82 | public bool IsScoreable { get; set; } 83 | 84 | [JsonProperty(PropertyName = "last_updated")] 85 | public DateTimeOffset LastUpdated { get; set; } 86 | 87 | [JsonProperty(PropertyName = "mode_int")] 88 | public int ModeInt { get; set; } 89 | 90 | [JsonProperty(PropertyName = "passcount")] 91 | public long Passcount { get; set; } 92 | 93 | [JsonProperty(PropertyName = "playcount")] 94 | public long Playcount { get; set; } 95 | 96 | [JsonProperty(PropertyName = "ranked")] 97 | public long Ranked { get; set; } 98 | 99 | [JsonProperty(PropertyName = "url")] 100 | public Uri Url { get; set; } 101 | 102 | [JsonProperty(PropertyName = "checksum", NullValueHandling = NullValueHandling.Ignore)] 103 | public string? Checksum { get; set; } 104 | 105 | [JsonProperty(PropertyName = "beatmapset", NullValueHandling = NullValueHandling.Ignore)] 106 | public Beatmapset? Beatmapset { get; set; } 107 | 108 | [JsonProperty(PropertyName = "failtimes")] 109 | public BeatmapFailtimes Failtimes { get; set; } 110 | 111 | [JsonProperty(PropertyName = "max_combo", NullValueHandling = NullValueHandling.Ignore)] 112 | public long? MaxCombo { get; set; } 113 | } 114 | 115 | 116 | public class BeatmapFailtimes 117 | { 118 | [JsonProperty(PropertyName = "fail", NullValueHandling = NullValueHandling.Ignore)] 119 | public int[]? Fail { get; set; } 120 | 121 | [JsonProperty(PropertyName = "exit", NullValueHandling = NullValueHandling.Ignore)] 122 | public int[]? Exit { get; set; } 123 | } 124 | 125 | [DefaultValue(Unknown)] 126 | public enum Status 127 | { 128 | /// 129 | /// 未知,在转换错误时为此值 130 | /// 131 | [Description("")] 132 | Unknown, 133 | 134 | [Description("graveyard")] 135 | Graveyard, 136 | 137 | [Description("wip")] 138 | WIP, 139 | 140 | [Description("pending")] 141 | Pending, 142 | 143 | [Description("ranked")] 144 | Ranked, 145 | 146 | [Description("approved")] 147 | Approved, 148 | 149 | [Description("qualified")] 150 | Qualified, 151 | 152 | [Description("loved")] 153 | Loved 154 | } 155 | } -------------------------------------------------------------------------------- /src/API/OSU/Models/BeatmapAttributes.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS8618 // 非null 字段未初始化 2 | using KanonBot.Serializer; 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Linq; 5 | using NullValueHandling = Newtonsoft.Json.NullValueHandling; 6 | 7 | namespace KanonBot.API.OSU; 8 | 9 | public partial class Models 10 | { 11 | 12 | public class BeatmapAttributes 13 | { 14 | // 不包含在json解析中,用作分辨mode 15 | public Mode Mode { get; set; } 16 | 17 | [JsonProperty(PropertyName = "max_combo")] 18 | // 共有部分 19 | public int MaxCombo { get; set; } 20 | 21 | [JsonProperty(PropertyName = "star_rating")] 22 | public double StarRating { get; set; } 23 | 24 | // osu, taiko, fruits包含 25 | [JsonProperty(PropertyName = "approach_rate")] 26 | public double ApproachRate { get; set; } 27 | 28 | // taiko, mania包含 29 | [JsonProperty(PropertyName = "great_hit_window")] 30 | public double GreatHitWindow { get; set; } 31 | 32 | // osu部分 33 | [JsonProperty(PropertyName = "aim_difficulty")] 34 | public double AimDifficulty { get; set; } 35 | 36 | [JsonProperty(PropertyName = "flashlight_difficulty")] 37 | public double FlashlightDifficulty { get; set; } 38 | 39 | [JsonProperty(PropertyName = "overall_difficulty")] 40 | public double OverallDifficulty { get; set; } 41 | 42 | [JsonProperty(PropertyName = "slider_factor")] 43 | public double SliderFactor { get; set; } 44 | 45 | [JsonProperty(PropertyName = "speed_difficulty")] 46 | public double SpeedDifficulty { get; set; } 47 | 48 | // taiko 49 | [JsonProperty(PropertyName = "stamina_difficulty")] 50 | public double StaminaDifficulty { get; set; } 51 | 52 | [JsonProperty(PropertyName = "rhythm_difficulty")] 53 | public double RhythmDifficulty { get; set; } 54 | 55 | [JsonProperty(PropertyName = "colour_difficulty")] 56 | public double ColourDifficulty { get; set; } 57 | 58 | // mania 59 | [JsonProperty(PropertyName = "score_multiplier")] 60 | public double ScoreMultiplier { get; set; } 61 | } 62 | 63 | 64 | 65 | } -------------------------------------------------------------------------------- /src/API/OSU/Models/BeatmapSearch.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS8618 // 非null 字段未初始化 2 | using KanonBot.Serializer; 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Linq; 5 | using NullValueHandling = Newtonsoft.Json.NullValueHandling; 6 | 7 | namespace KanonBot.API.OSU; 8 | 9 | public partial class Models 10 | { 11 | 12 | 13 | public class BeatmapSearchResult 14 | { 15 | [JsonProperty("beatmapsets")] 16 | public List Beatmapsets { get; set; } 17 | 18 | [JsonProperty("search")] 19 | public SearchResult Search { get; set; } 20 | 21 | [JsonProperty("recommended_difficulty")] 22 | public object? RecommendedDifficulty { get; set; } 23 | 24 | [JsonProperty("error")] 25 | public object? Error { get; set; } 26 | 27 | [JsonProperty("total")] 28 | public long Total { get; set; } 29 | 30 | [JsonProperty("cursor")] 31 | public CursorResult Cursor { get; set; } 32 | 33 | [JsonProperty("cursor_string")] 34 | public string CursorString { get; set; } 35 | 36 | public class SearchResult 37 | { 38 | [JsonProperty("sort")] 39 | public string sort { get; set; } 40 | } 41 | 42 | public class CursorResult 43 | { 44 | [JsonProperty("approved_date")] 45 | public long approved_date { get; set; } 46 | 47 | [JsonProperty("id")] 48 | public int id { get; set; } 49 | } 50 | } 51 | 52 | 53 | } -------------------------------------------------------------------------------- /src/API/OSU/Models/Beatmapset.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS8618 // 非null 字段未初始化 2 | using KanonBot.Serializer; 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Linq; 5 | using NullValueHandling = Newtonsoft.Json.NullValueHandling; 6 | 7 | namespace KanonBot.API.OSU; 8 | 9 | public partial class Models 10 | { 11 | public class Beatmapset 12 | { 13 | [JsonProperty(PropertyName = "artist")] 14 | public string Artist { get; set; } 15 | 16 | [JsonProperty(PropertyName = "artist_unicode")] 17 | public string ArtistUnicode { get; set; } 18 | 19 | [JsonProperty(PropertyName = "covers")] 20 | public BeatmapCovers Covers { get; set; } 21 | 22 | [JsonProperty(PropertyName = "creator")] 23 | public string Creator { get; set; } 24 | 25 | [JsonProperty(PropertyName = "favourite_count")] 26 | public long FavouriteCount { get; set; } 27 | 28 | [JsonProperty(PropertyName = "hype", NullValueHandling = NullValueHandling.Ignore)] 29 | public BeatmapHype? Hype { get; set; } 30 | 31 | [JsonProperty(PropertyName = "id")] 32 | public long Id { get; set; } 33 | 34 | [JsonProperty(PropertyName = "nsfw")] 35 | public bool IsNsfw { get; set; } 36 | 37 | [JsonProperty(PropertyName = "offset")] 38 | public long Offset { get; set; } 39 | 40 | [JsonProperty(PropertyName = "play_count")] 41 | public long PlayCount { get; set; } 42 | 43 | [JsonProperty(PropertyName = "preview_url")] 44 | public string PreviewUrl { get; set; } 45 | 46 | [JsonProperty(PropertyName = "source")] 47 | public string Source { get; set; } 48 | 49 | [JsonProperty(PropertyName = "spotlight")] 50 | public bool Spotlight { get; set; } 51 | 52 | [JsonProperty(PropertyName = "status")] 53 | public string Status { get; set; } 54 | 55 | [JsonProperty(PropertyName = "title")] 56 | public string Title { get; set; } 57 | 58 | [JsonProperty(PropertyName = "title_unicode")] 59 | public string TitleUnicode { get; set; } 60 | 61 | [JsonProperty(PropertyName = "user_id")] 62 | public long UserId { get; set; } 63 | 64 | [JsonProperty(PropertyName = "video")] 65 | public bool Video { get; set; } 66 | 67 | [JsonProperty(PropertyName = "availability")] 68 | public BeatmapAvailability Availability { get; set; } 69 | 70 | [JsonProperty(PropertyName = "bpm")] 71 | public long BPM { get; set; } 72 | 73 | [JsonProperty(PropertyName = "can_be_hyped")] 74 | public bool CanBeHyped { get; set; } 75 | 76 | [JsonProperty(PropertyName = "discussion_enabled")] 77 | public bool DiscussionEnabled { get; set; } 78 | 79 | [JsonProperty(PropertyName = "discussion_locked")] 80 | public bool DiscussionLocked { get; set; } 81 | 82 | [JsonProperty(PropertyName = "is_scoreable")] 83 | public bool IsScoreable { get; set; } 84 | 85 | [JsonProperty(PropertyName = "last_updated")] 86 | public DateTimeOffset LastUpdated { get; set; } 87 | 88 | [JsonProperty( 89 | PropertyName = "legacy_thread_url", 90 | NullValueHandling = NullValueHandling.Ignore 91 | )] 92 | public Uri? LegacyThreadUrl { get; set; } 93 | 94 | [JsonProperty(PropertyName = "nominations_summary")] 95 | public NominationsSummary NominationsSummary { get; set; } 96 | 97 | [JsonProperty(PropertyName = "ranked")] 98 | public long Ranked { get; set; } 99 | 100 | [JsonProperty(PropertyName = "ranked_date", NullValueHandling = NullValueHandling.Ignore)] 101 | public DateTimeOffset? RankedDate { get; set; } 102 | 103 | [JsonProperty(PropertyName = "storyboard")] 104 | public bool Storyboard { get; set; } 105 | 106 | [JsonProperty( 107 | PropertyName = "submitted_date", 108 | NullValueHandling = NullValueHandling.Ignore 109 | )] 110 | public DateTimeOffset? SubmittedDate { get; set; } 111 | 112 | [JsonProperty(PropertyName = "tags")] 113 | public string Tags { get; set; } 114 | 115 | [JsonProperty(PropertyName = "ratings")] 116 | public long[] Ratings { get; set; } 117 | 118 | [JsonProperty(PropertyName = "beatmaps", NullValueHandling = NullValueHandling.Ignore)] 119 | public Beatmap[]? Beatmaps { get; set; } 120 | 121 | // [JsonProperty(PropertyName = "track_id")] 122 | // public JObject TrackId { get; set; } 123 | } 124 | 125 | public class BeatmapAvailability 126 | { 127 | [JsonProperty(PropertyName = "download_disabled")] 128 | public bool DownloadDisabled { get; set; } 129 | 130 | [JsonProperty( 131 | PropertyName = "more_information", 132 | NullValueHandling = NullValueHandling.Ignore 133 | )] 134 | public string? MoreInformation { get; set; } 135 | } 136 | 137 | 138 | public class BeatmapCovers 139 | { 140 | [JsonProperty(PropertyName = "cover")] 141 | public string Cover { get; set; } 142 | 143 | [JsonProperty(PropertyName = "cover@2x")] 144 | public string Cover2x { get; set; } 145 | 146 | [JsonProperty(PropertyName = "card")] 147 | public string Card { get; set; } 148 | 149 | [JsonProperty(PropertyName = "card@2x")] 150 | public string Card2x { get; set; } 151 | 152 | [JsonProperty(PropertyName = "list")] 153 | public string List { get; set; } 154 | 155 | [JsonProperty(PropertyName = "list@2x")] 156 | public string List2x { get; set; } 157 | 158 | [JsonProperty(PropertyName = "slimcover")] 159 | public string SlimCover { get; set; } 160 | 161 | [JsonProperty(PropertyName = "slimcover@2x")] 162 | public string SlimCover2x { get; set; } 163 | } 164 | 165 | 166 | public class BeatmapHype 167 | { 168 | [JsonProperty(PropertyName = "current")] 169 | public int DownloadDisabled { get; set; } 170 | 171 | [JsonProperty(PropertyName = "required")] 172 | public int MoreInformation { get; set; } 173 | } 174 | 175 | public class NominationsSummary 176 | { 177 | [JsonProperty(PropertyName = "current")] 178 | public int Current { get; set; } 179 | 180 | [JsonProperty(PropertyName = "required")] 181 | public int NominationsSummaryRequired { get; set; } 182 | } 183 | 184 | } -------------------------------------------------------------------------------- /src/API/OSU/Models/Medal.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS8618 // 非null 字段未初始化 2 | using KanonBot.Serializer; 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Linq; 5 | using NullValueHandling = Newtonsoft.Json.NullValueHandling; 6 | 7 | namespace KanonBot.API.OSU; 8 | 9 | public partial class Models 10 | { 11 | 12 | public class Medal 13 | { 14 | [JsonProperty("id")] 15 | public uint Id { get; set; } 16 | 17 | [JsonProperty("description")] 18 | public string Description { get; set; } 19 | 20 | [JsonProperty("grouping")] 21 | public string grouping { get; set; } 22 | 23 | [JsonProperty("icon_url")] 24 | public string icon_url { get; set; } 25 | 26 | [JsonProperty("instructions", NullValueHandling = NullValueHandling.Ignore)] 27 | public string? instructions { get; set; } 28 | 29 | [JsonProperty("mode", NullValueHandling = NullValueHandling.Ignore)] 30 | public Mode? mode { get; set; } 31 | 32 | [JsonProperty("name")] 33 | public string name { get; set; } 34 | 35 | [JsonProperty("ordering")] 36 | public uint ordering { get; set; } 37 | 38 | [JsonProperty("slug")] 39 | public string slug { get; set; } 40 | } 41 | 42 | public class MedalCompact { 43 | 44 | [JsonProperty("achievement_id")] 45 | public uint MedalId { get; set; } 46 | 47 | [JsonProperty("achieved_at")] 48 | public DateTimeOffset achieved_at { get; set; } 49 | } 50 | } -------------------------------------------------------------------------------- /src/API/OSU/Models/Mods.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS8618 // 非null 字段未初始化 2 | using System.ComponentModel; 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Linq; 5 | using NullValueHandling = Newtonsoft.Json.NullValueHandling; 6 | 7 | namespace KanonBot.API.OSU; 8 | 9 | public partial class Models 10 | { 11 | public class Mod 12 | { 13 | [JsonProperty("acronym")] 14 | public string Acronym { get; set; } 15 | 16 | [JsonProperty("settings", NullValueHandling = NullValueHandling.Ignore)] 17 | public JObject? Settings { get; set; } 18 | 19 | [JsonIgnore] 20 | public bool IsClassic => Acronym == "CL"; 21 | 22 | [JsonIgnore] 23 | public bool IsVisualMod => Acronym == "HD" || Acronym == "FL"; 24 | 25 | [JsonIgnore] 26 | public bool IsSpeedChangeMod => 27 | Acronym == "DT" || Acronym == "NC" || Acronym == "HT" || Acronym == "DC"; 28 | 29 | public static Mod FromString(string mod) 30 | { 31 | return new Mod { Acronym = mod }; 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/API/OSU/Models/PPlusData.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS8618 // 非null 字段未初始化 2 | using KanonBot.Serializer; 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Linq; 5 | using NullValueHandling = Newtonsoft.Json.NullValueHandling; 6 | 7 | namespace KanonBot.API.OSU; 8 | 9 | public partial class Models 10 | { 11 | public class PPlusData 12 | { 13 | public UserData User { get; set; } 14 | 15 | public UserPerformances[]? Performances { get; set; } 16 | 17 | public class UserData 18 | { 19 | [JsonProperty("Rank")] 20 | public int Rank { get; set; } 21 | 22 | [JsonProperty("CountryRank")] 23 | public int CountryRank { get; set; } 24 | 25 | [JsonProperty("UserID")] 26 | public long UserId { get; set; } 27 | 28 | [JsonProperty("UserName")] 29 | public string UserName { get; set; } 30 | 31 | [JsonProperty("CountryCode")] 32 | public string CountryCode { get; set; } 33 | 34 | [JsonProperty("PerformanceTotal")] 35 | public double PerformanceTotal { get; set; } 36 | 37 | [JsonProperty("AimTotal")] 38 | public double AimTotal { get; set; } 39 | 40 | [JsonProperty("JumpAimTotal")] 41 | public double JumpAimTotal { get; set; } 42 | 43 | [JsonProperty("FlowAimTotal")] 44 | public double FlowAimTotal { get; set; } 45 | 46 | [JsonProperty("PrecisionTotal")] 47 | public double PrecisionTotal { get; set; } 48 | 49 | [JsonProperty("SpeedTotal")] 50 | public double SpeedTotal { get; set; } 51 | 52 | [JsonProperty("StaminaTotal")] 53 | public double StaminaTotal { get; set; } 54 | 55 | [JsonProperty("AccuracyTotal")] 56 | public double AccuracyTotal { get; set; } 57 | 58 | [JsonProperty("AccuracyPercentTotal")] 59 | public double AccuracyPercentTotal { get; set; } 60 | 61 | [JsonProperty("PlayCount")] 62 | public int PlayCount { get; set; } 63 | 64 | [JsonProperty("CountRankSS")] 65 | public int CountRankSS { get; set; } 66 | 67 | [JsonProperty("CountRankS")] 68 | public int CountRankS { get; set; } 69 | } 70 | 71 | public class UserPerformances 72 | { 73 | [JsonProperty("SetID")] 74 | public long SetId { get; set; } 75 | 76 | [JsonProperty("Artist")] 77 | public string Artist { get; set; } 78 | 79 | [JsonProperty("Title")] 80 | public string Title { get; set; } 81 | 82 | [JsonProperty("Version")] 83 | public string Version { get; set; } 84 | 85 | [JsonProperty("MaxCombo")] 86 | public int MaxCombo { get; set; } 87 | 88 | [JsonProperty("UserID")] 89 | public long UserId { get; set; } 90 | 91 | [JsonProperty("BeatmapID")] 92 | public long BeatmapId { get; set; } 93 | 94 | [JsonProperty("Total")] 95 | public double TotalTotal { get; set; } 96 | 97 | [JsonProperty("Aim")] 98 | public double Aim { get; set; } 99 | 100 | [JsonProperty("JumpAim")] 101 | public double JumpAim { get; set; } 102 | 103 | [JsonProperty("FlowAim")] 104 | public double FlowAim { get; set; } 105 | 106 | [JsonProperty("Precision")] 107 | public double Precision { get; set; } 108 | 109 | [JsonProperty("Speed")] 110 | public double Speed { get; set; } 111 | 112 | [JsonProperty("Stamina")] 113 | public double Stamina { get; set; } 114 | 115 | [JsonProperty("HigherSpeed")] 116 | public double HigherSpeed { get; set; } 117 | 118 | [JsonProperty("Accuracy")] 119 | public double Accuracy { get; set; } 120 | 121 | [JsonProperty("Count300")] 122 | public int CountGreat { get; set; } 123 | 124 | [JsonProperty("Count100")] 125 | public int CountOk { get; set; } 126 | 127 | [JsonProperty("Count50")] 128 | public int CountMeh { get; set; } 129 | 130 | [JsonProperty("Misses")] 131 | public int CountMiss { get; set; } 132 | 133 | [JsonProperty("AccuracyPercent")] 134 | public double AccuracyPercent { get; set; } 135 | 136 | [JsonProperty("Combo")] 137 | public int Combo { get; set; } 138 | 139 | [JsonProperty("EnabledMods")] 140 | public int EnabledMods { get; set; } 141 | 142 | [JsonProperty("Rank")] 143 | public string Rank { get; set; } 144 | 145 | [JsonProperty("Date")] 146 | public DateTimeOffset Date { get; set; } 147 | } 148 | } 149 | } -------------------------------------------------------------------------------- /src/API/OSU/Models/Score.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS8618 // 非null 字段未初始化 2 | using KanonBot.Serializer; 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Linq; 5 | using NullValueHandling = Newtonsoft.Json.NullValueHandling; 6 | 7 | namespace KanonBot.API.OSU; 8 | 9 | public partial class Models 10 | { 11 | 12 | public class BeatmapScore // 只是比score多了个当前bid的排名 13 | { 14 | [JsonProperty("position")] 15 | public int Position { get; set; } 16 | 17 | [JsonProperty("score")] 18 | public Score Score { get; set; } 19 | } 20 | 21 | 22 | public class Score 23 | { 24 | [JsonProperty("accuracy")] 25 | public double Accuracy { get; set; } 26 | 27 | [JsonProperty("best_id", NullValueHandling = NullValueHandling.Ignore)] 28 | public long BestId { get; set; } 29 | 30 | [JsonProperty("created_at")] 31 | public DateTimeOffset CreatedAt { get; set; } 32 | 33 | [JsonProperty("id")] 34 | public long Id { get; set; } 35 | 36 | [JsonProperty("max_combo")] 37 | public uint MaxCombo { get; set; } 38 | 39 | [JsonProperty("mode")] 40 | [JsonConverter(typeof(JsonEnumConverter))] 41 | public Mode Mode { get; set; } 42 | 43 | [JsonProperty("mode_int")] 44 | public int ModeInt { get; set; } 45 | 46 | [JsonProperty("mods")] 47 | public string[] Mods { get; set; } 48 | 49 | [JsonProperty("passed")] 50 | public bool Passed { get; set; } 51 | 52 | [JsonProperty("perfect")] 53 | public bool Perfect { get; set; } 54 | 55 | [JsonProperty("pp", NullValueHandling = NullValueHandling.Ignore)] 56 | public double PP { get; set; } 57 | 58 | [JsonProperty("rank")] 59 | public string Rank { get; set; } 60 | 61 | [JsonProperty("replay")] 62 | public bool Replay { get; set; } 63 | 64 | [JsonProperty("score")] 65 | public uint Scores { get; set; } 66 | 67 | [JsonProperty("statistics")] 68 | public ScoreStatistics Statistics { get; set; } 69 | 70 | [JsonProperty("user_id")] 71 | public long UserId { get; set; } 72 | 73 | [JsonProperty("beatmap", NullValueHandling = NullValueHandling.Ignore)] 74 | public Beatmap? Beatmap { get; set; } 75 | 76 | [JsonProperty("beatmapset", NullValueHandling = NullValueHandling.Ignore)] 77 | public Beatmapset? Beatmapset { get; set; } 78 | 79 | [JsonProperty("user", NullValueHandling = NullValueHandling.Ignore)] 80 | public User? User { get; set; } 81 | 82 | [JsonProperty("weight", NullValueHandling = NullValueHandling.Ignore)] 83 | public ScoreWeight? Weight { get; set; } 84 | 85 | public static implicit operator ScoreLazer(Score s) 86 | { 87 | var mods = s.Mods.Map(Mod.FromString).ToList(); 88 | mods.Add(Mod.FromString("CL")); 89 | return new ScoreLazer 90 | { 91 | Accuracy = s.Accuracy, 92 | BestId = s.BestId, 93 | EndedAt = s.CreatedAt, 94 | Id = s.Id, 95 | MaxCombo = s.MaxCombo, 96 | ModeInt = s.Mode.ToNum(), 97 | Mods = mods.ToArray(), 98 | Passed = s.Passed, 99 | pp = s.PP, 100 | Rank = s.Rank, 101 | HasReplay = s.Replay, 102 | Score = 0, 103 | LegacyTotalScore = s.Scores, 104 | Statistics = s.Statistics, 105 | UserId = s.UserId, 106 | Beatmap = s.Beatmap, 107 | Beatmapset = s.Beatmapset, 108 | User = s.User, 109 | Weight = s.Weight, 110 | ConvertFromOld = true 111 | }; 112 | } 113 | } 114 | 115 | public class ScoreStatistics 116 | { 117 | [JsonProperty("count_100", NullValueHandling = NullValueHandling.Ignore)] 118 | public uint CountOk { get; set; } 119 | 120 | [JsonProperty("count_300", NullValueHandling = NullValueHandling.Ignore)] 121 | public uint CountGreat { get; set; } 122 | 123 | [JsonProperty("count_50", NullValueHandling = NullValueHandling.Ignore)] 124 | public uint CountMeh { get; set; } 125 | 126 | [JsonProperty("count_geki", NullValueHandling = NullValueHandling.Ignore)] 127 | public uint CountGeki { get; set; } 128 | 129 | [JsonProperty("count_katu", NullValueHandling = NullValueHandling.Ignore)] 130 | public uint CountKatu { get; set; } 131 | 132 | [JsonProperty("count_miss", NullValueHandling = NullValueHandling.Ignore)] 133 | public uint CountMiss { get; set; } 134 | 135 | public static implicit operator ScoreStatisticsLazer(ScoreStatistics s) 136 | { 137 | return new ScoreStatisticsLazer 138 | { 139 | CountOk = s.CountOk, 140 | CountGreat = s.CountGreat, 141 | CountMeh = s.CountMeh, 142 | CountGeki = s.CountGeki, 143 | CountKatu = s.CountKatu, 144 | CountMiss = s.CountMiss 145 | }; 146 | } 147 | } 148 | 149 | 150 | public class ScoreWeight 151 | { 152 | [JsonProperty("percentage")] 153 | public double Percentage { get; set; } 154 | 155 | [JsonProperty("pp", NullValueHandling = NullValueHandling.Ignore)] 156 | public double PP { get; set; } 157 | } 158 | } -------------------------------------------------------------------------------- /src/API/OpenAI/API.cs: -------------------------------------------------------------------------------- 1 | using OpenAI_API; 2 | using OpenAI_API.Chat; 3 | using OpenAI_API.Completions; 4 | using OpenAI_API.Models; 5 | using OpenAI_API.Moderation; 6 | using System.Collections.Concurrent; 7 | 8 | namespace KanonBot.API 9 | { 10 | public partial class OpenAI 11 | { 12 | private static Config.Base config = Config.inner!; 13 | private static ConcurrentDictionary> ChatHistoryDict = new(); 14 | 15 | private static readonly int ChatBotMemorySpan = 20; 16 | 17 | public static async Task Chat(string chatmsg, string name, long uid) 18 | { 19 | OpenAIAPI? api; 20 | Conversation? chat; 21 | var dbinfo = await Database.Client.GetChatBotInfo(uid); 22 | 23 | if (dbinfo != null) 24 | { 25 | //使用用户数据库配置 26 | if (dbinfo.openaikey != "" && dbinfo.openaikey != "default") 27 | api = new OpenAIAPI(dbinfo.openaikey); 28 | else 29 | api = new OpenAIAPI(config.openai!.Key); 30 | chat = api.Chat.CreateConversation(); 31 | if (dbinfo.botdefine != "" && dbinfo.botdefine != "default") 32 | { 33 | if (dbinfo.botdefine!.IndexOf("#") > 0) 34 | { 35 | chat!.AppendSystemMessage(dbinfo.botdefine!); 36 | } 37 | else 38 | { 39 | var t = dbinfo.botdefine!.Split("#"); 40 | foreach (var item in t) chat!.AppendSystemMessage(item); 41 | } 42 | } 43 | else 44 | { 45 | if (config.openai!.PreDefine!.IndexOf("#") > 0) 46 | { 47 | chat!.AppendSystemMessage(config.openai!.PreDefine!); 48 | } 49 | else 50 | { 51 | var t = config.openai!.PreDefine!.Split("#"); 52 | foreach (var item in t) chat!.AppendSystemMessage(item); 53 | } 54 | } 55 | } 56 | else 57 | { 58 | //使用默认配置 59 | api = new OpenAIAPI(config.openai!.Key); 60 | chat = api.Chat.CreateConversation(); 61 | if (config.openai!.PreDefine!.IndexOf("#") > 0) 62 | { 63 | chat!.AppendSystemMessage(config.openai!.PreDefine!); 64 | } 65 | else 66 | { 67 | var t = config.openai!.PreDefine!.Split("#"); 68 | foreach (var item in t) chat!.AppendSystemMessage(item); 69 | } 70 | } 71 | 72 | chat.Model = Model.ChatGPTTurbo; 73 | chat.RequestParameters.Temperature = config.openai!.Temperature; 74 | chat.RequestParameters.TopP = config.openai.TopP; 75 | chat.RequestParameters.MaxTokens = config.openai.MaxTokens; 76 | chat.RequestParameters.NumChoicesPerMessage = 1; 77 | 78 | if (!ChatHistoryDict.ContainsKey(uid)) 79 | { 80 | try 81 | { 82 | ChatHistoryDict.TryAdd(uid, new() { chatmsg }); 83 | } 84 | catch 85 | { 86 | return "猫猫记忆模块出错了喵 T^T"; 87 | } 88 | } 89 | else 90 | { 91 | if (ChatHistoryDict[uid].Count < ChatBotMemorySpan) 92 | ChatHistoryDict[uid].Add(chatmsg); 93 | else 94 | { 95 | ChatHistoryDict[uid].RemoveAt(0); 96 | ChatHistoryDict[uid].Add(chatmsg); 97 | } 98 | } 99 | foreach (var item in ChatHistoryDict[uid]) 100 | { 101 | chat!.AppendUserInputWithName(name, item); 102 | } 103 | chat!.AppendUserInputWithName(name, chatmsg); 104 | return chat.GetResponseFromChatbotAsync().Result; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/API/PPYSB/API.cs: -------------------------------------------------------------------------------- 1 | // Flurl.Http.FlurlHttpTimeoutException 2 | using System.IO; 3 | using System.Net; 4 | using KanonBot.Database; 5 | using KanonBot.Serializer; 6 | using Newtonsoft.Json.Linq; 7 | 8 | namespace KanonBot.API.PPYSB 9 | { 10 | // 成绩类型,用作API查询 11 | // 共可以是 best, firsts, recent 12 | // 默认为best(bp查询) 13 | public enum UserScoreType 14 | { 15 | Best, 16 | Recent, 17 | } 18 | 19 | public static class Client 20 | { 21 | private static readonly Config.Base config = Config.inner!; 22 | private static readonly string EndPointV1 = "https://api.ppy.sb/v1/"; 23 | private static readonly string EndPointV2 = "https://api.ppy.sb/v2/"; 24 | 25 | static IFlurlRequest http() 26 | { 27 | var ep = EndPointV2; 28 | return ep.AllowHttpStatus(404); 29 | } 30 | 31 | static IFlurlRequest httpV1() 32 | { 33 | var ep = EndPointV1; 34 | return ep.AllowHttpStatus(404); 35 | } 36 | 37 | public static async Task GetUser(string userName) 38 | { 39 | var res = await httpV1() 40 | .AllowHttpStatus(404, 422) 41 | .AppendPathSegment("get_player_info") 42 | .SetQueryParam("scope", "all") 43 | .SetQueryParam("name", userName.Replace(" ", "_")) 44 | .GetAsync(); 45 | 46 | if (res.StatusCode is 404 or 422) 47 | return null; 48 | else 49 | { 50 | var u = await res.GetJsonAsync(); 51 | return u?.Player; 52 | } 53 | } 54 | 55 | public static async Task GetUser(long uid) 56 | { 57 | var res = await httpV1() 58 | .AppendPathSegment("get_player_info") 59 | .SetQueryParam("scope", "all") 60 | .SetQueryParam("id", value: uid) 61 | .GetAsync(); 62 | 63 | if (res.StatusCode == 404) 64 | return null; 65 | else 66 | { 67 | var u = await res.GetJsonAsync(); 68 | return u?.Player; 69 | } 70 | } 71 | 72 | async public static Task GetMap(long beatmapId) 73 | { 74 | var res = await http() 75 | .AppendPathSegment("maps") 76 | .AppendPathSegment(beatmapId) 77 | .GetAsync(); 78 | 79 | if (res.StatusCode == 404) 80 | return null; 81 | else 82 | { 83 | var u = await res.GetJsonAsync(); 84 | return u?.Data; 85 | } 86 | } 87 | 88 | async public static Task GetScore(long scoreId) 89 | { 90 | var res = await httpV1() 91 | .AppendPathSegment("get_score_info") 92 | .SetQueryParam("id", scoreId) 93 | .GetAsync(); 94 | 95 | if (res.StatusCode == 404) 96 | return null; 97 | else 98 | { 99 | var u = await res.GetJsonAsync(); 100 | return u?.Score; 101 | } 102 | } 103 | 104 | async public static Task GetMapScore(long userId, long beatmapId, Mode mode = Mode.OSU, uint mods = 0, bool pp_order = false) 105 | { 106 | var scores = await GetMapScores(userId, beatmapId, mode, 1, mods); 107 | var score = pp_order ? scores?.OrderByDescending(s => s.PP).FirstOrDefault() : scores?.FirstOrDefault(); 108 | 109 | if (score is not null) { 110 | var map = score.Beatmap; 111 | var score2 = await GetScore(score.Id); 112 | if (score2 is not null) { 113 | score2.Beatmap = map; 114 | return score2; 115 | } 116 | } 117 | 118 | return score; 119 | } 120 | 121 | async public static Task GetMapScores(long userId, long beatmapId, Mode mode = Mode.OSU, int limit = 50, uint mods = 0) 122 | { 123 | if (mode.IsSupported() == false) return null; 124 | 125 | var map = await GetMap(beatmapId); 126 | if (map == null) return null; 127 | 128 | var req = http() 129 | .AppendPathSegment("scores") 130 | .SetQueryParam("map_md5", map.Md5) 131 | .SetQueryParam("mode", mode.ToNum()) 132 | .SetQueryParam("user_id", userId) 133 | .SetQueryParam("status", 2) 134 | .SetQueryParam("page_size", limit) 135 | .SetQueryParam("page", 1); 136 | 137 | if (mods != 0) { 138 | req.SetQueryParam("mods", mods); 139 | } 140 | 141 | var res = await req.GetAsync(); 142 | 143 | if (res.StatusCode == 404) 144 | return null; 145 | else 146 | { 147 | var u = await res.GetJsonAsync(); 148 | return u?.Data.Map(s => { 149 | s.Beatmap = map; 150 | s.Beatmap.Mode = s.Mode; 151 | return s; 152 | }).ToArray(); 153 | } 154 | } 155 | 156 | async public static Task GetUserScores(long userId, UserScoreType scoreType = UserScoreType.Best, Mode mode = Mode.OSU, int limit = 1, int offset = 0, bool includeFails = true, bool includeLoved = false) 157 | { 158 | if (mode.IsSupported() == false) return null; 159 | var req = httpV1() 160 | .AppendPathSegment("get_player_scores") 161 | .SetQueryParam("scope", scoreType.ToStr()) 162 | .SetQueryParam("id", userId) 163 | .SetQueryParam("include_failed", includeFails ? 1 : 0) 164 | .SetQueryParam("include_loved", includeLoved ? 1 : 0) 165 | .SetQueryParam("mode", mode.ToNum()); 166 | 167 | 168 | req.SetQueryParam("limit", limit + offset); 169 | 170 | var res = await req.GetAsync(); 171 | if (res.StatusCode == 404) 172 | return null; 173 | else 174 | { 175 | var u = await res.GetJsonAsync(); 176 | return u?.Scores.Skip(offset).Take(limit).ToArray(); 177 | } 178 | } 179 | 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/API/PPYSB/Extensions.cs: -------------------------------------------------------------------------------- 1 | using KanonBot.API.OSU; 2 | 3 | namespace KanonBot.API.PPYSB; 4 | 5 | public static class PPYSBExtensions 6 | { 7 | public static string ToStr(this UserScoreType type) 8 | { 9 | return type switch 10 | { 11 | UserScoreType.Recent => "recent", 12 | UserScoreType.Best => "best", 13 | _ => throw new ArgumentOutOfRangeException(), 14 | }; 15 | } 16 | 17 | public static string ToDisplay(this Mode mode) 18 | { 19 | return mode switch 20 | { 21 | Mode.OSU => "vn!standard", 22 | Mode.Taiko => "vn!taiko", 23 | Mode.Fruits => "vn!catch", 24 | Mode.Mania => "vn!mania", 25 | Mode.RelaxOsu => "rx!standard", 26 | Mode.RelaxTaiko => "rx!taiko", 27 | Mode.RelaxFruits => "rx!catch", 28 | Mode.RelaxMania => "rx!mania", 29 | Mode.AutoPilotOsu => "ap!standard", 30 | Mode.AutoPilotTaiko => "ap!taiko", 31 | Mode.AutoPilotFruits => "ap!catch", 32 | Mode.AutoPilotMania => "ap!mania", 33 | _ => throw new ArgumentOutOfRangeException(), 34 | }; 35 | } 36 | 37 | public static int ToNum(this Mode mode) 38 | { 39 | return mode switch 40 | { 41 | Mode.OSU => 0, 42 | Mode.Taiko => 1, 43 | Mode.Fruits => 2, 44 | Mode.Mania => 3, 45 | Mode.RelaxOsu => 4, 46 | Mode.RelaxTaiko => 5, 47 | Mode.RelaxFruits => 6, 48 | Mode.RelaxMania => 7, 49 | Mode.AutoPilotOsu => 8, 50 | Mode.AutoPilotTaiko => 9, 51 | Mode.AutoPilotFruits => 10, 52 | Mode.AutoPilotMania => 11, 53 | _ => throw new ArgumentOutOfRangeException() 54 | }; 55 | } 56 | 57 | public static Mode ToRx(this Mode mode) 58 | { 59 | return mode switch 60 | { 61 | Mode.OSU => Mode.RelaxOsu, 62 | Mode.Taiko => Mode.RelaxTaiko, 63 | Mode.Fruits => Mode.RelaxFruits, 64 | Mode.Mania => Mode.RelaxMania, 65 | Mode.RelaxOsu => Mode.RelaxOsu, 66 | Mode.RelaxTaiko => Mode.RelaxTaiko, 67 | Mode.RelaxFruits => Mode.RelaxFruits, 68 | Mode.RelaxMania => Mode.RelaxMania, 69 | Mode.AutoPilotOsu => Mode.RelaxOsu, 70 | Mode.AutoPilotTaiko => Mode.RelaxTaiko, 71 | Mode.AutoPilotFruits => Mode.RelaxFruits, 72 | Mode.AutoPilotMania => Mode.RelaxMania, 73 | _ => throw new ArgumentOutOfRangeException() 74 | }; 75 | } 76 | 77 | public static Mode ToAp(this Mode mode) 78 | { 79 | return mode switch 80 | { 81 | Mode.OSU => Mode.AutoPilotOsu, 82 | Mode.Taiko => Mode.AutoPilotTaiko, 83 | Mode.Fruits => Mode.AutoPilotFruits, 84 | Mode.Mania => Mode.AutoPilotMania, 85 | Mode.RelaxOsu => Mode.AutoPilotOsu, 86 | Mode.RelaxTaiko => Mode.AutoPilotTaiko, 87 | Mode.RelaxFruits => Mode.AutoPilotFruits, 88 | Mode.RelaxMania => Mode.AutoPilotMania, 89 | Mode.AutoPilotOsu => Mode.AutoPilotOsu, 90 | Mode.AutoPilotTaiko => Mode.AutoPilotTaiko, 91 | Mode.AutoPilotFruits => Mode.AutoPilotFruits, 92 | Mode.AutoPilotMania => Mode.AutoPilotMania, 93 | _ => throw new ArgumentOutOfRangeException() 94 | }; 95 | } 96 | 97 | public static bool IsSupported(this Mode mode) 98 | { 99 | return mode switch 100 | { 101 | Mode.OSU => true, 102 | Mode.Taiko => true, 103 | Mode.Fruits => true, 104 | Mode.Mania => true, 105 | Mode.RelaxOsu => true, 106 | Mode.RelaxTaiko => true, 107 | Mode.RelaxFruits => true, 108 | Mode.RelaxMania => false, 109 | Mode.AutoPilotOsu => true, 110 | Mode.AutoPilotTaiko => false, 111 | Mode.AutoPilotFruits => false, 112 | Mode.AutoPilotMania => false, 113 | _ => throw new ArgumentOutOfRangeException() 114 | }; 115 | } 116 | 117 | public static Mode? ParsePpysbMode(this string value) 118 | { 119 | value = value.ToLower(); // 大写字符转小写 120 | return value switch 121 | { 122 | "0" or "osu" or "std" => Mode.OSU, 123 | "1" or "taiko" or "tko" => Mode.Taiko, 124 | "2" or "fruits" or "catch" or "ctb" => Mode.Fruits, 125 | "3" or "mania" or "m" => Mode.Mania, 126 | "4" or "rx0" or "rxosu" or "rxstd" => Mode.RelaxOsu, 127 | "5" or "rx1" or "rxtaiko" or "rxtko" => Mode.RelaxTaiko, 128 | "6" or "rx2" or "rxfruits" or "rxcatch" or "rxctb" => Mode.RelaxFruits, 129 | "7" or "rx3" or "rxmania" or "rxm" => Mode.RelaxMania, 130 | "8" or "ap0" or "aposu" or "apstd" => Mode.AutoPilotOsu, 131 | "9" or "ap1" or "aptaiko" or "aptko" => Mode.AutoPilotTaiko, 132 | "10" or "ap2" or "apfruits" or "apcatch" or "apctb" => Mode.AutoPilotFruits, 133 | "11" or "ap3" or "apmania" or "apm" => Mode.AutoPilotMania, 134 | _ => null 135 | }; 136 | } 137 | 138 | public static Mode? ToPpysbMode(this int value) 139 | { 140 | return value switch 141 | { 142 | 0 => Mode.OSU, 143 | 1 => Mode.Taiko, 144 | 2 => Mode.Fruits, 145 | 3 => Mode.Mania, 146 | 4 => Mode.RelaxOsu, 147 | 5 => Mode.RelaxTaiko, 148 | 6 => Mode.RelaxFruits, 149 | 7 => Mode.RelaxMania, 150 | 8 => Mode.AutoPilotOsu, 151 | 9 => Mode.AutoPilotTaiko, 152 | 10 => Mode.AutoPilotFruits, 153 | 11 => Mode.AutoPilotMania, 154 | _ => null 155 | }; 156 | } 157 | 158 | } -------------------------------------------------------------------------------- /src/API/PPYSB/Mode.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS8618 // 非null 字段未初始化 2 | using System.ComponentModel; 3 | 4 | namespace KanonBot.API.PPYSB; 5 | 6 | public enum Mode 7 | { 8 | OSU = 0, 9 | Taiko = 1, 10 | Fruits = 2, 11 | Mania = 3, 12 | RelaxOsu = 4, 13 | RelaxTaiko = 5, 14 | RelaxFruits = 6, 15 | RelaxMania = 7, 16 | AutoPilotOsu = 8, 17 | AutoPilotTaiko = 9, 18 | AutoPilotFruits = 10, 19 | AutoPilotMania = 11, 20 | } 21 | -------------------------------------------------------------------------------- /src/API/PPYSB/Models/Beatmap.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS8618 // 非null 字段未初始化 2 | using System.ComponentModel; 3 | using System.Text.RegularExpressions; 4 | using KanonBot.Serializer; 5 | using Newtonsoft.Json; 6 | using Newtonsoft.Json.Linq; 7 | using NullValueHandling = Newtonsoft.Json.NullValueHandling; 8 | 9 | namespace KanonBot.API.PPYSB; 10 | 11 | public partial class Models 12 | { 13 | public class MapResponseV2 : ApiResponseV2 { 14 | [JsonProperty("data")] 15 | public Beatmap Data { get; set; } 16 | } 17 | 18 | public class Beatmap { 19 | [JsonProperty("md5")] 20 | public string Md5 { get; set; } 21 | 22 | [JsonProperty("id")] 23 | public long BeatmapId { get; set; } 24 | 25 | [JsonProperty("set_id")] 26 | public long BeatmapsetId { get; set; } 27 | 28 | [JsonProperty("artist")] 29 | public string Artist { get; set; } 30 | 31 | [JsonProperty("title")] 32 | public string Title { get; set; } 33 | 34 | [JsonProperty("version")] 35 | public string Version { get; set; } 36 | 37 | [JsonProperty("creator")] 38 | public string Creator { get; set; } 39 | 40 | [JsonProperty("last_update")] 41 | public DateTimeOffset LastUpdate { get; set; } 42 | 43 | [JsonProperty("total_length")] 44 | public uint TotalLength { get; set; } 45 | 46 | [JsonProperty("max_combo")] 47 | public long MaxCombo { get; set; } 48 | 49 | [JsonProperty("status")] 50 | public Status Status { get; set; } 51 | 52 | [JsonProperty("plays")] 53 | public long Plays { get; set; } 54 | 55 | [JsonProperty("passes")] 56 | public long Passes { get; set; } 57 | 58 | [JsonProperty("mode")] 59 | public Mode Mode { get; set; } 60 | 61 | [JsonProperty("bpm")] 62 | public double BPM { get; set; } 63 | 64 | [JsonProperty("cs")] 65 | public double CS { get; set; } 66 | 67 | [JsonProperty("od")] 68 | public double OD { get; set; } 69 | 70 | [JsonProperty("ar")] 71 | public double AR { get; set; } 72 | 73 | [JsonProperty("hp")] 74 | public double HP { get; set; } 75 | 76 | [JsonProperty("diff")] 77 | public double DifficultyRating { get; set; } 78 | } 79 | 80 | public enum Status 81 | { 82 | NotSubmitted = -1, 83 | Pending = 0, 84 | UpdateAvailable = 1, 85 | Ranked = 2, 86 | Approved = 3, 87 | Qualified = 4, 88 | Loved = 5 89 | } 90 | } 91 | 92 | // { 93 | // "md5": "4e392566be350059b31f029ad4d889cf", 94 | // "id": 81136, 95 | // "set_id": 23754, 96 | // "artist": "USBduck", 97 | // "title": "Keyboard Cat ZONE", 98 | // "version": "Party Time", 99 | // "creator": "Kurosanyan", 100 | // "last_update": "2012-05-19T09:38:11", 101 | // "total_length": 105, 102 | // "max_combo": 798, 103 | // "status": 2, 104 | // "plays": 40, 105 | // "passes": 10, 106 | // "mode": 0, 107 | // "bpm": 165, 108 | // "cs": 4, 109 | // "od": 7, 110 | // "ar": 9, 111 | // "hp": 7, 112 | // "diff": 5.447 113 | // } -------------------------------------------------------------------------------- /src/API/PPYSB/Models/Privileges.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Text.RegularExpressions; 3 | using KanonBot.Serializer; 4 | using Newtonsoft.Json; 5 | using Newtonsoft.Json.Linq; 6 | using NullValueHandling = Newtonsoft.Json.NullValueHandling; 7 | 8 | namespace KanonBot.API.PPYSB; 9 | 10 | public partial class Models 11 | { 12 | public enum Privileges 13 | { 14 | UNRESTRICTED = 1 << 0, 15 | VERIFIED = 1 << 1, 16 | 17 | WHITELISTED = 1 << 2, 18 | 19 | SUPPORTER = 1 << 4, 20 | PREMIUM = 1 << 5, 21 | 22 | ALUMNI = 1 << 7, 23 | 24 | TOURNEY_MANAGER = 1 << 10, 25 | NOMINATOR = 1 << 11, 26 | MODERATOR = 1 << 12, 27 | ADMINISTRATOR = 1 << 13, 28 | DEVELOPER = 1 << 14, 29 | 30 | DONATOR = SUPPORTER | PREMIUM, 31 | STAFF = MODERATOR | ADMINISTRATOR | DEVELOPER, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/API/PPYSB/Models/Response.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS8618 // 非null 字段未初始化 2 | using System.ComponentModel; 3 | using System.Text.RegularExpressions; 4 | using KanonBot.Serializer; 5 | using Newtonsoft.Json; 6 | using Newtonsoft.Json.Linq; 7 | using NullValueHandling = Newtonsoft.Json.NullValueHandling; 8 | 9 | namespace KanonBot.API.PPYSB; 10 | 11 | public partial class Models 12 | { 13 | public class ApiResponse { 14 | [JsonProperty("status")] 15 | public string Status { get; set; } 16 | } 17 | public class ApiResponseV2 { 18 | [JsonProperty("status")] 19 | public string Status { get; set; } 20 | 21 | [JsonProperty("meta")] 22 | public Dictionary Meta { get; set; } 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /src/API/PPYSB/Models/Score.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS8618 // 非null 字段未初始化 2 | using System.ComponentModel; 3 | using System.Text.RegularExpressions; 4 | using KanonBot.Serializer; 5 | using Newtonsoft.Json; 6 | using Newtonsoft.Json.Linq; 7 | using NullValueHandling = Newtonsoft.Json.NullValueHandling; 8 | 9 | namespace KanonBot.API.PPYSB; 10 | 11 | public partial class Models 12 | { 13 | public class ScoreResponseV2 : ApiResponseV2 { 14 | [JsonProperty("data")] 15 | public Score[] Data { get; set; } 16 | } 17 | 18 | public class PlayerScoreResponse : ApiResponse { 19 | [JsonProperty("scores")] 20 | public Score[] Scores { get; set; } 21 | 22 | [JsonProperty("player")] 23 | public ScoreUser Player { get; set; } 24 | } 25 | 26 | public class ScoreResponse : ApiResponse { 27 | [JsonProperty("score")] 28 | public Score Score { get; set; } 29 | } 30 | 31 | 32 | public class Score { 33 | [JsonProperty("id")] 34 | public long Id { get; set; } 35 | 36 | [JsonProperty("score")] 37 | public uint Scores { get; set; } 38 | 39 | [JsonProperty("pp")] 40 | public double PP { get; set; } 41 | 42 | [JsonProperty("acc")] 43 | public double Acc { get; set; } 44 | 45 | [JsonProperty("max_combo")] 46 | public uint MaxCombo { get; set; } 47 | 48 | [JsonProperty("mods")] 49 | public uint Mods { get; set; } 50 | 51 | [JsonProperty("n300")] 52 | public uint Count300 { get; set; } 53 | 54 | [JsonProperty("n100")] 55 | public uint Count100 { get; set; } 56 | 57 | [JsonProperty("n50")] 58 | public uint Count50 { get; set; } 59 | 60 | [JsonProperty("nmiss")] 61 | public uint CountMiss { get; set; } 62 | 63 | [JsonProperty("ngeki")] 64 | public uint CountGeki { get; set; } 65 | 66 | [JsonProperty("nkatu")] 67 | public uint CountKatu { get; set; } 68 | 69 | [JsonProperty("grade")] 70 | public string Rank { get; set; } 71 | 72 | [JsonProperty("status")] 73 | public uint Status { get; set; } 74 | 75 | [JsonProperty("mode")] 76 | public Mode Mode { get; set; } 77 | 78 | [JsonProperty("play_time")] 79 | public DateTimeOffset PlayTime { get; set; } 80 | 81 | [JsonProperty("time_elapsed")] 82 | public string TimeElapsed { get; set; } 83 | 84 | [JsonProperty("perfect")] 85 | public uint Perfect { get; set; } 86 | 87 | [JsonProperty("beatmap")] 88 | public Beatmap Beatmap { get; set; } 89 | } 90 | 91 | public class ScoreUser { 92 | 93 | [JsonProperty("id")] 94 | public uint Id { get; set; } 95 | 96 | [JsonProperty("name")] 97 | public string Username { get; set; } 98 | 99 | [JsonProperty("clan")] 100 | public object? Clan { get; set; } 101 | 102 | } 103 | 104 | } 105 | 106 | // { 107 | // "status": "success", 108 | // "scores": [ 109 | // { 110 | // "id": 2324, 111 | // "score": 10134396, 112 | // "pp": 111.765, 113 | // "acc": 84.706, 114 | // "max_combo": 795, 115 | // "mods": 8, 116 | // "n300": 459, 117 | // "n100": 134, 118 | // "n50": 2, 119 | // "nmiss": 0, 120 | // "ngeki": 26, 121 | // "nkatu": 48, 122 | // "grade": "B", 123 | // "status": 2, 124 | // "mode": 0, 125 | // "play_time": "2020-02-18T17:40:57", 126 | // "time_elapsed": 0, 127 | // "perfect": 0, 128 | // "beatmap": { 129 | // "md5": "4e392566be350059b31f029ad4d889cf", 130 | // "id": 81136, 131 | // "set_id": 23754, 132 | // "artist": "USBduck", 133 | // "title": "Keyboard Cat ZONE", 134 | // "version": "Party Time", 135 | // "creator": "Kurosanyan", 136 | // "last_update": "2012-05-19T09:38:11", 137 | // "total_length": 105, 138 | // "max_combo": 798, 139 | // "status": 2, 140 | // "plays": 40, 141 | // "passes": 10, 142 | // "mode": 0, 143 | // "bpm": 165, 144 | // "cs": 4, 145 | // "od": 7, 146 | // "ar": 9, 147 | // "hp": 7, 148 | // "diff": 5.447 149 | // } 150 | // } 151 | // ], 152 | // "player": { 153 | // "id": 1104, 154 | // "name": "水瓶", 155 | // "clan": null 156 | // } 157 | // } -------------------------------------------------------------------------------- /src/API/oss.cs: -------------------------------------------------------------------------------- 1 | 2 | using Aliyun.OSS.Common; 3 | using Aliyun.OSS; 4 | 5 | 6 | namespace KanonBot.API; 7 | public class Ali 8 | { 9 | private static Config.OSS config = Config.inner!.oss!; 10 | public static string? PutFile(string key, byte[] data) 11 | { 12 | using var stream = Utils.Byte2Stream(data); 13 | var client = new OssClient(config.endPoint, config.accessKeyId!, config.accessKeySecret!); 14 | try 15 | { 16 | var res = client.PutObject(config.bucketName!, key, stream); 17 | return config.url + key; 18 | } 19 | catch (OssException ex) 20 | { 21 | Log.Error("oss上传文件失败: {0}; Error info: {1}. \nRequestID:{2}\tHostID:{3}", 22 | ex.ErrorCode, ex.Message, ex.RequestId, ex.HostId); 23 | } 24 | catch (Exception ex) 25 | { 26 | Log.Error("oss上传文件失败: {0}", ex.Message); 27 | } 28 | return null; 29 | } 30 | 31 | // ext 为文件扩展名,不带点 32 | public static string? PutFile(byte[] data, string ext, bool temp = true) 33 | { 34 | if (temp) 35 | return PutFile($"temp-{Guid.NewGuid().ToString()[0..6]}.{ext}", data); 36 | else 37 | return PutFile($"{Guid.NewGuid().ToString()[0..6]}.{ext}", data); 38 | } 39 | 40 | public static void ListAllBuckets() 41 | { 42 | var client = new OssClient(config.endPoint, config.accessKeyId!, config.accessKeySecret!); 43 | var buckets = client.ListBuckets(); 44 | 45 | foreach (var bucket in buckets) 46 | { 47 | Log.Information(bucket.Name + ", " + bucket.Location + ", " + bucket.Owner); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/Error.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | namespace KanonBot; 4 | public class KanonError : Exception 5 | { 6 | public KanonError(string message) : base(message) 7 | { 8 | 9 | } 10 | } -------------------------------------------------------------------------------- /src/Event.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using Newtonsoft.Json; 3 | using KanonBot.Drivers; 4 | 5 | namespace KanonBot.Event; 6 | 7 | public interface IEvent 8 | { 9 | string ToString(); 10 | } 11 | 12 | public class RawEvent : IEvent 13 | { 14 | public object value { get; set; } 15 | public RawEvent(object e) 16 | { 17 | this.value = e; 18 | } 19 | 20 | public override string ToString() 21 | { 22 | return value switch { 23 | JObject j => j.ToString(Formatting.None), 24 | _ => $"{value}", 25 | }; 26 | } 27 | } 28 | 29 | 30 | public class Ready : IEvent 31 | { 32 | public string selfId { get; set; } 33 | public Platform platform { get; set; } 34 | public Ready(string selfId, Platform platform) 35 | { 36 | this.selfId = selfId; 37 | this.platform = platform; 38 | } 39 | 40 | public override string ToString() 41 | { 42 | return $""; 43 | } 44 | } 45 | 46 | public class HeartBeat : IEvent 47 | { 48 | public DateTimeOffset value { get; set; } 49 | public HeartBeat(long timestamp) 50 | { 51 | this.value = Utils.TimeStampSecToDateTime(timestamp).ToLocalTime(); 52 | } 53 | 54 | public override string ToString() 55 | { 56 | return $""; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/FodyWeavers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | osu.Game.Resources 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using global::System; 2 | global using global::System.Collections.Generic; 3 | global using global::System.Linq; 4 | global using global::System.Threading; 5 | global using global::System.Threading.Tasks; 6 | 7 | global using Flurl; 8 | global using Flurl.Http; 9 | global using Serilog; 10 | 11 | global using LanguageExt; 12 | global using LanguageExt.Common; 13 | global using static LanguageExt.Prelude; 14 | global using LanguageExt.Pretty; 15 | 16 | global using KanonBot; 17 | global using static KanonBot.API.OSU.OSUExtensions; 18 | global using static KanonBot.API.PPYSB.PPYSBConverters; 19 | global using static KanonBot.API.PPYSB.PPYSBExtensions; -------------------------------------------------------------------------------- /src/KanonBot.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net9.0 5 | KanonBot 6 | disable 7 | enable 8 | true 9 | true 10 | true 11 | portable 12 | en-US 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/Mail.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable IDE0044 // 添加只读修饰符 2 | #pragma warning disable CS8602 // 解引用可能出现空引用。 3 | #pragma warning disable CS8604 // 解引用可能出现空引用。 4 | 5 | using System.Net; 6 | using System.Security.Authentication; 7 | using MailKit; 8 | using MailKit.Net.Smtp; 9 | using MimeKit; 10 | 11 | namespace KanonBot; 12 | public static class Mail 13 | { 14 | private static Config.Base config = Config.inner!; 15 | public class MailStruct 16 | { 17 | public required IEnumerable MailTo; //收件人,可添加多个 18 | public IEnumerable MailCC = []; //抄送人,不建议添加 19 | public required string Subject; //标题 20 | public required string Body; //正文 21 | public required bool IsBodyHtml; 22 | } 23 | public static async Task Send(MailStruct ms) 24 | { 25 | MimeKit.MimeMessage message = new(); 26 | ms.MailTo.Iter(s => message.To.Add(new MailboxAddress(s, s))); //设置收件人 27 | if (message.To.Count == 0) return; 28 | message.From.Add(new MailboxAddress(config.mail.username, config.mail.username)); 29 | 30 | message.Subject = ms.Subject; 31 | if (ms.IsBodyHtml) { 32 | message.Body = new TextPart("html") { Text = ms.Body }; 33 | } else { 34 | message.Body = new TextPart("plain") { Text = ms.Body }; 35 | } 36 | // var client = new SmtpClient(config.mail.smtpHost, config.mail.smtpPort) 37 | // { 38 | // Credentials = new System.Net.NetworkCredential(config.mail.username, config.mail.password), //设置邮箱用户名与密码 39 | // EnableSsl = true //启用SSL 40 | // }; //设置邮件服务器 41 | // ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; 42 | // client.Send(message); //发送 43 | 44 | using var client = new SmtpClient(); 45 | client.Connect(config.mail.smtpHost, config.mail.smtpPort, MailKit.Security.SecureSocketOptions.StartTls); 46 | client.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13; // 只允许 TLS 1.2 和 1.3 47 | client.Authenticate(config.mail.username, config.mail.password); 48 | await client.SendAsync(message); 49 | client.Disconnect(true); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Message/AtSegment.cs: -------------------------------------------------------------------------------- 1 | using KanonBot.Drivers; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Linq; 4 | 5 | namespace KanonBot.Message; 6 | 7 | public class AtSegment : IMsgSegment, IEquatable 8 | { 9 | public Platform platform { get; set; } 10 | // all 表示全体成员 11 | public string value { get; set; } 12 | public AtSegment(string target, Platform platform) 13 | { 14 | this.value = target; 15 | this.platform = platform; 16 | } 17 | 18 | public string Build() 19 | { 20 | var platform = this.platform switch 21 | { 22 | Platform.OneBot => "qq", 23 | Platform.Guild => "gulid", 24 | Platform.Discord => "discord", 25 | Platform.KOOK => "kook", 26 | _ => "unknown", 27 | }; 28 | return $"{platform}={value}"; 29 | } 30 | 31 | public bool Equals(AtSegment? other) 32 | { 33 | return other != null && this.value == other.value && this.platform == other.platform; 34 | } 35 | 36 | public bool Equals(IMsgSegment? other) 37 | { 38 | if (other is AtSegment r) 39 | return this.Equals(r); 40 | else 41 | return false; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/Message/Chain.cs: -------------------------------------------------------------------------------- 1 | using KanonBot.Drivers; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Linq; 4 | 5 | namespace KanonBot.Message; 6 | 7 | public class Chain: IEquatable 8 | { 9 | List msgList { get; set; } 10 | public Chain() 11 | { 12 | this.msgList = new(); 13 | } 14 | 15 | public static Chain FromList(List list) 16 | { 17 | return new Chain { msgList = list }; 18 | } 19 | 20 | public void Add(IMsgSegment n) 21 | { 22 | this.msgList.Add(n); 23 | } 24 | 25 | public Chain msg(string v) 26 | { 27 | this.Add(new TextSegment(v)); 28 | return this; 29 | } 30 | public Chain at(string v, Platform p) 31 | { 32 | this.Add(new AtSegment(v, p)); 33 | return this; 34 | } 35 | public Chain image(string v, ImageSegment.Type t) 36 | { 37 | this.Add(new ImageSegment(v, t)); 38 | return this; 39 | } 40 | 41 | public IEnumerable Iter() 42 | { 43 | return this.msgList.AsEnumerable(); 44 | } 45 | 46 | 47 | public string Build() 48 | { 49 | var raw = ""; 50 | foreach (var item in this.msgList) 51 | { 52 | raw += item.Build(); 53 | } 54 | return raw; 55 | } 56 | 57 | public override string ToString() 58 | { 59 | return this.Build(); 60 | } 61 | 62 | public int Length() => this.msgList.Count; 63 | public bool StartsWith(string s) 64 | { 65 | if (this.msgList.Count == 0) 66 | return false; 67 | else 68 | return this.msgList[0] is TextSegment t && t.value.StartsWith(s); 69 | } 70 | public bool StartsWith(AtSegment at) 71 | { 72 | if (this.msgList.Count == 0) 73 | return false; 74 | else 75 | return this.msgList[0] is AtSegment t && t.value == at.value && t.platform == at.platform; 76 | } 77 | 78 | public T? Find() where T : class, IMsgSegment => 79 | this.msgList.Find(t => t is T) as T; 80 | 81 | public bool Equals(Chain? other) 82 | { 83 | if (other == null) 84 | return false; 85 | if (this.msgList.Count != other.msgList.Count) 86 | return false; 87 | for (int i = 0; i < this.msgList.Count; i++) 88 | { 89 | if (!this.msgList[i].Equals(other.msgList[i])) 90 | return false; 91 | } 92 | return true; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Message/EmojiSegment.cs: -------------------------------------------------------------------------------- 1 | using KanonBot.Drivers; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Linq; 4 | 5 | namespace KanonBot.Message; 6 | 7 | public class EmojiSegment : IMsgSegment, IEquatable 8 | { 9 | public string value { get; set; } 10 | public EmojiSegment(string value) 11 | { 12 | this.value = value; 13 | } 14 | 15 | public string Build() 16 | { 17 | return $""; 18 | } 19 | 20 | public bool Equals(EmojiSegment? other) 21 | { 22 | return other != null && this.value == other.value; 23 | } 24 | 25 | public bool Equals(IMsgSegment? other) 26 | { 27 | if (other is EmojiSegment r) 28 | return this.Equals(r); 29 | else 30 | return false; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Message/IMsgSegment.cs: -------------------------------------------------------------------------------- 1 | using KanonBot.Drivers; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Linq; 4 | 5 | namespace KanonBot.Message; 6 | 7 | public interface IMsgSegment : IEquatable 8 | { 9 | string Build(); 10 | } 11 | -------------------------------------------------------------------------------- /src/Message/ImageSegment.cs: -------------------------------------------------------------------------------- 1 | using KanonBot.Drivers; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Linq; 4 | 5 | namespace KanonBot.Message; 6 | 7 | public class ImageSegment : IMsgSegment, IEquatable 8 | { 9 | public enum Type 10 | { 11 | File, // 如果是file就是文件地址 12 | Base64, 13 | Url 14 | } 15 | public Type t { get; set; } 16 | public string value { get; set; } 17 | public ImageSegment(string value, Type t) 18 | { 19 | this.value = value; 20 | this.t = t; 21 | } 22 | 23 | public string Build() 24 | { 25 | return this.t switch 26 | { 27 | Type.File => $"", 28 | Type.Base64 => $"", 29 | Type.Url => $"", 30 | // 保险 31 | _ => "", 32 | }; 33 | } 34 | 35 | public bool Equals(ImageSegment? other) 36 | { 37 | return other != null && this.value == other.value && this.t == other.t; 38 | } 39 | 40 | public bool Equals(IMsgSegment? other) 41 | { 42 | if (other is ImageSegment r) 43 | return this.Equals(r); 44 | else 45 | return false; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Message/RawSegment.cs: -------------------------------------------------------------------------------- 1 | using KanonBot.Drivers; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Linq; 4 | 5 | namespace KanonBot.Message; 6 | 7 | public class RawSegment : IMsgSegment, IEquatable 8 | { 9 | public Object value { get; set; } 10 | public string type { get; set; } 11 | public RawSegment(string type, Object value) 12 | { 13 | this.type = type; 14 | this.value = value; 15 | } 16 | 17 | public string Build() 18 | { 19 | return value switch { 20 | JObject j => $"", 21 | _ => $"", 22 | }; 23 | } 24 | 25 | public bool Equals(RawSegment? other) 26 | { 27 | return other != null && this.type == other.type && this.value == other.value; 28 | } 29 | 30 | public bool Equals(IMsgSegment? other) 31 | { 32 | if (other is RawSegment r) 33 | return this.Equals(r); 34 | else 35 | return false; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Message/TextSegment.cs: -------------------------------------------------------------------------------- 1 | using KanonBot.Drivers; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Linq; 4 | 5 | namespace KanonBot.Message; 6 | 7 | public class TextSegment : IMsgSegment, IEquatable 8 | { 9 | public string value { get; set; } 10 | public TextSegment(string msg) 11 | { 12 | this.value = msg; 13 | } 14 | 15 | 16 | public string Build() 17 | { 18 | return value.ToString(); 19 | } 20 | 21 | public bool Equals(TextSegment? other) 22 | { 23 | return other != null && this.value == other.value; 24 | } 25 | 26 | public bool Equals(IMsgSegment? other) 27 | { 28 | if (other is TextSegment r) 29 | return this.Equals(r); 30 | else 31 | return false; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/OsuPerformance/Oppai.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using KanonBot.LegacyImage; 3 | using KanonBot.Serializer; 4 | using LanguageExt.ClassInstances.Pred; 5 | using RosuPP; 6 | using static KanonBot.API.OSU.OSUExtensions; 7 | using Oppai = OppaiSharp; 8 | using OSU = KanonBot.API.OSU; 9 | 10 | namespace KanonBot.OsuPerformance; 11 | 12 | public static class OppaiCalculator 13 | { 14 | public static Oppai.Beatmap LoadBeatmap(byte[] b) 15 | { 16 | using var stream = new MemoryStream(b, false); 17 | using var reader = new StreamReader(stream); 18 | return Oppai.Beatmap.Read(reader); 19 | } 20 | 21 | public static Draw.ScorePanelData CalculatePanelData(byte[] b, API.OSU.Models.ScoreLazer score) 22 | { 23 | var data = new Draw.ScorePanelData { scoreInfo = score }; 24 | if (score.IsLazer) { 25 | data.server = "Lazer"; 26 | } 27 | var statistics = data.scoreInfo.ConvertStatistics; 28 | 29 | using var rosubeatmap = Beatmap.FromBytes(b); 30 | 31 | Mode rmode = data.scoreInfo.Mode.ToRosu(); 32 | rosubeatmap.Convert(rmode); 33 | 34 | var clockRate = 1.0; 35 | using var mods = Mods.FromJson(data.scoreInfo.JsonMods, rmode); 36 | 37 | using var builder = BeatmapAttributesBuilder.New(); 38 | builder.Mode(rmode); 39 | builder.Mods(mods); 40 | var bmAttr = builder.Build(rosubeatmap); 41 | var bpm = bmAttr.clock_rate * rosubeatmap.Bpm(); 42 | clockRate = bmAttr.clock_rate; 43 | 44 | var beatmap = LoadBeatmap(b); 45 | var dAttr = new Oppai.DiffCalc().Calc(beatmap, (Oppai.Mods)mods.Bits()); 46 | var bAttr = new Oppai.PPv2( 47 | new Oppai.PPv2Parameters( 48 | beatmap, 49 | dAttr, 50 | c300: (int)statistics.CountGreat, 51 | c100: (int)statistics.CountOk, 52 | c50: (int)statistics.CountMeh, 53 | cMiss: (int)statistics.CountMiss, 54 | combo: (int)data.scoreInfo.MaxCombo, 55 | mods: (Oppai.Mods)mods.Bits() 56 | ) 57 | ); 58 | var maxcombo = beatmap.GetMaxCombo(); 59 | 60 | data.ppInfo = PPInfo.New(score, bAttr, dAttr, bmAttr, bpm, clockRate, maxcombo); 61 | 62 | // 5种acc + 全连 63 | double[] accs = [100.00, 99.00, 98.00, 97.00, 95.00, data.scoreInfo.AccAuto * 100.00]; 64 | 65 | data.ppInfo.ppStats = []; 66 | 67 | for (int i = 0; i < accs.Length; i++) 68 | { 69 | ref var acc = ref accs[i]; 70 | 71 | using var p = Performance.New(); 72 | p.Lazer(score.IsLazer); 73 | p.Mode(rmode); 74 | p.Mods(mods); 75 | p.Accuracy(acc); 76 | var state = p.GenerateState(rosubeatmap); 77 | 78 | bAttr = new Oppai.PPv2( 79 | new Oppai.PPv2Parameters( 80 | beatmap, 81 | dAttr, 82 | c300: (int)state.n300, 83 | c100: (int)state.n100, 84 | c50: (int)state.n50, 85 | cMiss: (int)state.misses, 86 | combo: (int)state.max_combo, 87 | mods: (Oppai.Mods)mods.Bits() 88 | ) 89 | ); 90 | 91 | data.ppInfo.ppStats.Add(PPInfo.New(score, bAttr, dAttr, bmAttr, bpm, clockRate, maxcombo).ppStat); 92 | } 93 | 94 | data.mode = rmode; 95 | 96 | return data; 97 | } 98 | 99 | public static PPInfo CalculateData(byte[] b, API.OSU.Models.ScoreLazer score) 100 | { 101 | var statistics = score.ConvertStatistics; 102 | 103 | using var rosubeatmap = Beatmap.FromBytes(b); 104 | 105 | Mode rmode = score.Mode.ToRosu(); 106 | 107 | var mods_json = score.JsonMods; 108 | using var mods = Mods.FromJson(mods_json, rmode); 109 | 110 | using var builder = BeatmapAttributesBuilder.New(); 111 | builder.Mode(rmode); 112 | builder.Mods(mods); 113 | var bmAttr = builder.Build(rosubeatmap); 114 | 115 | var bpm = bmAttr.clock_rate * rosubeatmap.Bpm(); 116 | var clockRate = bmAttr.clock_rate; 117 | 118 | var beatmap = LoadBeatmap(b); 119 | var dAttr = new Oppai.DiffCalc().Calc(beatmap, (Oppai.Mods)mods.Bits()); 120 | var bAttr = new Oppai.PPv2( 121 | new Oppai.PPv2Parameters( 122 | beatmap, 123 | dAttr, 124 | c300: (int)statistics.CountGreat, 125 | c100: (int)statistics.CountOk, 126 | c50: (int)statistics.CountMeh, 127 | cMiss: (int)statistics.CountMiss, 128 | combo: (int)score.MaxCombo, 129 | mods: (Oppai.Mods)mods.Bits() 130 | ) 131 | ); 132 | var maxcombo = beatmap.GetMaxCombo(); 133 | 134 | return PPInfo.New(score, bAttr, dAttr, bmAttr, bpm, clockRate, maxcombo); 135 | } 136 | } 137 | 138 | public partial class PPInfo 139 | { 140 | public static PPInfo New( 141 | API.OSU.Models.ScoreLazer score, 142 | Oppai.PPv2 result, 143 | Oppai.DiffCalc dAttr, 144 | RosuPP.BeatmapAttributes bmAttr, 145 | double bpm, 146 | double clockrate, 147 | int maxcombo 148 | ) 149 | { 150 | return new PPInfo() 151 | { 152 | star = dAttr.Total, 153 | CS = bmAttr.cs, 154 | HP = bmAttr.hp, 155 | AR = bmAttr.ar, 156 | OD = bmAttr.od, 157 | accuracy = result.Acc, 158 | maxCombo = (uint)maxcombo, 159 | bpm = bpm, 160 | clockrate = clockrate, 161 | ppStat = new PPInfo.PPStat() 162 | { 163 | total = result.Total, 164 | aim = result.Aim, 165 | speed = result.Speed, 166 | acc = result.Acc, 167 | strain = null, 168 | flashlight = null, 169 | }, 170 | ppStats = null 171 | }; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/OsuPerformance/PPInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.IO; 3 | using System.Runtime.InteropServices; 4 | using System.Security.Cryptography; 5 | using KanonBot.LegacyImage; 6 | using LanguageExt.ClassInstances.Pred; 7 | using static KanonBot.API.OSU.OSUExtensions; 8 | using OSU = KanonBot.API.OSU; 9 | 10 | namespace KanonBot.OsuPerformance; 11 | 12 | public partial class PPInfo 13 | { 14 | public required double star, 15 | CS, 16 | HP, 17 | AR, 18 | OD; 19 | public double? accuracy; 20 | public uint? maxCombo; 21 | public double bpm; 22 | public double clockrate; 23 | public required PPStat ppStat; 24 | public List? ppStats; 25 | 26 | public struct PPStat 27 | { 28 | public required double total; 29 | public double? aim, 30 | speed, 31 | acc, 32 | strain, 33 | flashlight; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/OsuPerformance/UniversalCalculator.cs: -------------------------------------------------------------------------------- 1 | using KanonBot.LegacyImage; 2 | 3 | namespace KanonBot.OsuPerformance 4 | { 5 | public enum CalculatorKind 6 | { 7 | Unset, 8 | Old, 9 | Osu, 10 | Rosu, 11 | Oppai, 12 | Sb 13 | } 14 | 15 | public static class UniversalCalculator 16 | { 17 | public static async Task CalculatePanelData( 18 | API.OSU.Models.ScoreLazer score, 19 | CalculatorKind kind = CalculatorKind.Unset 20 | ) 21 | { 22 | var b = await Utils.LoadOrDownloadBeatmap(score.Beatmap!); 23 | return CalculatePanelData(b, score, kind); 24 | } 25 | 26 | public static Draw.ScorePanelData CalculatePanelData( 27 | byte[] b, 28 | API.OSU.Models.ScoreLazer score, 29 | CalculatorKind kind = CalculatorKind.Unset 30 | ) 31 | { 32 | if (kind is CalculatorKind.Oppai && score.Mode != API.OSU.Mode.OSU) { 33 | kind = CalculatorKind.Unset; 34 | } 35 | 36 | // oldpp_calc 37 | if (kind == CalculatorKind.Unset && KanonBot.Config.inner!.calcOldPP) { 38 | var currpp = OsuCalculator.CalculatePanelData(b, score); 39 | var oldpp = RosuCalculator.CalculatePanelData(b, score); 40 | currpp.oldPP = oldpp.ppInfo!.ppStat.total; 41 | return currpp; 42 | } 43 | 44 | return kind switch 45 | { 46 | CalculatorKind.Osu => OsuCalculator.CalculatePanelData(b, score), 47 | CalculatorKind.Rosu => RosuCalculator.CalculatePanelData(b, score), 48 | CalculatorKind.Oppai => OppaiCalculator.CalculatePanelData(b, score), 49 | CalculatorKind.Sb => SBRosuCalculator.CalculatePanelData(b, score), 50 | CalculatorKind.Old => OppaiCalculator.CalculatePanelData(b, score), 51 | _ => RosuCalculator.CalculatePanelData(b, score), 52 | }; 53 | } 54 | 55 | public static async Task CalculateData( 56 | API.OSU.Models.ScoreLazer score, 57 | CalculatorKind kind = CalculatorKind.Unset 58 | ) 59 | { 60 | var b = await Utils.LoadOrDownloadBeatmap(score.Beatmap!); 61 | return CalculateData(b, score, kind); 62 | } 63 | 64 | public static PPInfo CalculateData( 65 | byte[] b, 66 | API.OSU.Models.ScoreLazer score, 67 | CalculatorKind kind = CalculatorKind.Unset 68 | ) 69 | { 70 | if (kind is CalculatorKind.Oppai && score.Mode != API.OSU.Mode.OSU) { 71 | kind = CalculatorKind.Unset; 72 | } 73 | 74 | return kind switch 75 | { 76 | CalculatorKind.Osu => OsuCalculator.CalculateData(b, score), 77 | CalculatorKind.Rosu => RosuCalculator.CalculateData(b, score), 78 | CalculatorKind.Oppai => OppaiCalculator.CalculateData(b,score), 79 | CalculatorKind.Sb => SBRosuCalculator.CalculateData(b,score), 80 | CalculatorKind.Old => OppaiCalculator.CalculateData(b, score), 81 | _ => RosuCalculator.CalculateData(b,score), 82 | }; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "KanonBot": { 4 | "commandName": "Project", 5 | "environmentVariables": { 6 | "KANONBOT_TEST_USER_ID": "12345678" 7 | } 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /src/Serializer.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Globalization; 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Converters; 5 | using Newtonsoft.Json.Linq; 6 | using Tomlet; 7 | using Tomlet.Exceptions; 8 | using Tomlet.Models; 9 | 10 | namespace KanonBot.Serializer; 11 | 12 | public static class Json 13 | { 14 | public static string Serialize(object? self) => 15 | JsonConvert.SerializeObject(self, Settings.Json); 16 | 17 | public static string Serialize(object? self, Formatting format) => 18 | JsonConvert.SerializeObject(self, format); 19 | 20 | public static T? Deserialize(string json) => 21 | JsonConvert.DeserializeObject(json, Settings.Json); 22 | 23 | public static JObject ToLinq(string json) => JObject.Parse(json); 24 | } 25 | 26 | public static class Toml 27 | { 28 | public static string Serialize(object self) => TomletMain.TomlStringFrom(self); 29 | public static T Deserialize(string toml) 30 | where T : class, new() => TomletMain.To(toml); 31 | } 32 | 33 | internal static class Settings 34 | { 35 | public static readonly JsonSerializerSettings Json = 36 | new() 37 | { 38 | MetadataPropertyHandling = MetadataPropertyHandling.Ignore, 39 | DateParseHandling = DateParseHandling.DateTimeOffset, 40 | Formatting = Formatting.None, 41 | Converters = 42 | { 43 | new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal } 44 | }, 45 | }; 46 | } 47 | 48 | // https://justsimplycode.com/2021/08/01/custom-json-converter-to-de-serialise-enum-description-value-to-enum-value/ 49 | public class JsonEnumConverter : JsonConverter 50 | { 51 | public override bool CanConvert(Type objectType) 52 | { 53 | var enumType = Nullable.GetUnderlyingType(objectType) ?? objectType; 54 | return enumType.IsEnum; 55 | } 56 | 57 | public override object? ReadJson( 58 | JsonReader reader, 59 | Type objectType, 60 | object? existingValue, 61 | JsonSerializer serializer 62 | ) 63 | { 64 | if (reader.Value is long) 65 | return Enum.ToObject(objectType, reader.Value); 66 | 67 | string description = reader.Value?.ToString() ?? string.Empty; 68 | 69 | if (description is null) 70 | return null; 71 | 72 | foreach (var field in objectType.GetFields()) 73 | { 74 | if ( 75 | Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) 76 | is DescriptionAttribute attribute 77 | ) 78 | { 79 | if (attribute.Description == description) 80 | return field.GetValue(null); 81 | } 82 | else 83 | { 84 | if (field.Name == description) 85 | return field.GetValue(null); 86 | } 87 | } 88 | 89 | Log.Warning("Unknown Json Enum Value: {0}", description); 90 | // throw new ArgumentException("Not found.", nameof(description)); 91 | return null; 92 | } 93 | 94 | public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) 95 | { 96 | if (string.IsNullOrEmpty(value!.ToString())) 97 | { 98 | writer.WriteValue(""); 99 | return; 100 | } 101 | writer.WriteValue(Utils.GetDesc(value)); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Utils/Avatar.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Security.Cryptography; 3 | using SixLabors.ImageSharp; 4 | using SixLabors.ImageSharp.PixelFormats; 5 | 6 | namespace KanonBot; 7 | 8 | public static partial class Utils 9 | { 10 | public static async Task> LoadOrDownloadAvatar(API.OSU.Models.User userInfo) 11 | { 12 | var filename = $"{userInfo.Id}.png"; 13 | if (userInfo.AvatarUrl.Host == "a.ppy.sb") { 14 | filename = $"sb-{userInfo.Id}.png"; 15 | } 16 | var avatarPath = $"./work/avatar/{filename}"; 17 | return await TryAsync(Utils.ReadImageRgba(avatarPath)) 18 | .IfFail(async () => 19 | { 20 | try 21 | { 22 | avatarPath = await userInfo.AvatarUrl.DownloadFileAsync( 23 | "./work/avatar/", 24 | filename 25 | ); 26 | } 27 | catch (Exception ex) 28 | { 29 | var msg = $"从API下载用户头像时发生了一处异常\n异常类型: {ex.GetType()}\n异常信息: '{ex.Message}'"; 30 | Log.Error(msg); 31 | throw; // 下载失败直接抛出error 32 | } 33 | return await Utils.ReadImageRgba(avatarPath); // 下载后再读取 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Utils/Beatmap.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Security.Cryptography; 3 | 4 | namespace KanonBot; 5 | 6 | public static partial class Utils 7 | { 8 | public static async Task LoadOrDownloadBeatmap(API.OSU.Models.Beatmap bm) 9 | { 10 | try 11 | { 12 | byte[]? f = null; 13 | // 检查有没有本地的谱面 14 | if (File.Exists($"./work/beatmap/{bm.BeatmapId}.osu")) 15 | { 16 | f = await File.ReadAllBytesAsync($"./work/beatmap/{bm.BeatmapId}.osu"); 17 | if (bm.Checksum is not null) 18 | { 19 | using (var md5 = MD5.Create()) 20 | { 21 | var hash = md5.ComputeHash(f); 22 | var hash_online = System.Convert.FromHexString(bm.Checksum); 23 | 24 | if (!hash.SequenceEqual(hash_online)) 25 | { 26 | // 删除本地的谱面 27 | File.Delete($"./work/beatmap/{bm.BeatmapId}.osu"); 28 | f = null; 29 | } 30 | } 31 | } 32 | } 33 | 34 | if (f is null) 35 | { 36 | // 下载谱面 37 | await API.OSU.Client.DownloadBeatmapFile(bm.BeatmapId); 38 | f = await File.ReadAllBytesAsync($"./work/beatmap/{bm.BeatmapId}.osu"); 39 | } 40 | 41 | // 读取铺面 42 | return f!; 43 | } 44 | catch 45 | { 46 | // 加载失败,删除重新抛异常 47 | File.Delete($"./work/beatmap/{bm.BeatmapId}.osu"); 48 | throw; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Utils/ConcurrentDictionaryExtensions.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS8714 // 类型不能用作泛型类型或方法中的类型参数。类型参数的为 Null 性与 "notnull" 约束不匹配。 2 | using System; 3 | using System.Threading.Tasks; 4 | 5 | namespace System.Collections.Concurrent 6 | { 7 | public static class ConcurrentDictionaryExtensions 8 | { 9 | /// 10 | /// Provides an alternative to that disposes values that implement . 11 | /// 12 | /// 13 | /// 14 | /// 15 | /// 16 | /// 17 | /// 18 | public static TValue GetOrAddWithDispose( 19 | this ConcurrentDictionary dictionary, 20 | TKey key, 21 | Func valueFactory) where TValue : IDisposable 22 | { 23 | while (true) 24 | { 25 | if (dictionary.TryGetValue(key, out var value)) 26 | { 27 | // Try to get the value 28 | return value; 29 | } 30 | 31 | /// Try to add the value 32 | value = valueFactory(key); 33 | if (dictionary.TryAdd(key, value)) 34 | { 35 | // Won the race, so return the instance 36 | return value; 37 | } 38 | 39 | // Lost the race, dispose the created object 40 | value.Dispose(); 41 | } 42 | } 43 | 44 | 45 | /// 46 | /// Provides an alternative to specifically for asynchronous values. The factory method will only run once. 47 | /// 48 | /// 49 | /// 50 | /// 51 | /// 52 | /// 53 | /// 54 | public static async Task GetOrAddAsync( 55 | this ConcurrentDictionary> dictionary, 56 | TKey key, 57 | Func> valueFactory) 58 | { 59 | while (true) 60 | { 61 | if (dictionary.TryGetValue(key, out var task)) 62 | { 63 | return await task; 64 | } 65 | 66 | // This is the task that we'll return to all waiters. We'll complete it when the factory is complete 67 | var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); 68 | if (dictionary.TryAdd(key, tcs.Task)) 69 | { 70 | try 71 | { 72 | var value = await valueFactory(key); 73 | tcs.TrySetResult(value); 74 | return await tcs.Task; 75 | } 76 | catch (Exception ex) 77 | { 78 | // Make sure all waiters see the exception 79 | tcs.SetException(ex); 80 | 81 | // We remove the entry if the factory failed so it's not a permanent failure 82 | // and future gets can retry (this could be a pluggable policy) 83 | dictionary.TryRemove(key, out _); 84 | throw; 85 | } 86 | } 87 | } 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /src/Utils/File.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Security.Cryptography; 3 | using SixLabors.ImageSharp; 4 | using SixLabors.ImageSharp.Formats; 5 | using SixLabors.ImageSharp.PixelFormats; 6 | using Img = SixLabors.ImageSharp.Image; 7 | 8 | namespace KanonBot; 9 | 10 | public static partial class Utils 11 | { 12 | 13 | public static string Byte2File(string fileName, byte[] buffer) 14 | { 15 | using (var fs = new FileStream(fileName, FileMode.Create, FileAccess.Write)) 16 | { 17 | fs.Write(buffer, 0, buffer.Length); 18 | } 19 | return Path.GetFullPath(fileName);; 20 | } 21 | 22 | public static Stream LoadFile2ReadStream(string filePath) 23 | { 24 | var fs = new FileStream( 25 | filePath, 26 | FileMode.Open, 27 | FileAccess.Read, 28 | FileShare.ReadWrite 29 | ); 30 | return fs; 31 | } 32 | 33 | public static async Task LoadFile2Byte(string filePath) 34 | { 35 | using var fs = LoadFile2ReadStream(filePath); 36 | byte[] bt = new byte[fs.Length]; 37 | var mem = new Memory(bt); 38 | await fs.ReadExactlyAsync(mem); 39 | fs.Close(); 40 | return mem.ToArray(); 41 | } 42 | 43 | async public static Task> ReadImageRgba(string path) 44 | { 45 | return await Img.LoadAsync(path); 46 | } 47 | 48 | async public static Task<(Image, IImageFormat)> ReadImageRgbaWithFormat(string path) 49 | { 50 | using var s = Utils.LoadFile2ReadStream(path); 51 | var img = await Img.LoadAsync(s); 52 | return (img, img.Metadata.DecodedImageFormat!); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Utils/Image.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Security.Cryptography; 3 | using SixLabors.ImageSharp; 4 | using SixLabors.ImageSharp.PixelFormats; 5 | using SixLabors.ImageSharp.Processing; 6 | 7 | namespace KanonBot; 8 | 9 | public static partial class Utils 10 | { 11 | public static Color GetDominantColor(Image image) 12 | { 13 | image.Mutate(x => x.Resize(1, 1)); 14 | 15 | // 获取该像素的颜色 16 | Rgba32 dominantColor = image[0, 0]; 17 | return dominantColor; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Utils/Mail.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Security.Cryptography; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace KanonBot; 6 | 7 | public static partial class Utils 8 | { 9 | 10 | [GeneratedRegex(@"([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,5})+")] 11 | private static partial Regex EmailRegex(); 12 | public static bool IsMailAddr(string str) 13 | { 14 | if (EmailRegex().IsMatch(str)) 15 | return true; 16 | return false; 17 | } 18 | 19 | 20 | public static string HideMailAddr(string mailAddr) 21 | { 22 | try 23 | { 24 | var t1 = mailAddr.Split('@'); 25 | string[] t2 = new string[t1[0].Length]; 26 | for (int i = 0; i < t1[0].Length; i++) 27 | { 28 | t2[i] = "*"; 29 | } 30 | t2[0] = t1[0][0].ToString(); 31 | t2[t1[0].Length - 1] = t1[0][^1].ToString(); 32 | string ret = ""; 33 | foreach (string s in t2) 34 | { 35 | ret += s; 36 | } 37 | ret += "@"; 38 | t2 = new string[t1[1].Length]; 39 | for (int i = 0; i < t1[1].Length; i++) 40 | { 41 | t2[i] = "*"; 42 | } 43 | t2[0] = t1[1][0].ToString(); 44 | t2[t1[1].Length - 1] = t1[1][^1].ToString(); 45 | t2[t1[1].IndexOf(".")] = "."; 46 | foreach (string s in t2) 47 | { 48 | ret += s; 49 | } 50 | return ret; 51 | } 52 | catch 53 | { 54 | return mailAddr; 55 | } 56 | } 57 | 58 | public static async Task SendDebugMail(string body) 59 | { 60 | if (Config.inner!.mail is not null) { 61 | Mail.MailStruct ms = 62 | new() 63 | { 64 | MailTo = Config.inner.mail.mailTo, 65 | Subject = $"KanonBot 错误自动上报 - 发生于 {DateTime.Now}", 66 | Body = body, 67 | IsBodyHtml = false 68 | }; 69 | try 70 | { 71 | await Mail.Send(ms); 72 | } 73 | catch { } 74 | } 75 | } 76 | 77 | public static async Task SendMail(IEnumerable mailto, string title, string body, bool isBodyHtml) 78 | { 79 | Mail.MailStruct ms = 80 | new() 81 | { 82 | MailTo = mailto, 83 | Subject = title, 84 | Body = body, 85 | IsBodyHtml = isBodyHtml 86 | }; 87 | try 88 | { 89 | await Mail.Send(ms); 90 | } 91 | catch { } 92 | } 93 | 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/Utils/Random.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Security.Cryptography; 3 | using System.Text; 4 | 5 | namespace KanonBot; 6 | 7 | public static partial class Utils 8 | { 9 | private static RandomNumberGenerator rng = RandomNumberGenerator.Create(); 10 | 11 | public static int RandomNum(int min, int max) 12 | { 13 | var r = new Random( 14 | DateTime.Now.Millisecond 15 | + DateTime.Now.Second 16 | + DateTime.Now.Minute 17 | + DateTime.Now.Microsecond 18 | + DateTime.Now.Nanosecond 19 | ); 20 | return r.Next(min, max); 21 | } 22 | 23 | public static byte[] GenerateRandomBytes(int length) 24 | { 25 | byte[] randomBytes = new byte[length]; 26 | rng.GetBytes(randomBytes); 27 | return randomBytes; 28 | } 29 | 30 | public static string RandomStr(int length, bool URLparameter = false) 31 | { 32 | string str = ""; 33 | str += "0123456789"; 34 | str += "abcdefghijklmnopqrstuvwxyz"; 35 | str += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 36 | if (!URLparameter) 37 | str += "!_-@#$%+^&()[]'~`"; 38 | StringBuilder sb = new(); 39 | for (int i = 0; i < length; i++) 40 | { 41 | byte[] randomBytes = GenerateRandomBytes(100); 42 | int randomIndex = randomBytes[i] % str.Length; 43 | sb.Append(str[randomIndex]); 44 | } 45 | return sb.ToString(); 46 | } 47 | 48 | public static string RandomRedemptionCode() 49 | { 50 | StringBuilder sb = new(); 51 | string str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 52 | for (int o = 0; o < 5; o++) 53 | { 54 | for (int i = 0; i < 5; i++) 55 | { 56 | byte[] randomBytes = GenerateRandomBytes(255); 57 | int randomIndex = randomBytes[i] % str.Length; 58 | sb.Append(str[randomIndex]); 59 | } 60 | if (o < 4) sb.Append('-'); 61 | } 62 | return sb.ToString(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Utils/Reflection.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Reflection; 3 | 4 | namespace KanonBot; 5 | 6 | public static partial class Utils 7 | { 8 | public static string GetDesc(object? value) 9 | { 10 | FieldInfo? fieldInfo = value!.GetType().GetField(value.ToString()!); 11 | if (fieldInfo == null) 12 | return string.Empty; 13 | DescriptionAttribute[] attributes = (DescriptionAttribute[]) 14 | fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false); 15 | return attributes.Length > 0 ? attributes[0].Description : string.Empty; 16 | } 17 | 18 | public static string? GetObjectDescription(Object value) 19 | { 20 | foreach (var field in value.GetType().GetFields()) 21 | { 22 | // 获取object的类型,并遍历获取DescriptionAttribute 23 | // 提取出匹配的那个 24 | if ( 25 | Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) 26 | is DescriptionAttribute attribute 27 | ) 28 | { 29 | if (field.GetValue(null)?.Equals(value) ?? false) 30 | return attribute.Description; 31 | } 32 | } 33 | return null; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/Utils/Time.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Security.Cryptography; 3 | 4 | namespace KanonBot; 5 | 6 | public static partial class Utils 7 | { 8 | public static string GetTimeStamp(bool isMillisec) 9 | { 10 | TimeSpan ts = DateTime.Now - new DateTime(1970, 1, 1, 0, 0, 0, 0); 11 | if (!isMillisec) 12 | return Convert.ToInt64(ts.TotalSeconds).ToString(); 13 | else 14 | return Convert.ToInt64(ts.TotalMilliseconds).ToString(); 15 | } 16 | 17 | public static DateTimeOffset TimeStampMilliToDateTime(int timeStamp) 18 | { 19 | return DateTimeOffset.FromUnixTimeMilliseconds(timeStamp); 20 | } 21 | 22 | public static DateTimeOffset TimeStampSecToDateTime(long timeStamp) 23 | { 24 | return DateTimeOffset.FromUnixTimeSeconds(timeStamp); 25 | } 26 | 27 | public static string Duration2String(long duration) 28 | { 29 | long day, 30 | hour, 31 | minute, 32 | second; 33 | day = duration / 86400; 34 | duration %= 86400; 35 | hour = duration / 3600; 36 | duration %= 3600; 37 | minute = duration / 60; 38 | second = duration % 60; 39 | return $"{day}d {hour}h {minute}m {second}s"; 40 | } 41 | 42 | public static string Duration2StringWithoutSec(long duration) 43 | { 44 | long day, 45 | hour, 46 | minute, 47 | second; 48 | day = duration / 86400; 49 | duration %= 86400; 50 | hour = duration / 3600; 51 | duration %= 3600; 52 | minute = duration / 60; 53 | second = duration % 60; 54 | return $"{day}d {hour}h {minute}m"; 55 | } 56 | 57 | public static string Duration2TimeString(long duration) 58 | { 59 | long hour, 60 | minute, 61 | second; 62 | hour = duration / 3600; 63 | duration %= 3600; 64 | minute = duration / 60; 65 | second = duration % 60; 66 | if (hour > 0) 67 | return $"{hour}:{minute:00}:{second:00}"; 68 | return $"{minute}:{second:00}"; 69 | } 70 | 71 | public static string Duration2TimeStringForScoreV3(long duration) 72 | { 73 | long hour, 74 | minute, 75 | second; 76 | hour = duration / 3600; 77 | duration %= 3600; 78 | minute = duration / 60; 79 | second = duration % 60; 80 | if (hour > 0) 81 | return $"{hour}H,{minute:00}M,{second:00}S"; 82 | return $"{minute}M,{second:00}S"; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/drivers/Discord/API.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using KanonBot.Message; 3 | using System.IO; 4 | using Discord.WebSocket; 5 | using libDiscord = Discord; 6 | using Discord; 7 | using System.Net.Http; 8 | 9 | namespace KanonBot.Drivers; 10 | public partial class Discord 11 | { 12 | // API 部分 * 包装 Driver 13 | public class API 14 | { 15 | private string AuthToken; 16 | public API(string authToken) 17 | { 18 | this.AuthToken = $"Bot {authToken}"; 19 | } 20 | 21 | // IFlurlRequest http() 22 | // { 23 | // return EndPoint.WithHeader("Authorization", this.AuthToken); 24 | // } 25 | 26 | async public Task SendMessage(IMessageChannel channel, Chain msgChain, libDiscord.IMessage? originalTarget = null) 27 | { 28 | var messageRef = originalTarget is null ? null : new MessageReference(originalTarget.Id); 29 | var allowedMentions = new AllowedMentions(AllowedMentionTypes.None); 30 | foreach (var seg in msgChain.Iter()) { 31 | switch (seg) 32 | { 33 | case ImageSegment s: 34 | switch (s.t) 35 | { 36 | case ImageSegment.Type.Base64: { 37 | var uuid = Guid.NewGuid(); 38 | using var _s = Utils.Byte2Stream(Convert.FromBase64String(s.value)); 39 | await channel.SendFileAsync(_s, $"{uuid}.png", messageReference: messageRef, allowedMentions: allowedMentions); 40 | messageRef = null; 41 | } break; 42 | case ImageSegment.Type.File: { 43 | var uuid = Guid.NewGuid(); 44 | using var _s = Utils.LoadFile2ReadStream(s.value); 45 | await channel.SendFileAsync(_s, $"{uuid}.png", messageReference: messageRef, allowedMentions: allowedMentions); 46 | messageRef = null; 47 | } break; 48 | case ImageSegment.Type.Url: { 49 | var uuid = Guid.NewGuid(); 50 | using var _s = await s.value.GetStreamAsync(); 51 | await channel.SendFileAsync(_s, $"{uuid}.png", messageReference: messageRef, allowedMentions: allowedMentions); 52 | messageRef = null; 53 | } break; 54 | default: 55 | break; 56 | } 57 | break; 58 | case TextSegment s: 59 | await channel.SendMessageAsync(s.value, messageReference: messageRef, allowedMentions: allowedMentions); 60 | messageRef = null; 61 | break; 62 | case AtSegment s: 63 | if (s.value == "all") { 64 | await channel.SendMessageAsync($"@everyone", messageReference: messageRef); 65 | } else { 66 | await channel.SendMessageAsync($"<@{s.value}>", messageReference: messageRef); 67 | } 68 | messageRef = null; 69 | break; 70 | default: 71 | await channel.SendMessageAsync(seg.Build(), messageReference: messageRef, allowedMentions: allowedMentions); 72 | messageRef = null; 73 | break; 74 | } 75 | } 76 | } 77 | 78 | 79 | 80 | 81 | 82 | } 83 | } -------------------------------------------------------------------------------- /src/drivers/Discord/Driver.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using Discord; 3 | using Discord.Net.Rest; 4 | using Discord.Net.WebSockets; 5 | using Discord.WebSocket; 6 | using KanonBot.Event; 7 | using Serilog.Events; 8 | 9 | namespace KanonBot.Drivers; 10 | public partial class Discord : ISocket, IDriver 11 | { 12 | public static readonly Platform platform = Platform.Discord; 13 | public string? selfID { get; private set; } 14 | DiscordSocketClient instance; 15 | event IDriver.MessageDelegate? msgAction; 16 | event IDriver.EventDelegate? eventAction; 17 | string token; 18 | public API api; 19 | public Discord(string token, string botID) 20 | { 21 | // 初始化变量 22 | this.token = token; 23 | this.selfID = botID; 24 | 25 | this.api = new(token); 26 | 27 | var client = new DiscordSocketClient( 28 | new() { 29 | WebSocketProvider = DefaultWebSocketProvider.Create(WebRequest.DefaultWebProxy), 30 | RestClientProvider = DefaultRestClientProvider.Create(true), 31 | } 32 | ); 33 | client.Log += LogAsync; 34 | 35 | // client.MessageUpdated += this.Parse; 36 | client.MessageReceived += msg => { 37 | Task.Run(async () => { 38 | try 39 | { 40 | await this.Parse(msg); 41 | } 42 | catch (Exception ex) { Log.Error("未捕获的异常 ↓\n{ex}", ex); } 43 | }); 44 | return Task.CompletedTask; 45 | }; 46 | 47 | client.Ready += () => 48 | { 49 | // 连接成功 50 | return Task.CompletedTask; 51 | }; 52 | 53 | this.instance = client; 54 | } 55 | private static async Task LogAsync(LogMessage message) 56 | { 57 | var severity = message.Severity switch 58 | { 59 | LogSeverity.Critical => LogEventLevel.Fatal, 60 | LogSeverity.Error => LogEventLevel.Error, 61 | LogSeverity.Warning => LogEventLevel.Warning, 62 | LogSeverity.Info => LogEventLevel.Information, 63 | LogSeverity.Verbose => LogEventLevel.Verbose, 64 | LogSeverity.Debug => LogEventLevel.Debug, 65 | _ => LogEventLevel.Information 66 | }; 67 | Log.Write(severity, message.Exception, "[Discord] [{Source}] {Message}", message.Source, message.Message); 68 | await Task.CompletedTask; 69 | } 70 | 71 | 72 | private async Task Parse(SocketMessage message) 73 | { 74 | if (message.Author.Id == this.instance.CurrentUser.Id) 75 | return; 76 | 77 | // 过滤掉bot消息和系统消息 78 | if (message is SocketUserMessage m) 79 | { 80 | if (message.Author.IsBot) return; 81 | if (this.msgAction is null) return; 82 | 83 | var ms = await m.Channel.GetMessageAsync(m.Id); 84 | await this.msgAction.Invoke(new Target() 85 | { 86 | platform = Platform.Discord, 87 | sender = m.Author.Id.ToString(), 88 | selfAccount = this.selfID, 89 | msg = Message.Parse(ms), 90 | raw = ms, 91 | socket = this 92 | }); 93 | } 94 | else 95 | { 96 | if (this.eventAction is null) return; 97 | await this.eventAction.Invoke( 98 | this, 99 | new RawEvent(message) 100 | ); 101 | } 102 | } 103 | 104 | 105 | public IDriver onMessage(IDriver.MessageDelegate action) 106 | { 107 | this.msgAction += action; 108 | return this; 109 | } 110 | public IDriver onEvent(IDriver.EventDelegate action) 111 | { 112 | this.eventAction += action; 113 | return this; 114 | } 115 | 116 | public void Send(string message) 117 | { 118 | throw new NotSupportedException("不支持"); 119 | } 120 | 121 | public Task SendAsync(string message) 122 | { 123 | throw new NotSupportedException("不支持"); 124 | } 125 | 126 | public async Task Start() 127 | { 128 | await this.instance.LoginAsync(TokenType.Bot, this.token); 129 | await this.instance.StartAsync(); 130 | } 131 | 132 | public void Dispose() 133 | { 134 | this.instance.Dispose(); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/drivers/Discord/Message.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using Discord; 3 | using Discord.WebSocket; 4 | using KanonBot.Message; 5 | 6 | namespace KanonBot.Drivers; 7 | 8 | public partial class Discord 9 | { 10 | [GeneratedRegex(@"<@(\d*?)>", RegexOptions.Multiline)] 11 | private static partial Regex AtPattern(); 12 | 13 | [GeneratedRegex(@"<@&(\d*?)>", RegexOptions.Multiline)] 14 | private static partial Regex AtRolePattern(); 15 | 16 | [GeneratedRegex(@"@everyone", RegexOptions.Multiline)] 17 | private static partial Regex AtEveryone(); 18 | 19 | [GeneratedRegex(@"@here", RegexOptions.Multiline)] 20 | private static partial Regex AtHere(); 21 | 22 | public class Message 23 | { 24 | /// 25 | /// 解析部分附件只支持图片 26 | /// 27 | /// 28 | /// 29 | public static Chain Parse(IMessage MessageData) 30 | { 31 | var chain = new Chain(); 32 | // 处理 content 33 | var segList = new List<(Match m, IMsgSegment seg)>(); 34 | 35 | foreach (Match m in AtPattern().Matches(MessageData.Content).Cast()) 36 | { 37 | segList.Add((m, new AtSegment(m.Groups[1].Value, Platform.Discord))); 38 | } 39 | 40 | foreach (Match m in AtRolePattern().Matches(MessageData.Content).Cast()) 41 | { 42 | segList.Add((m, new AtSegment($"&{m.Groups[1].Value}", Platform.Discord))); 43 | } 44 | 45 | foreach (Match m in AtEveryone().Matches(MessageData.Content).Cast()) 46 | { 47 | segList.Add((m, new AtSegment("all", Platform.Discord))); 48 | } 49 | 50 | foreach (Match m in AtHere().Matches(MessageData.Content).Cast()) 51 | { 52 | segList.Add((m, new AtSegment("all", Platform.Discord))); 53 | } 54 | 55 | void AddText(ref Chain chain, string text) 56 | { 57 | // 匹配一下attacment 58 | foreach (var embed in MessageData.Embeds) 59 | { 60 | if (embed.Type == EmbedType.Image) 61 | { 62 | // 添加图片 63 | if (embed.Image is not null) { 64 | chain.Add(new ImageSegment(embed.Image.Value.Url, ImageSegment.Type.Url)); 65 | // text = text.Replace(embed.Image.Value.Url, ""); 66 | } 67 | } 68 | } 69 | if (text.Length != 0) 70 | chain.Add(new TextSegment(Utils.KOOKUnEscape(text))); 71 | } 72 | 73 | var pos = 0; 74 | foreach (var x in segList.OrderBy(x => x.m.Index)) { 75 | if (pos < x.m.Index) 76 | { 77 | AddText(ref chain, MessageData.Content[pos..x.m.Index]); 78 | } 79 | chain.Add(x.seg); 80 | pos = x.m.Index + x.m.Length; 81 | } 82 | 83 | if (pos < MessageData.Content.Length) { 84 | AddText(ref chain, MessageData.Content[pos..]); 85 | } 86 | 87 | return chain; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/drivers/Drivers.cs: -------------------------------------------------------------------------------- 1 | using KanonBot.Event; 2 | using KanonBot.Serializer; 3 | 4 | 5 | namespace KanonBot.Drivers; 6 | 7 | public enum Platform 8 | { 9 | Unknown, 10 | OneBot, 11 | Guild, 12 | KOOK, 13 | Discord, 14 | OSU 15 | } 16 | 17 | public interface IDriver 18 | { 19 | delegate Task MessageDelegate(Target target); 20 | delegate Task EventDelegate(ISocket socket, IEvent kevent); 21 | IDriver onMessage(MessageDelegate action); 22 | IDriver onEvent(EventDelegate action); 23 | Task Start(); 24 | void Dispose(); 25 | } 26 | public interface ISocket 27 | { 28 | string? selfID { get; } 29 | void Send(string message); 30 | Task SendAsync(string message); 31 | void Send(Object obj) => Send(Json.Serialize(obj)); 32 | Task SendAsync(Object obj) => SendAsync(Json.Serialize(obj)); 33 | } 34 | 35 | public interface IReply 36 | { 37 | Task Reply(Target target, Message.Chain msg); 38 | } 39 | 40 | public class Drivers 41 | { 42 | ManualResetEvent exitEvent = new(false); 43 | List driverList; 44 | public Drivers() 45 | { 46 | this.driverList = new(); 47 | } 48 | public Drivers append(IDriver n) 49 | { 50 | this.driverList.Add(n); 51 | return this; 52 | } 53 | 54 | public void StartAll() 55 | { 56 | var tasks = driverList.Map(x => x.Start()).ToArray(); 57 | Task.WaitAll(tasks); 58 | exitEvent.WaitOne(); 59 | } 60 | 61 | public void StopAll() 62 | { 63 | foreach (var driver in this.driverList) { 64 | driver.Dispose(); 65 | } 66 | exitEvent.Set(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/drivers/FakeSocket.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualBasic.CompilerServices; 2 | using KanonBot.API; 3 | using KanonBot.Message; 4 | 5 | namespace KanonBot.Drivers; 6 | 7 | public partial class FakeSocket : ISocket, IReply 8 | { 9 | public required Action? action; 10 | public string? selfID => throw new NotImplementedException(); 11 | 12 | public void Send(string message) 13 | { 14 | action?.Invoke(message); 15 | } 16 | 17 | public Task SendAsync(string message) 18 | { 19 | action?.Invoke(message); 20 | return Task.CompletedTask; 21 | } 22 | 23 | public async Task Reply(Target target, Message.Chain msg) 24 | { 25 | foreach (var s in msg.Iter()) 26 | { 27 | switch (s) 28 | { 29 | case ImageSegment i: 30 | var url = i.t switch 31 | { 32 | ImageSegment.Type.Base64 33 | => Utils.Byte2File( 34 | $"./work/tmp/{Guid.NewGuid()}.png", 35 | Convert.FromBase64String(i.value) 36 | ), 37 | ImageSegment.Type.Url => i.value, 38 | ImageSegment.Type.File => i.value, 39 | _ => throw new ArgumentException("不支持的图片类型") 40 | }; 41 | await this.SendAsync($"image;{url}"); 42 | break; 43 | default: 44 | await this.SendAsync(s.Build()); 45 | break; 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/drivers/KOOK/API.CS: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using KanonBot.Message; 3 | using System.IO; 4 | 5 | namespace KanonBot.Drivers; 6 | public partial class Kook 7 | { 8 | // API 部分 * 包装 Driver 9 | public class API 10 | { 11 | readonly string AuthToken; 12 | public static readonly string EndPoint = "https://www.kookapp.cn/api/v3"; 13 | public API(string authToken) 14 | { 15 | this.AuthToken = $"Bot {authToken}"; 16 | } 17 | 18 | private IFlurlRequest Http() 19 | { 20 | return EndPoint.WithHeader("Authorization", this.AuthToken); 21 | } 22 | 23 | async public Task GetWebsocketUrl() 24 | { 25 | var res = await this.Http() 26 | .AppendPathSegments("gateway", "index") 27 | .SetQueryParam("compress", 0) 28 | .GetJsonAsync(); 29 | 30 | if (((int)res["code"]!) != 0) 31 | { 32 | throw new Exception($"无法获取KOOK WebSocket地址,Code:{res["code"]},Message:{res["message"]}"); 33 | } 34 | 35 | return res["data"]!["url"]!.ToString(); 36 | } 37 | 38 | 39 | /// 40 | /// 传入文件数据与文件名,如无文件名则会随机生成字符串 41 | /// 42 | /// 43 | /// 44 | /// url 45 | async public Task CreateAsset(Stream data, string? filename = null) 46 | { 47 | var res = await this.Http() 48 | .AppendPathSegments("asset", "create") 49 | .SetQueryParam("compress", 0) 50 | .PostMultipartAsync(mp => mp 51 | .AddFile("file", data, filename ?? Utils.RandomStr(10)) 52 | ); 53 | var j = await res.GetJsonAsync(); 54 | return j["data"]!["url"]!.ToString(); 55 | } 56 | 57 | 58 | 59 | async public Task SendPrivateMessage(string userID, Chain msgChain, Guid? QuotedMessageId = null) 60 | { 61 | var messages = await Message.Build(this, msgChain); 62 | foreach (var msg in messages) 63 | { 64 | msg.TargetId = userID; 65 | msg.QuotedMessageId = QuotedMessageId; 66 | await this.Http() 67 | .AppendPathSegments("direct-message", "create") 68 | .PostJsonAsync(msg); 69 | } 70 | } 71 | async public Task SendChannelMessage(string channelID, Chain msgChain, Guid? QuotedMessageId = null, string? TempMsgTargetId = null) 72 | { 73 | var messages = await Message.Build(this, msgChain); 74 | if (messages.Count > 0) 75 | messages[0].QuotedMessageId = QuotedMessageId; 76 | foreach (var msg in messages) 77 | { 78 | msg.TargetId = channelID; 79 | msg.EphemeralUserId = TempMsgTargetId; 80 | await this.Http() 81 | .AppendPathSegments("message", "create") 82 | .PostJsonAsync(msg); 83 | } 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /src/drivers/KOOK/Driver.cs: -------------------------------------------------------------------------------- 1 | using KanonBot.Event; 2 | using Serilog.Events; 3 | using libKook = Kook; 4 | using Kook.WebSocket; 5 | 6 | namespace KanonBot.Drivers; 7 | public partial class Kook : ISocket, IDriver 8 | { 9 | public static readonly Platform platform = Platform.Guild; 10 | public string? selfID { get; private set; } 11 | KookSocketClient instance; 12 | event IDriver.MessageDelegate? msgAction; 13 | event IDriver.EventDelegate? eventAction; 14 | string token; 15 | public API api; 16 | public Kook(string token, string botID) 17 | { 18 | // 初始化变量 19 | this.token = token; 20 | this.selfID = botID; 21 | 22 | this.api = new(token); 23 | 24 | var client = new KookSocketClient(); 25 | client.Log += LogAsync; 26 | 27 | // client.MessageUpdated += this.Parse; 28 | client.DirectMessageReceived += (msg, user, channel) => Task.Run(() => 29 | { 30 | try 31 | { 32 | this.Parse(msg); 33 | } 34 | catch (Exception ex) { Log.Error("未捕获的异常 ↓\n{ex}", ex); } 35 | }); 36 | client.MessageReceived += (msg, user, channel) => { 37 | Task.Run(() => { 38 | try 39 | { 40 | this.Parse(msg); 41 | } 42 | catch (Exception ex) { Log.Error("未捕获的异常 ↓\n{ex}", ex); } 43 | }); 44 | return Task.CompletedTask; 45 | }; 46 | client.Ready += () => 47 | { 48 | // 连接成功 49 | return Task.CompletedTask; 50 | }; 51 | 52 | this.instance = client; 53 | } 54 | private static async Task LogAsync(libKook.LogMessage message) 55 | { 56 | var severity = message.Severity switch 57 | { 58 | libKook.LogSeverity.Critical => LogEventLevel.Fatal, 59 | libKook.LogSeverity.Error => LogEventLevel.Error, 60 | libKook.LogSeverity.Warning => LogEventLevel.Warning, 61 | libKook.LogSeverity.Info => LogEventLevel.Information, 62 | libKook.LogSeverity.Verbose => LogEventLevel.Verbose, 63 | libKook.LogSeverity.Debug => LogEventLevel.Debug, 64 | _ => LogEventLevel.Information 65 | }; 66 | Log.Write(severity, message.Exception, "[KOOK] [{Source}] {Message}", message.Source, message.Message); 67 | await Task.CompletedTask; 68 | } 69 | 70 | 71 | private void Parse(SocketMessage message) 72 | { 73 | // 过滤掉bot消息和系统消息 74 | if (message.Source != libKook.MessageSource.User) 75 | { 76 | this.eventAction?.Invoke( 77 | this, 78 | new RawEvent(message) 79 | ); 80 | } 81 | else 82 | { 83 | this.msgAction?.Invoke(new Target() 84 | { 85 | platform = Platform.KOOK, 86 | sender = message.Author.Id.ToString(), 87 | selfAccount = this.selfID, 88 | msg = Message.Parse(message), 89 | raw = message, 90 | socket = this 91 | }); 92 | } 93 | 94 | } 95 | private async Task ParseUpdateMessage(libKook.Cacheable before, SocketMessage after, ISocketMessageChannel channel) 96 | { 97 | var message = await before.GetOrDownloadAsync(); 98 | Log.Debug($"{message} -> {after}"); 99 | } 100 | 101 | 102 | public IDriver onMessage(IDriver.MessageDelegate action) 103 | { 104 | this.msgAction += action; 105 | return this; 106 | } 107 | public IDriver onEvent(IDriver.EventDelegate action) 108 | { 109 | this.eventAction += action; 110 | return this; 111 | } 112 | 113 | public void Send(string message) 114 | { 115 | throw new NotSupportedException("不支持"); 116 | } 117 | 118 | public Task SendAsync(string message) 119 | { 120 | throw new NotSupportedException("不支持"); 121 | } 122 | 123 | public async Task Start() 124 | { 125 | await this.instance.LoginAsync(tokenType: libKook.TokenType.Bot, this.token); 126 | await this.instance.StartAsync(); 127 | } 128 | 129 | public void Dispose() 130 | { 131 | this.instance.Dispose(); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/drivers/KOOK/Enums.cs: -------------------------------------------------------------------------------- 1 | namespace KanonBot.Drivers; 2 | public partial class Kook 3 | { 4 | public class Enums 5 | { 6 | public enum MessageType 7 | { 8 | Text = 1, 9 | Image = 2, 10 | Video = 3, 11 | File = 4, 12 | Audio = 8, 13 | KMarkdown = 9, 14 | Card = 10, 15 | System = 255 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/drivers/KOOK/Message.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Text.RegularExpressions; 3 | using KanonBot.Message; 4 | using libKook = Kook; 5 | using Kook.WebSocket; 6 | 7 | namespace KanonBot.Drivers; 8 | 9 | public partial class Kook 10 | { 11 | [GeneratedRegex(@"\(met\)(.*?)\(met\)", RegexOptions.Multiline)] 12 | private static partial Regex AtPattern(); 13 | [GeneratedRegex(@"\(rol\)(.*?)\(rol\)", RegexOptions.Multiline)] 14 | private static partial Regex AtAdminPattern(); 15 | 16 | public class Message 17 | { 18 | public async static Task> Build(API api, Chain msgChain) 19 | { 20 | var msglist = new List(); 21 | foreach (var seg in msgChain.Iter()) 22 | { 23 | var req = new Models.MessageCreate(); 24 | switch (seg) 25 | { 26 | case ImageSegment s: 27 | req.MessageType = Enums.MessageType.Image; 28 | switch (s.t) 29 | { 30 | case ImageSegment.Type.Base64: 31 | using ( 32 | var _s = Utils.Byte2Stream(Convert.FromBase64String(s.value)) 33 | ) 34 | { 35 | req.Content = await api.CreateAsset(_s); 36 | } 37 | break; 38 | case ImageSegment.Type.File: 39 | using (var __s = Utils.LoadFile2ReadStream(s.value)) 40 | { 41 | req.Content = await api.CreateAsset(__s); 42 | } 43 | break; 44 | case ImageSegment.Type.Url: 45 | req.Content = s.value; 46 | break; 47 | default: 48 | break; 49 | } 50 | break; 51 | case TextSegment s: 52 | if (msglist.Count > 0) 53 | { 54 | if (msglist[^1].MessageType == Enums.MessageType.Text) 55 | { 56 | msglist[^1].Content += s.value; 57 | continue; 58 | } 59 | } 60 | req.MessageType = Enums.MessageType.Text; 61 | req.Content = s.value; 62 | break; 63 | case AtSegment s: 64 | string _at; 65 | if (s.platform == Platform.KOOK) 66 | _at = $"(met){s.value}(met)"; 67 | else 68 | throw new NotSupportedException("不支持的平台类型"); 69 | if (msglist.Count > 0) 70 | { 71 | // 将一类文字消息合并起来到 Text 中 72 | if (msglist[^1].MessageType == Enums.MessageType.Text) 73 | { 74 | msglist[^1].Content += _at; 75 | continue; 76 | } 77 | } 78 | req.MessageType = Enums.MessageType.Text; 79 | req.Content = _at; 80 | break; 81 | default: 82 | throw new NotSupportedException("不支持的平台类型"); 83 | } 84 | msglist.Add(req); 85 | } 86 | return msglist; 87 | } 88 | 89 | /// 90 | /// 解析部分附件只支持图片 91 | /// 92 | /// 93 | /// 94 | public static Chain Parse(SocketMessage MessageData) 95 | { 96 | var chain = new Chain(); 97 | // 处理 content 98 | var segList = new List<(Match m, IMsgSegment seg)>(); 99 | foreach (Match m in AtPattern().Matches(MessageData.Content).AsEnumerable()) 100 | { 101 | segList.Add((m, new AtSegment(m.Groups[1].Value, Platform.KOOK))); 102 | } 103 | foreach (Match m in AtAdminPattern().Matches(MessageData.Content).AsEnumerable()) 104 | { 105 | segList.Add((m, new RawSegment("KOOK AT ADMIN", m.Groups[1].Value))); 106 | } 107 | var AddText = (string text) => 108 | { 109 | var x = text.Trim(); 110 | // 匹配一下attacment 111 | foreach (var Attachment in MessageData.Attachments) 112 | { 113 | if (Attachment.Type == libKook.AttachmentType.Image) 114 | { 115 | // 添加图片,删除文本 116 | chain.Add(new ImageSegment(Attachment.Url, ImageSegment.Type.Url)); 117 | x = x.Replace(Attachment.Url, ""); 118 | } 119 | } 120 | if (x.Length != 0) 121 | chain.Add(new TextSegment(Utils.KOOKUnEscape(x))); 122 | }; 123 | var pos = 0; 124 | segList 125 | .OrderBy(x => x.m.Index) 126 | .ToList() 127 | .ForEach(x => 128 | { 129 | if (pos < x.m.Index) 130 | { 131 | AddText(MessageData.Content[pos..x.m.Index]); 132 | } 133 | chain.Add(x.seg); 134 | pos = x.m.Index + x.m.Length; 135 | }); 136 | if (pos < MessageData.Content.Length) 137 | AddText(MessageData.Content[pos..]); 138 | 139 | return chain; 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/drivers/KOOK/Models.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS8618 // 非null 字段未初始化 2 | 3 | using Newtonsoft.Json; 4 | using NullValueHandling = Newtonsoft.Json.NullValueHandling; 5 | 6 | namespace KanonBot.Drivers; 7 | public partial class Kook 8 | { 9 | public class Models 10 | { 11 | public class MessageCreate 12 | { 13 | 14 | [JsonProperty(PropertyName = "type")] 15 | public Enums.MessageType MessageType { get; set; } 16 | 17 | [JsonProperty(PropertyName = "target_id")] 18 | public string TargetId { get; set; } 19 | 20 | [JsonProperty(PropertyName = "content")] 21 | public string Content { get; set; } 22 | 23 | [JsonProperty(PropertyName = "quote", NullValueHandling = NullValueHandling.Ignore)] 24 | public Guid? QuotedMessageId { get; set; } 25 | 26 | [JsonProperty(PropertyName = "nonce", NullValueHandling = NullValueHandling.Ignore)] 27 | public Guid? Nonce { get; set; } 28 | 29 | [JsonProperty(PropertyName = "temp_target_id", NullValueHandling = NullValueHandling.Ignore)] 30 | public string? EphemeralUserId { get; set; } 31 | 32 | public MessageCreate Clone() 33 | { 34 | var other = this.MemberwiseClone() as MessageCreate; 35 | return other!; 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/drivers/OneBot/API.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using DotNext.Threading; 3 | using KanonBot.Message; 4 | using KanonBot.Serializer; 5 | using LanguageExt.UnsafeValueAccess; 6 | using Newtonsoft.Json; 7 | using Newtonsoft.Json.Linq; 8 | using Serilog; 9 | namespace KanonBot.Drivers; 10 | public partial class OneBot 11 | { 12 | // API 部分 * 包装 Driver 13 | public class API 14 | { 15 | readonly ISocket socket; 16 | public API(ISocket socket) 17 | { 18 | this.socket = socket; 19 | } 20 | 21 | #region 消息收发 22 | public class RetCallback 23 | { 24 | public AsyncManualResetEvent ResetEvent { get; } = new AsyncManualResetEvent(false); 25 | public Models.CQResponse? Data { get; set; } 26 | } 27 | 28 | private AsyncExclusiveLock l = new(); 29 | public ConcurrentDictionary CallbackList = new(); 30 | public async Task Echo(Models.CQResponse res) 31 | { 32 | using(await l.AcquireLockAsync(CancellationToken.None)) 33 | { 34 | this.CallbackList[res.Echo].Data = res; 35 | this.CallbackList[res.Echo].ResetEvent.Set(); 36 | } 37 | } 38 | private async Task Send(Models.CQRequest req) 39 | { 40 | using(await l.AcquireLockAsync(CancellationToken.None)) 41 | { 42 | // 创建回调 43 | this.CallbackList[req.Echo] = new RetCallback(); 44 | } 45 | 46 | // 发送 47 | await this.socket.SendAsync(req); 48 | await this.CallbackList[req.Echo].ResetEvent.WaitAsync(); 49 | 50 | RetCallback ret; 51 | using(await l.AcquireLockAsync(CancellationToken.None)) { 52 | // 获取并移除回调 53 | this.CallbackList.Remove(req.Echo, out ret!); 54 | } 55 | return ret.Data!; 56 | } 57 | #endregion 58 | 59 | // 发送群消息 60 | public async Task SendGroupMessage(long groupId, Chain msgChain) 61 | { 62 | var message = Message.Build(msgChain); 63 | var req = new Models.CQRequest 64 | { 65 | action = Enums.Actions.SendMsg, 66 | Params = new Models.SendMessage 67 | { 68 | MessageType = Enums.MessageType.Group, 69 | GroupId = groupId, 70 | Message = message, 71 | AutoEscape = false 72 | }, 73 | }; 74 | 75 | var res = await this.Send(req); 76 | if (res.Status == "ok") 77 | return (long)res.Data["message_id"]!; 78 | else 79 | return null; 80 | } 81 | 82 | // 发送私聊消息 83 | public async Task SendPrivateMessage(long userId, Chain msgChain) 84 | { 85 | var message = Message.Build(msgChain); 86 | var req = new Models.CQRequest 87 | { 88 | action = Enums.Actions.SendMsg, 89 | Params = new Models.SendMessage 90 | { 91 | MessageType = Enums.MessageType.Private, 92 | UserId = userId, 93 | Message = message, 94 | AutoEscape = false 95 | }, 96 | }; 97 | 98 | var res = await this.Send(req); 99 | if (res.Status == "ok") 100 | return (long)res.Data["message_id"]!; 101 | else 102 | return null; 103 | } 104 | 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/drivers/OneBot/Driver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using System.Net.WebSockets; 6 | using KanonBot.Message; 7 | using KanonBot.Serializer; 8 | using KanonBot.Event; 9 | using Newtonsoft.Json; 10 | using Serilog; 11 | using KanonBot; 12 | 13 | namespace KanonBot.Drivers; 14 | public partial class OneBot 15 | { 16 | public static readonly Platform platform = Platform.OneBot; 17 | event IDriver.MessageDelegate? msgAction; 18 | event IDriver.EventDelegate? eventAction; 19 | } 20 | -------------------------------------------------------------------------------- /src/drivers/OneBot/Message.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | using KanonBot.Message; 4 | using KanonBot.API; 5 | using KanonBot.Serializer; 6 | using Serilog; 7 | namespace KanonBot.Drivers; 8 | public partial class OneBot 9 | { 10 | public class Message 11 | { 12 | public static List Build(Chain msgChain) 13 | { 14 | var ListSegment = new List(); 15 | foreach (var msg in msgChain.Iter()) 16 | { 17 | ListSegment.Add( 18 | msg switch { 19 | TextSegment text => new Models.Segment { 20 | msgType = Enums.SegmentType.Text, 21 | rawData = new JObject { { "text", text.value } } 22 | }, 23 | ImageSegment image => new Models.Segment { 24 | msgType = Enums.SegmentType.Image, 25 | rawData = image.t switch { 26 | ImageSegment.Type.Base64 => new JObject { { "file", $"base64://{image.value}" } }, 27 | ImageSegment.Type.Url => new JObject { { "file", image.value } }, 28 | ImageSegment.Type.File => new JObject { { "file", Ali.PutFile(Utils.LoadFile2Byte(image.value).Result, "jpg") } }, // 这里还有缺陷,如果图片上传失败的话,还是会尝试发送 29 | _ => throw new ArgumentException("不支持的图片类型") 30 | } 31 | }, 32 | AtSegment at => at.platform switch { 33 | Platform.OneBot => new Models.Segment { 34 | msgType = Enums.SegmentType.At, 35 | rawData = new JObject { { "qq", at.value } } 36 | }, 37 | _ => throw new ArgumentException("不支持的平台类型") 38 | }, 39 | EmojiSegment face => new Models.Segment { 40 | msgType = Enums.SegmentType.Face, 41 | rawData = new JObject { { "id", face.value } } 42 | }, 43 | // 收到未知消息就转换为纯文本 44 | _ => new Models.Segment { 45 | msgType = Enums.SegmentType.Text, 46 | rawData = new JObject { { "text", msg.Build() } } 47 | } 48 | } 49 | ); 50 | } 51 | return ListSegment; 52 | } 53 | 54 | public static Chain Parse(List MessageList) 55 | { 56 | var chain = new Chain(); 57 | foreach (var obj in MessageList) 58 | { 59 | chain.Add( 60 | obj.msgType switch { 61 | Enums.SegmentType.Text => new TextSegment(obj.rawData["text"]!.ToString()), 62 | Enums.SegmentType.Image => obj.rawData.ContainsKey("url") ? new ImageSegment(obj.rawData["url"]!.ToString(), ImageSegment.Type.Url) : new ImageSegment(obj.rawData["file"]!.ToString(), ImageSegment.Type.File), 63 | Enums.SegmentType.At => new AtSegment(obj.rawData["qq"]!.ToString(), Platform.OneBot), 64 | Enums.SegmentType.Face => new EmojiSegment(obj.rawData["id"]!.ToString()), 65 | _ => new RawSegment(obj.msgType.ToString(), obj.rawData) 66 | } 67 | ); 68 | } 69 | return chain; 70 | } 71 | 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/drivers/QQGuild/API.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | 3 | namespace KanonBot.Drivers; 4 | public partial class QQGuild 5 | { 6 | // API 部分 * 包装 Driver 7 | public class API 8 | { 9 | public static readonly string DefaultEndPoint = "https://api.sgroup.qq.com"; 10 | public static readonly string SandboxEndPoint = "https://sandbox.api.sgroup.qq.com"; 11 | string EndPoint; 12 | string AuthToken; 13 | public API(string authToken,bool sandbox) 14 | { 15 | this.EndPoint = sandbox ? SandboxEndPoint : DefaultEndPoint; 16 | this.AuthToken = authToken; 17 | } 18 | 19 | IFlurlRequest http() 20 | { 21 | return this.EndPoint.WithHeader("Authorization", this.AuthToken); 22 | } 23 | 24 | async public Task GetWebsocketUrl() 25 | { 26 | return (await this.http().AppendPathSegments("gateway", "bot").GetJsonAsync())["url"]!.ToString(); 27 | } 28 | 29 | async public Task SendMessage(string ChannelID, Models.SendMessageData data) 30 | { 31 | return await this.http() 32 | .AppendPathSegments("channels", ChannelID, "messages") 33 | .PostJsonAsync(data) 34 | .ReceiveJson(); 35 | } 36 | 37 | 38 | 39 | } 40 | } -------------------------------------------------------------------------------- /src/drivers/QQGuild/Message.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using KanonBot.Message; 3 | using KanonBot.API; 4 | 5 | namespace KanonBot.Drivers; 6 | public partial class QQGuild 7 | { 8 | public partial class Message 9 | { 10 | [GeneratedRegex(@"<@!?(.*?)> ?", RegexOptions.Multiline)] 11 | private static partial Regex AtPattern(); 12 | [GeneratedRegex(@"", RegexOptions.Multiline)] 13 | private static partial Regex EmojiPattern(); 14 | [GeneratedRegex(@"<#(.*?)>", RegexOptions.Multiline)] 15 | private static partial Regex ChannelPattern(); 16 | [GeneratedRegex(@"@everyone", RegexOptions.Multiline)] 17 | private static partial Regex AtEveryonePattern(); 18 | public static Models.SendMessageData Build(Models.SendMessageData data, Chain msgChain) 19 | { 20 | var content = String.Empty; 21 | foreach (var msg in msgChain.Iter()) 22 | { 23 | var tmp = msg switch { 24 | TextSegment text => Utils.GuildEscape(text.value), 25 | EmojiSegment face => $"", 26 | AtSegment at => at.platform switch { 27 | Platform.Guild => at.value switch { 28 | "all" => "@everyone", 29 | _ => $"<@!{at.value}>" 30 | }, 31 | _ => throw new ArgumentException("不支持的平台类型") 32 | }, 33 | ImageSegment => null, // 图片不在此处处理 34 | _ => msg.Build(), 35 | }; 36 | content += tmp ?? String.Empty; 37 | 38 | if (msg is ImageSegment image) { 39 | data.ImageUrl = image.t switch { 40 | ImageSegment.Type.Base64 => Ali.PutFile(Convert.FromBase64String(image.value), "jpg", true), 41 | ImageSegment.Type.Url => image.value, 42 | ImageSegment.Type.File => Ali.PutFile(Utils.LoadFile2Byte(image.value).Result, "jpg", true), 43 | _ => throw new ArgumentException("不支持的图片类型") 44 | }; 45 | } 46 | } 47 | data.Content = content; 48 | return data; 49 | } 50 | 51 | public static Chain Parse(Models.MessageData MessageData) 52 | { 53 | // 先处理 content 54 | var segList = new List<(Match m, IMsgSegment seg)>(); 55 | foreach (Match m in AtPattern().Matches(MessageData.Content).Cast()) 56 | { 57 | segList.Add((m, new AtSegment(m.Groups[1].Value, Platform.Guild))); 58 | } 59 | foreach (Match m in EmojiPattern().Matches(MessageData.Content).Cast()) 60 | { 61 | segList.Add((m, new EmojiSegment(m.Groups[1].Value))); 62 | } 63 | foreach (Match m in ChannelPattern().Matches(MessageData.Content).Cast()) 64 | { 65 | segList.Add((m, new RawSegment("Channel", m.Groups[1].Value))); 66 | } 67 | foreach (Match m in AtEveryonePattern().Matches(MessageData.Content).Cast()) 68 | { 69 | segList.Add((m, new AtSegment("all", Platform.Guild))); 70 | } 71 | var chain = new Chain(); 72 | var AddText = (string text) => 73 | { 74 | var x = text.Trim(); 75 | if (x.Length != 0) 76 | chain.Add(new TextSegment(Utils.GuildUnEscape(x))); 77 | }; 78 | var pos = 0; 79 | segList.OrderBy(x => x.m.Index).ToList().ForEach(x => 80 | { 81 | if (pos < x.m.Index) 82 | { 83 | AddText(MessageData.Content[pos..x.m.Index]); 84 | } 85 | chain.Add(x.seg); 86 | pos = x.m.Index + x.m.Length; 87 | }); 88 | if (pos < MessageData.Content.Length) 89 | AddText(MessageData.Content[pos..]); 90 | 91 | // 然后处理 Attachments 92 | if (MessageData.Attachments != null) 93 | { 94 | foreach (var attachment in MessageData.Attachments) 95 | { 96 | if (attachment["content_type"]?.ToString() == "image/jpeg") 97 | { 98 | chain.Add(new ImageSegment($"https://{attachment["url"]}", ImageSegment.Type.Url)); 99 | } 100 | } 101 | } 102 | return chain; 103 | } 104 | 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/drivers/Target.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Channels; 2 | using libKook = Kook; 3 | using libDiscord = Discord; 4 | using Msg = KanonBot.Message; 5 | 6 | namespace KanonBot.Drivers; 7 | 8 | // 消息target封装 9 | // 暂时还不知道怎么写 10 | public class Target 11 | { 12 | public static Atom)>> Waiters { get; set; } = 13 | Atom)>>(new()); 14 | 15 | public async Task> prompt(TimeSpan timeout) 16 | { 17 | var channel = Channel.CreateBounded(1); 18 | Waiters.Swap(l => 19 | { 20 | l.Add((this, channel.Writer)); 21 | return l; 22 | }); 23 | var ret = await channel.Reader.ReadAsync().AsTask().TimeOut(timeout); 24 | Waiters.Swap(l => 25 | { 26 | l.Remove((this, channel.Writer)); 27 | return l; 28 | }); 29 | return ret; 30 | } 31 | 32 | public required Msg.Chain msg { get; init; } 33 | 34 | // account和sender为用户ID字符串,可以是qq号,khl号,等等 35 | public required string? selfAccount { get; init; } 36 | public required string? sender { get; init; } 37 | public required Platform platform { get; init; } 38 | public bool isFromAdmin { get; set; } = false; 39 | 40 | // 原平台消息结构 41 | public object? raw { get; init; } 42 | 43 | // 原平台接口 44 | public required ISocket socket { get; init; } 45 | 46 | public Task reply(string m) 47 | { 48 | return this.reply(new Msg.Chain().msg(m)); 49 | } 50 | 51 | public async Task reply(Msg.Chain msgChain) 52 | { 53 | switch (this.socket!) 54 | { 55 | case Discord d: 56 | var discordRawMessage = this.raw as libDiscord.IMessage; 57 | try 58 | { 59 | await d.api.SendMessage(discordRawMessage!.Channel, msgChain, discordRawMessage); 60 | } 61 | catch (Exception ex) 62 | { 63 | Log.Warning("发送Discord消息失败 ↓\n{ex}", ex); 64 | return false; 65 | } 66 | break; 67 | case Kook s: 68 | var KookRawMessage = this.raw as libKook.WebSocket.SocketMessage; 69 | try 70 | { 71 | await s.api.SendChannelMessage( 72 | KookRawMessage!.Channel.Id.ToString(), 73 | msgChain, 74 | KookRawMessage.Id 75 | ); 76 | } 77 | catch (Exception ex) 78 | { 79 | Log.Warning("发送Kook消息失败 ↓\n{ex}", ex); 80 | return false; 81 | } 82 | break; 83 | case QQGuild s: 84 | var GuildMessageData = (this.raw as QQGuild.Models.MessageData)!; 85 | try 86 | { 87 | await s.api.SendMessage( 88 | GuildMessageData.ChannelID, 89 | new QQGuild.Models.SendMessageData() 90 | { 91 | MessageId = GuildMessageData.ID, 92 | MessageReference = new() { MessageId = GuildMessageData.ID } 93 | }.Build(msgChain) 94 | ); 95 | } 96 | catch (Exception ex) 97 | { 98 | Log.Warning("发送QQ频道消息失败 ↓\n{ex}", ex); 99 | return false; 100 | } 101 | break; 102 | case OneBot.Client s: 103 | switch (this.raw) 104 | { 105 | case OneBot.Models.GroupMessage g: 106 | { 107 | if ((await s.api.SendGroupMessage(g.GroupId, msgChain)).HasValue) 108 | { 109 | Log.Warning("发送QQ消息失败"); 110 | return false; 111 | } 112 | break; 113 | } 114 | case OneBot.Models.PrivateMessage p: 115 | if ((await s.api.SendPrivateMessage(p.UserId, msgChain)).HasValue) 116 | { 117 | Log.Warning("发送QQ消息失败"); 118 | return false; 119 | } 120 | break; 121 | default: 122 | break; 123 | } 124 | break; 125 | case OneBot.Server.Socket s: 126 | switch (this.raw) 127 | { 128 | case OneBot.Models.GroupMessage g: 129 | if ((await s.api.SendGroupMessage(g.GroupId, msgChain)).HasValue) 130 | { 131 | return false; 132 | } 133 | break; 134 | case OneBot.Models.PrivateMessage p: 135 | if ((await s.api.SendPrivateMessage(p.UserId, msgChain)).HasValue) 136 | { 137 | return false; 138 | } 139 | break; 140 | default: 141 | break; 142 | } 143 | break; 144 | default: 145 | if (this.socket is IReply r) { 146 | await r.Reply(this, msgChain); 147 | } else { 148 | await socket.SendAsync(msgChain.ToString()); 149 | } 150 | break; 151 | } 152 | return true; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/functions/Bot/help.cs: -------------------------------------------------------------------------------- 1 | using KanonBot.Drivers; 2 | using KanonBot.Message; 3 | using KanonBot.API; 4 | 5 | namespace KanonBot.Functions.OSUBot 6 | { 7 | public class Help 8 | { 9 | public async static Task Execute(Target target, string cmd) 10 | { 11 | await target.reply( 12 | """ 13 | 用户查询: 14 | !info/recent/bp/get 15 | 绑定/用户设置: 16 | !reg/set 17 | 更多细节请移步 https://info.desu.life/?p=383 查阅 18 | """ 19 | ); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/functions/Bot/ping.cs: -------------------------------------------------------------------------------- 1 | using KanonBot.Drivers; 2 | using KanonBot.Message; 3 | using KanonBot.API; 4 | 5 | namespace KanonBot.Functions.OSUBot 6 | { 7 | public class Ping 8 | { 9 | public async static Task Execute(Target target) 10 | { 11 | await target.reply("meow~"); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/functions/Bot/su.cs: -------------------------------------------------------------------------------- 1 | using Flurl.Util; 2 | using KanonBot.Drivers; 3 | using KanonBot.Functions.OSUBot; 4 | using static KanonBot.Functions.Accounts; 5 | 6 | namespace KanonBot.Functions.OSU 7 | { 8 | public static class Su 9 | { 10 | public static async Task Execute(Target target, string cmd) 11 | { 12 | if (target.isFromAdmin == false) return; 13 | try 14 | { 15 | var AccInfo = GetAccInfo(target); 16 | var userinfo = await Database.Client.GetUsersByUID(AccInfo.uid, AccInfo.platform); 17 | if (userinfo == null) 18 | { 19 | return;//直接忽略 20 | } 21 | List permissions = new(); 22 | if (userinfo!.permissions!.IndexOf(";") < 1) //一般不会出错,默认就是user 23 | { 24 | permissions.Add(userinfo.permissions); 25 | } 26 | else 27 | { 28 | var t1 = userinfo.permissions.Split(";"); 29 | foreach (var x in t1) 30 | { 31 | permissions.Add(x); 32 | } 33 | } 34 | //检查用户权限 35 | int permissions_flag = -1; 36 | 37 | foreach (var x in permissions) 38 | { 39 | switch (x) 40 | { 41 | case "restricted": 42 | permissions_flag = -3; 43 | break; 44 | case "banned": 45 | permissions_flag = -1; 46 | break; 47 | case "user": 48 | if (permissions_flag < 1) permissions_flag = 1; 49 | break; 50 | case "mod": 51 | if (permissions_flag < 2) permissions_flag = 2; 52 | break; 53 | case "admin": 54 | if (permissions_flag < 3) permissions_flag = 3; 55 | break; 56 | case "system": 57 | permissions_flag = -2; 58 | break; 59 | default: 60 | break; 61 | } 62 | 63 | } 64 | 65 | if (permissions_flag != 3) return; //权限不够不处理 66 | 67 | //execute 68 | string rootCmd, childCmd = ""; 69 | try 70 | { 71 | var tmp = cmd.Split(' ', 2, StringSplitOptions.TrimEntries);; 72 | rootCmd = tmp[0]; 73 | childCmd = tmp[1]; 74 | } 75 | catch { rootCmd = cmd; } 76 | 77 | switch (rootCmd.ToLower()) 78 | { 79 | case "updateall": 80 | await SuDailyUpdateAsync(target); 81 | return; 82 | case "restrict_user_byoid": 83 | await RestriceUser(target, childCmd, 1); 84 | return; 85 | case "restrict_user_byemail": 86 | await RestriceUser(target, childCmd, 2); 87 | return; 88 | default: 89 | return; 90 | } 91 | } 92 | catch { }//直接忽略 93 | } 94 | 95 | 96 | public static async Task RestriceUser(Target target, string cmd, int bywhat) //1=byoid 2=byemail 97 | { 98 | //SetOsuUserPermissionByOid 99 | switch (bywhat) 100 | { 101 | case 1: 102 | if (await Database.Client.GetUserByOsuUID(long.Parse(cmd)) == null) 103 | { 104 | 105 | await target.reply($"该用户未注册desu.life账户或osu!账户不存在,请重新确认"); 106 | return; 107 | } 108 | await Database.Client.SetOsuUserPermissionByOid(long.Parse(cmd), "restricted"); 109 | await target.reply($"restricted"); 110 | return; 111 | case 2: 112 | if (await Database.Client.GetUser(cmd) == null) 113 | { 114 | await target.reply($"该用户未注册desu.life账户,请重新确认"); 115 | return; 116 | } 117 | await Database.Client.SetOsuUserPermissionByEmail(cmd, "restricted"); 118 | await target.reply($"restricted"); 119 | return; 120 | default: 121 | return; 122 | } 123 | } 124 | 125 | public static async Task SuDailyUpdateAsync(Target target) 126 | { 127 | await target.reply("已手动开始数据更新,稍后会发送结果。"); 128 | var _ = Task.Run(async () => { 129 | var (count, span) = await GeneralUpdate.UpdateUsers(); 130 | var Text = "共用时"; 131 | if (span.Hours > 0) Text += $" {span.Hours} 小时"; 132 | if (span.Minutes > 0) Text += $" {span.Minutes} 分钟"; 133 | Text += $" {span.Seconds} 秒"; 134 | try 135 | { 136 | await target.reply($"数据更新完成,一共更新了 {count} 个用户\n{Text}"); 137 | } 138 | catch 139 | { 140 | await target.reply($"数据更新完成\n{Text}"); 141 | } 142 | }); 143 | } 144 | 145 | 146 | 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/functions/osu/GeneralUpdate.cs: -------------------------------------------------------------------------------- 1 | using KanonBot.API; 2 | using Serilog; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Runtime.CompilerServices; 7 | using System.Security.Cryptography; 8 | using System.Diagnostics; 9 | using System.Threading.Tasks; 10 | using static KanonBot.Database.Model; 11 | using CronNET.Impl; 12 | using System.IO; 13 | using static KanonBot.API.OSU.OSUExtensions; 14 | using DotNext.Collections.Generic; 15 | 16 | namespace KanonBot.Functions.OSU 17 | { 18 | public static class GeneralUpdate 19 | { 20 | private static readonly CronDaemon daemon = new(); 21 | public static void DailyUpdate() 22 | { 23 | // * * * * * 24 | // ┬ ┬ ┬ ┬ ┬ 25 | // │ │ │ │ │ 26 | // │ │ │ │ │ 27 | // │ │ │ │ └───── day of week (0 - 6) (Sunday=0 ) 28 | // │ │ │ └────────── month (1 - 12) 29 | // │ │ └─────────────── day of month (1 - 31) 30 | // │ └──────────────────── hour (0 - 23) 31 | // └───────────────────────── min (0 - 59) 32 | // `* * * * *` Every minute. 33 | // `0 * * * *` Top of every hour. 34 | // `0,1,2 * * * *` Every hour at minutes 0, 1, and 2. 35 | // `*/2 * * * *` Every two minutes. 36 | // `1-55 * * * *` Every minute through the 55th minute. 37 | // `* 1,10,20 * * *` Every 1st, 10th, and 20th hours. 38 | 39 | daemon.Add(new CronJob(async () => 40 | { 41 | Log.Information("开始每日用户数据更新"); 42 | var (count, span) = await UpdateUsers(); 43 | Log.Information("更新完毕,总花费时间 {0}s", span.TotalSeconds); 44 | Log.Information("启动检查徽章有效期任务"); 45 | await OSUBot.Badge.CheckBadgeIsVaildJob(); 46 | Log.Information("检查徽章有效期任务完成"); 47 | Environment.Exit(0); 48 | }, "DailyUpdate", "0 4 * * *")); // 每天早上4点运行的意思,具体参考https://crontab.cronhub.io/ 49 | daemon.Start(CancellationToken.None); 50 | } 51 | 52 | 53 | 54 | async public static Task<(long, TimeSpan)> UpdateUsers() 55 | { 56 | var stopwatch = Stopwatch.StartNew(); 57 | var userList = await Database.Client.GetOsuUserList(); 58 | 59 | await Parallel.ForEachAsync(userList, new ParallelOptions { MaxDegreeOfParallelism = 4 }, async (userID, _) => { 60 | try 61 | { 62 | await UpdateUser(userID, false); 63 | } 64 | catch (System.Exception e) 65 | { 66 | Log.Warning("更新用户信息时出错,ex: {@0}", e); 67 | } 68 | }); 69 | 70 | stopwatch.Stop(); 71 | 72 | //删除头像以及osu!web缓存 73 | Directory.GetFiles($"./work/avatar/").ForEach(file => { 74 | try { File.Delete(file); } catch { } 75 | }); 76 | 77 | Directory.GetFiles($"./work/legacy/v1_cover/osu!web/").ForEach(file => { 78 | try { File.Delete(file); } catch { } 79 | }); 80 | 81 | return (userList.Count, stopwatch.Elapsed); 82 | } 83 | 84 | static readonly IReadOnlyList modes = [API.OSU.Mode.OSU, API.OSU.Mode.Taiko, API.OSU.Mode.Fruits, API.OSU.Mode.Mania]; 85 | async public static Task UpdateUser(long userID, bool is_newuser) 86 | { 87 | foreach (var mode in modes) 88 | { 89 | Log.Information($"正在更新用户数据....[{userID}/{mode}]"); 90 | var userInfo = await API.OSU.Client.GetUser(userID, mode); 91 | if (userInfo == null) continue; 92 | if (userInfo.Statistics.PP == 0) continue; 93 | OsuArchivedRec rec = new() 94 | { 95 | uid = userInfo!.Id, 96 | play_count = (int)userInfo.Statistics.PlayCount, 97 | ranked_score = userInfo.Statistics.RankedScore, 98 | total_score = userInfo.Statistics.TotalScore, 99 | total_hit = userInfo.Statistics.TotalHits, 100 | level = userInfo.Statistics.Level.Current, 101 | level_percent = userInfo.Statistics.Level.Progress, 102 | performance_point = userInfo.Statistics.PP, 103 | accuracy = userInfo.Statistics.HitAccuracy, 104 | count_SSH = userInfo.Statistics.GradeCounts.SSH, 105 | count_SS = userInfo.Statistics.GradeCounts.SS, 106 | count_SH = userInfo.Statistics.GradeCounts.SH, 107 | count_S = userInfo.Statistics.GradeCounts.S, 108 | count_A = userInfo.Statistics.GradeCounts.A, 109 | playtime = (int)userInfo.Statistics.PlayTime, 110 | country_rank = (int)userInfo.Statistics.CountryRank, 111 | global_rank = (int)userInfo.Statistics.GlobalRank, 112 | gamemode = mode.ToStr() 113 | }; 114 | await Database.Client.InsertOsuUserData(rec, false); 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/functions/osu/getbg.cs: -------------------------------------------------------------------------------- 1 | using KanonBot.Drivers; 2 | using KanonBot.Message; 3 | using KanonBot.API; 4 | using SixLabors.ImageSharp.Formats.Png; 5 | using SixLabors.ImageSharp.Formats.Jpeg; 6 | using System.IO; 7 | using LanguageExt.UnsafeValueAccess; 8 | using KanonBot.Functions.OSU; 9 | using KanonBot.OsuPerformance; 10 | 11 | 12 | namespace KanonBot.Functions.OSUBot 13 | { 14 | public class GetBackground 15 | { 16 | async public static Task Execute(Target target, string cmd) 17 | { 18 | var command = BotCmdHelper.CmdParser( 19 | cmd, 20 | BotCmdHelper.FuncType.Search, 21 | false, 22 | true, 23 | true, 24 | false, 25 | false 26 | ); 27 | 28 | var index = Math.Max(0, command.order_number - 1); 29 | var isBid = int.TryParse(command.search_arg, out var bid); 30 | 31 | bool beatmapFound = true; 32 | API.OSU.Models.BeatmapSearchResult? beatmaps = null; 33 | API.OSU.Models.Beatmapset? beatmapset = null; 34 | 35 | beatmaps = await API.OSU.Client.SearchBeatmap(command.search_arg, null); 36 | if (beatmaps != null && isBid) { 37 | beatmaps.Beatmapsets = beatmaps.Beatmapsets.OrderByDescending(x => x.Beatmaps.Find(y => y.BeatmapId == bid) != null).ToList(); 38 | } 39 | beatmapset = beatmaps?.Beatmapsets.Skip(index).FirstOrDefault(); 40 | 41 | if (beatmapset == null) 42 | { 43 | beatmapFound = false; 44 | } 45 | else if (beatmapset.Beatmaps == null) 46 | { 47 | beatmapFound = false; 48 | } 49 | else if (beatmapset.Beatmaps.Length == 0) 50 | { 51 | beatmapFound = false; 52 | } 53 | 54 | if (!beatmapFound) 55 | { 56 | beatmaps = await API.OSU.Client.SearchBeatmap(command.search_arg, null, false); 57 | beatmapFound = true; 58 | } 59 | 60 | if (beatmaps != null && isBid) { 61 | beatmaps.Beatmapsets = beatmaps.Beatmapsets.OrderByDescending(x => x.Beatmaps.Find(y => y.BeatmapId == bid) != null).ToList(); 62 | } 63 | beatmapset = beatmaps?.Beatmapsets.Skip(index).FirstOrDefault(); 64 | 65 | if (beatmapset == null) 66 | { 67 | beatmapFound = false; 68 | } 69 | else if (beatmapset.Beatmaps == null) 70 | { 71 | beatmapFound = false; 72 | } 73 | else if (beatmapset.Beatmaps.Length == 0) 74 | { 75 | beatmapFound = false; 76 | } 77 | 78 | if (!beatmapFound) 79 | { 80 | await target.reply("未找到谱面。"); 81 | return; 82 | } 83 | 84 | // beatmapset!.Beatmaps = beatmapset 85 | // .Beatmaps!.OrderByDescending(x => x.DifficultyRating) 86 | // .ToArray(); 87 | 88 | // API.OSU.Models.Beatmap? beatmap = null; 89 | 90 | // if (isBid) 91 | // { 92 | // beatmap = beatmapset 93 | // .Beatmaps.Find(x => x.BeatmapId == command.bid) 94 | // .IfNone(() => beatmapset.Beatmaps.First()); 95 | // } 96 | // else 97 | // { 98 | // beatmap = beatmapset.Beatmaps.First(); 99 | // } 100 | 101 | // beatmap.Beatmapset = beatmaps!.Beatmapsets[0]; 102 | 103 | await target.reply($"https://assets.ppy.sh/beatmaps/{beatmapset!.Id}/covers/raw.jpg"); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/functions/osu/ppvs.cs: -------------------------------------------------------------------------------- 1 | using KanonBot.Drivers; 2 | using KanonBot.Message; 3 | using KanonBot.API; 4 | using KanonBot.Serializer; 5 | using SixLabors.ImageSharp.Formats.Jpeg; 6 | using System.IO; 7 | 8 | namespace KanonBot.Functions.OSUBot 9 | { 10 | public class PPvs 11 | { 12 | public async static Task Execute(Target target, string cmd) 13 | { 14 | var cmds = cmd.Split('#'); 15 | if (cmds.Length == 1) { 16 | if (cmds[0].Length == 0) 17 | { 18 | await target.reply("!ppvs 要对比的用户"); 19 | return; 20 | } 21 | 22 | 23 | var AccInfo = Accounts.GetAccInfo(target); 24 | var DBUser = await Accounts.GetAccount(AccInfo.uid, AccInfo.platform); 25 | if (DBUser == null) 26 | { await target.reply("你还没有绑定 desu.life 账户,先使用 !reg 你的邮箱 来进行绑定或注册哦。"); return; } 27 | 28 | var _u = await Database.Client.GetUsersByUID(AccInfo.uid, AccInfo.platform); 29 | var DBOsuInfo = (await Accounts.CheckOsuAccount(_u!.uid))!; 30 | if (DBOsuInfo == null) 31 | { await target.reply("你还没有绑定osu账户,请使用 !bind osu 你的osu用户名 来绑定你的osu账户喵。"); return; } 32 | 33 | // 分别获取两位的信息 34 | var userSelf = await API.OSU.Client.GetUser(DBOsuInfo.osu_uid); 35 | if (userSelf == null) 36 | { 37 | await target.reply("被办了。"); 38 | return; 39 | } 40 | 41 | var user2 = await API.OSU.Client.GetUser(cmds[0]); 42 | if (user2 == null) 43 | { 44 | await target.reply("猫猫没有找到此用户。"); 45 | return; 46 | } 47 | 48 | await target.reply("正在获取pp+数据,请稍等。。"); 49 | 50 | LegacyImage.Draw.PPVSPanelData data = new(); 51 | 52 | var d1 = await Database.Client.GetOsuPPlusData(userSelf.Id); 53 | if (d1 == null) 54 | { 55 | var d1temp = await API.OSU.Client.TryGetUserPlusData(userSelf); 56 | if (d1temp == null) 57 | { 58 | await target.reply("获取pp+数据时出错,等会儿再试试吧"); 59 | return; 60 | } 61 | d1 = d1temp.User; 62 | await Database.Client.UpdateOsuPPlusData(d1, userSelf.Id); 63 | } 64 | data.u2Name = userSelf.Username; 65 | data.u2 = d1; 66 | 67 | var d2 = await Database.Client.GetOsuPPlusData(user2.Id); 68 | if (d2 == null) 69 | { 70 | var d2temp = await API.OSU.Client.TryGetUserPlusData(user2); 71 | if (d2temp == null) 72 | { 73 | await target.reply("获取pp+数据时出错,等会儿再试试吧"); 74 | return; 75 | } 76 | d2 = d2temp.User; 77 | await Database.Client.UpdateOsuPPlusData(d2, user2.Id); 78 | } 79 | data.u1Name = user2.Username; 80 | data.u1 = d2; 81 | 82 | using var stream = new MemoryStream(); 83 | using var img = await LegacyImage.Draw.DrawPPVS(data); 84 | await img.SaveAsync(stream, new JpegEncoder()); 85 | await target.reply(new Chain().image(Convert.ToBase64String(stream.ToArray(), 0, (int)stream.Length), ImageSegment.Type.Base64)); 86 | } else if (cmds.Length == 2) { 87 | if (cmds[0].Length == 0 || cmds[1].Length == 0) 88 | { 89 | await target.reply("!ppvs 用户1#用户2"); 90 | return; 91 | } 92 | 93 | // 分别获取两位的信息 94 | var user1 = await API.OSU.Client.GetUser(cmds[0]); 95 | if (user1 == null) 96 | { 97 | await target.reply($"猫猫没有找到叫 {cmds[0]} 用户。"); 98 | return; 99 | } 100 | 101 | var user2 = await API.OSU.Client.GetUser(cmds[1]); 102 | if (user2 == null) 103 | { 104 | await target.reply($"猫猫没有找到叫 {cmds[1]} 用户。"); 105 | return; 106 | } 107 | 108 | await target.reply("正在获取pp+数据,请稍等。。"); 109 | 110 | LegacyImage.Draw.PPVSPanelData data = new(); 111 | 112 | var d1 = await Database.Client.GetOsuPPlusData(user1.Id); 113 | if (d1 == null) 114 | { 115 | var d1temp = await API.OSU.Client.TryGetUserPlusData(user1); 116 | if (d1temp == null) 117 | { 118 | await target.reply("获取pp+数据时出错,等会儿再试试吧"); 119 | return; 120 | } 121 | d1 = d1temp.User; 122 | await Database.Client.UpdateOsuPPlusData(d1, user1.Id); 123 | } 124 | data.u2Name = user1.Username; 125 | data.u2 = d1; 126 | 127 | var d2 = await Database.Client.GetOsuPPlusData(user2.Id); 128 | if (d2 == null) 129 | { 130 | var d2temp = await API.OSU.Client.TryGetUserPlusData(user2); 131 | if (d2temp == null) 132 | { 133 | await target.reply("获取pp+数据时出错,等会儿再试试吧"); 134 | return; 135 | } 136 | d2 = d2temp.User; 137 | await Database.Client.UpdateOsuPPlusData(d2, user2.Id); 138 | } 139 | data.u1Name = user2.Username; 140 | data.u1 = d2; 141 | 142 | 143 | using var stream = new MemoryStream(); 144 | using var img = await LegacyImage.Draw.DrawPPVS(data); 145 | await img.SaveAsync(stream, new JpegEncoder()); 146 | await target.reply(new Chain().image(Convert.ToBase64String(stream.ToArray(), 0, (int)stream.Length), ImageSegment.Type.Base64)); 147 | } else { 148 | await target.reply("!ppvs 用户1#用户2/!ppvs 要对比的用户"); 149 | } 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/functions/osu/update.cs: -------------------------------------------------------------------------------- 1 | using KanonBot.Drivers; 2 | using KanonBot.Message; 3 | using KanonBot.API; 4 | using KanonBot.Functions.OSU; 5 | using System.IO; 6 | using LanguageExt.UnsafeValueAccess; 7 | using KanonBot.API.OSU; 8 | 9 | namespace KanonBot.Functions.OSUBot 10 | { 11 | public class Update 12 | { 13 | async public static Task Execute(Target target, string cmd) 14 | { 15 | #region 验证 16 | long? osuID = null; 17 | API.OSU.Mode? mode; 18 | Database.Model.User? DBUser = null; 19 | Database.Model.UserOSU? DBOsuInfo = null; 20 | 21 | // 解析指令 22 | var command = BotCmdHelper.CmdParser(cmd, BotCmdHelper.FuncType.Info); 23 | mode = command.osu_mode; 24 | 25 | // 解析指令 26 | if (command.self_query) 27 | { 28 | // 验证账户 29 | var AccInfo = Accounts.GetAccInfo(target); 30 | DBUser = await Accounts.GetAccount(AccInfo.uid, AccInfo.platform); 31 | if (DBUser == null) 32 | { await target.reply("你还没有绑定 desu.life 账户,先使用 !reg 你的邮箱 来进行绑定或注册哦。"); return; } 33 | // 验证账号信息 34 | DBOsuInfo = await Accounts.CheckOsuAccount(DBUser.uid); 35 | if (DBOsuInfo == null) 36 | { await target.reply("你还没有绑定osu账户,请使用 !bind osu 你的osu用户名 来绑定你的osu账户喵。"); return; } 37 | mode ??= DBOsuInfo.osu_mode?.ToMode()!.Value; // 从数据库解析,理论上不可能错 38 | osuID = DBOsuInfo.osu_uid; 39 | } 40 | else 41 | { 42 | // 查询用户是否绑定 43 | var (atOSU, atDBUser) = await Accounts.ParseAtOsu(command.osu_username); 44 | if (atOSU.IsNone && !atDBUser.IsNone) { 45 | DBUser = atDBUser.ValueUnsafe(); 46 | DBOsuInfo = await Accounts.CheckOsuAccount(DBUser.uid); 47 | if (DBOsuInfo == null) 48 | { 49 | await target.reply("ta还没有绑定osu账户呢。"); 50 | } 51 | else 52 | { 53 | await target.reply("被办了。"); 54 | } 55 | return; 56 | } else if (!atOSU.IsNone && atDBUser.IsNone) { 57 | var _osuinfo = atOSU.ValueUnsafe(); 58 | mode ??= _osuinfo.Mode; 59 | osuID = _osuinfo.Id; 60 | } else if (!atOSU.IsNone && !atDBUser.IsNone) { 61 | DBUser = atDBUser.ValueUnsafe(); 62 | DBOsuInfo = await Accounts.CheckOsuAccount(DBUser.uid); 63 | var _osuinfo = atOSU.ValueUnsafe(); 64 | mode ??= DBOsuInfo!.osu_mode?.ToMode()!.Value; 65 | osuID = _osuinfo.Id; 66 | } else { 67 | // 普通查询 68 | var tempOsuInfo = await API.OSU.Client.GetUser(command.osu_username, command.osu_mode ?? API.OSU.Mode.OSU); 69 | if (tempOsuInfo != null) 70 | { 71 | DBOsuInfo = await Database.Client.GetOsuUser(tempOsuInfo.Id); 72 | if (DBOsuInfo != null) 73 | { 74 | DBUser = await Accounts.GetAccountByOsuUid(tempOsuInfo.Id); 75 | mode ??= DBOsuInfo.osu_mode?.ToMode()!.Value; 76 | } 77 | mode ??= tempOsuInfo.Mode; 78 | osuID = tempOsuInfo.Id; 79 | } 80 | else 81 | { 82 | // 直接取消查询,简化流程 83 | await target.reply("猫猫没有找到此用户。"); 84 | return; 85 | } 86 | } 87 | } 88 | 89 | // 验证osu信息 90 | var OnlineOsuInfo = await API.OSU.Client.GetUser(osuID!.Value, mode!.Value); 91 | if (OnlineOsuInfo == null) 92 | { 93 | await target.reply("猫猫没有找到此用户。"); 94 | // 中断查询 95 | return; 96 | } 97 | OnlineOsuInfo.Mode = mode!.Value; 98 | #endregion 99 | 100 | await target.reply("少女祈祷中..."); 101 | 102 | if (DBUser is not null) { 103 | var sbdbinfo = await Accounts.CheckPpysbAccount(DBUser.uid); 104 | if (sbdbinfo is not null) { 105 | try { File.Delete($"./work/avatar/sb-{sbdbinfo.osu_uid}.png"); } catch { } 106 | } 107 | } 108 | 109 | //try { File.Delete($"./work/v1_cover/{OnlineOsuInfo!.Id}.png"); } catch { } 110 | try { File.Delete($"./work/avatar/{OnlineOsuInfo!.Id}.png"); } catch { } 111 | try { File.Delete($"./work/legacy/v1_cover/osu!web/{OnlineOsuInfo!.Id}.png"); } catch { } 112 | await target.reply("主要数据已更新完毕,pp+数据正在后台更新,请稍后使用info功能查看结果。"); 113 | 114 | _ = Task.Run(async () => { 115 | try { await Database.Client.UpdateOsuPPlusData((await API.OSU.Client.TryGetUserPlusData(OnlineOsuInfo!))!.User, OnlineOsuInfo!.Id); } 116 | catch { }//更新pp+失败,不返回信息 117 | }); 118 | 119 | } 120 | } 121 | } 122 | --------------------------------------------------------------------------------