├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── dotnet.yml │ ├── dotnet_datamaintenance.yml │ └── dotnet_web.yml ├── .gitignore ├── Bleatingsheep.NewHydrant.Bot.Common ├── Bleatingsheep.NewHydrant.Bot.Common.csproj ├── Chrome.cs ├── CitedImageUrlUtility.cs ├── Extensions │ └── EnumerableExtensions.cs └── Osu │ ├── LegacyDataProviderExtensions.cs │ └── OsuHelper.cs ├── Bleatingsheep.NewHydrant.Bot.Private ├── Admin │ ├── AdminVerifier.cs │ ├── IVerifier.cs │ └── Rebind.cs ├── Bleatingsheep.NewHydrant.Bot.Private.csproj ├── Core │ └── Bind.cs ├── Osu │ ├── ApiV2.cs │ ├── BloodcatUtilities.cs │ ├── Newbie │ │ ├── CqExtensions.cs │ │ ├── HardcodedProvider.cs │ │ ├── INewbieInfoProvider.cs │ │ ├── NewbieCardChecker.cs │ │ ├── NewbieKeeper.cs │ │ ├── NotifyOnJoinRequest.Charts.cs │ │ ├── NotifyOnJoinRequest.TrustedUserInfo.cs │ │ ├── NotifyOnJoinRequest.cs │ │ └── 统计新人群成员.cs │ ├── NewlyRankedBeatmapNotify.cs │ ├── PPBeatmapInfo.cs │ ├── Recommendations │ │ ├── Clear.cs │ │ └── RetriveRecommendationData.cs │ ├── Snapshots │ │ ├── SyncSchedule.cs │ │ └── UpdateSnapshotsJob.cs │ ├── UpdatePlusData.cs │ └── Yearly │ │ └── YearlyCachePreparation.cs ├── Tests │ ├── ChromeRelaunch.cs │ ├── ChromeTabCountReport.cs │ ├── ImageTest.cs │ ├── LoadAvg.cs │ └── 渲染任意页面.cs └── 啥玩意儿啊 │ ├── LocalTime.cs │ ├── MemePost │ ├── MemePostInformation.cs │ └── PostToMemeRepository.cs │ └── ShowLocalTime.cs ├── Bleatingsheep.NewHydrant.Bot.Public ├── Bleatingsheep.NewHydrant.Bot.Public.csproj ├── Mahjong │ ├── IMajsoulAnalyzer.cs │ ├── LocalAkochanReviewer.cs │ ├── Mahjong.cs │ ├── MahjongObjectStorage.cs │ ├── MahjongOptions.cs │ ├── MajsoulDanPTProvider.cs │ └── RemoteAkochanReviewer.cs ├── Osu │ ├── ArilyInfo.cs │ ├── BPMe.cs │ ├── C8Mod.cs │ ├── Highlight.cs │ ├── PerformancePlusUser.cs │ ├── Plus │ │ ├── IPlusApi.cs │ │ ├── PlusPlus.cs │ │ ├── PlusReturn.cs │ │ └── Recommendation.cs │ ├── PpTth2.cs │ ├── QueryHelper.cs │ ├── QueryMotherShip.cs │ ├── QueryTrigger.cs │ ├── Recommendations │ │ └── Recommand.cs │ ├── Snapshots │ │ ├── BotCommandTrigger.cs │ │ ├── SnapshotUtility.cs │ │ ├── SpeakingTrigger.cs │ │ └── UserParameter.cs │ └── Yearly │ │ └── MyYearly.cs ├── Utilities │ ├── DateUtility.cs │ └── IncrementUtility.cs └── 啥玩意儿啊 │ ├── Adhd.cs │ ├── Exchange │ ├── BocRateClient.cs │ ├── CibRate.cs │ ├── CmbcRate.cs │ ├── ExchangeRates.cs │ ├── ExchangeResponse.cs │ ├── ICibRate.cs │ ├── ICmbRateProvider.cs │ └── IExchangeRate.cs │ ├── IP.cs │ ├── Moebooru │ ├── AdvancedKonachan.cs │ ├── Api.cs │ ├── Konachan.cs │ └── Post.cs │ ├── Pixiv.cs │ ├── 估值.cs │ ├── 加拿大新冠数据.cs │ ├── 帮助.cs │ ├── 日本新冠数据.cs │ ├── 标普500期货.cs │ ├── 牛津词典.cs │ ├── 真随机数.cs │ ├── 知乎日报.cs │ ├── 称金币.ExecuteInfo.cs │ ├── 称金币.cs │ ├── 美国新冠数据.cs │ └── 获取图片链接.cs ├── Bleatingsheep.NewHydrant.Bot ├── Bleatingsheep.NewHydrant.Bot.csproj ├── HydrantStartup.cs ├── NLog.config ├── Program.cs ├── ReplicaConfig.cs └── appsettings.json.template ├── Bleatingsheep.NewHydrant.Data ├── Bleatingsheep.NewHydrant.Data.csproj ├── DataMaintainer.cs ├── DataProvider.cs ├── IDataProvider.cs ├── ILegacyDataProvider.cs ├── IOsuDataUpdator.cs ├── OsuDataUpdator.cs ├── Results │ ├── IXfsDataResult.cs │ ├── XfsDataError.cs │ ├── XfsDataResult.cs │ └── XfsDataResultExtensions.cs └── global.cs ├── Bleatingsheep.NewHydrant.DataMaintenance ├── Bleatingsheep.NewHydrant.DataMaintenance.csproj ├── NLog.config ├── Program.cs ├── Properties │ └── launchSettings.json ├── SyncScheduleService.cs ├── UpdateSnapshotsService.cs ├── Worker.cs └── appsettings.Development.json ├── Bleatingsheep.NewHydrant ├── Attributions │ ├── ComponentAttribute.cs │ ├── IInitializable.cs │ ├── IMessageCommand.cs │ ├── IMessageMonitor.cs │ ├── IRegularAsync.cs │ └── ParameterAttribute.cs ├── Bleatingsheep.NewHydrant.csproj ├── Core │ ├── ExecutingException.cs │ ├── Hydrant.cs │ ├── IHydrantStartup.cs │ ├── ScheduleInfo.cs │ ├── Service.cs │ └── TypeExtensions.cs ├── Extentions │ └── EnumerableExtensions.cs ├── LICENSE ├── TODO.md └── Template.cs ├── Bleatingsheep.OsuApiClient ├── Beatmap.cs ├── BestPerformance.cs ├── Bleatingsheep.OsuMixedApi.csproj ├── BloodcatApi.cs ├── Diagnostics.cs ├── Execute.cs ├── HttpMethods.cs ├── Iso3166.cs ├── Mode.cs ├── ModeExtensions.cs ├── Mods.cs ├── ModsExtensions.cs ├── MotherShip │ ├── MotherShipApiClient.cs │ ├── MotherShipResponse.cs │ ├── MotherShipUserInfo.cs │ └── UserHistory.cs ├── OsuApiClient.cs ├── OsuApiFailedException.cs ├── PlayRecord.cs ├── System.Collections.Generic │ └── CollectionExtensions.cs ├── ThreadSafeRandom.cs └── UserInfo.cs ├── Bleatingsheep.OsuQqBot.Database ├── Bleatingsheep.OsuQqBot.Database.csproj ├── Migrations │ ├── 20220609123332_MigrateToPostgres.Designer.cs │ ├── 20220609123332_MigrateToPostgres.cs │ ├── 20220805141925_AddUserField.Designer.cs │ ├── 20220805141925_AddUserField.cs │ ├── 20220830195158_AddGroupField.Designer.cs │ ├── 20220830195158_AddGroupField.cs │ ├── 20220830202628_FixNullConstraint.Designer.cs │ ├── 20220830202628_FixNullConstraint.cs │ ├── 20221210013516_AddBeatmapInfoCache.Designer.cs │ ├── 20221210013516_AddBeatmapInfoCache.cs │ ├── 20221223165347_AllowNullBeatmapInfoCache,AddExpirationDate.Designer.cs │ ├── 20221223165347_AllowNullBeatmapInfoCache,AddExpirationDate.cs │ ├── 20230315032711_RemoveOldConcurrencyCheckFieldDueToType.Designer.cs │ ├── 20230315032711_RemoveOldConcurrencyCheckFieldDueToType.cs │ ├── 20230624200901_AddRecommendationPP.Designer.cs │ ├── 20230624200901_AddRecommendationPP.cs │ ├── 20230805223750_AddUserPlayRecordId.Designer.cs │ ├── 20230805223750_AddUserPlayRecordId.cs │ └── NewbieContextModelSnapshot.cs └── Models │ ├── BeatmapInfoCacheEntry.cs │ ├── BindingInfo.cs │ ├── BotGroupField.cs │ ├── BotUserField.cs │ ├── DuplicateAuthentication.cs │ ├── MessageEntry.cs │ ├── NewbieContext.cs │ ├── OperationHistory.cs │ ├── PlayRecordQueryTemp.cs │ ├── PlusHistory.cs │ ├── RecommendationEntry.cs │ ├── RelationshipInfo.cs │ ├── UpdateSchedule.cs │ ├── UserPlayRecord.cs │ ├── UserSnapshot.cs │ └── WebLog.cs ├── LICENSE ├── NewHydrantApi ├── Controllers │ ├── BiliLiveAddController.cs │ ├── BindingController.cs │ ├── IPController.cs │ ├── MyIPController.cs │ ├── PlusController.cs │ ├── UserPlays.cs │ └── UserSnapshotController.cs ├── NewHydrantApi.csproj ├── Program.cs ├── Startup.cs ├── appsettings.Development.json ├── appsettings.template.json └── nlog.config ├── OsuQqBotHttp.sln ├── README.md ├── README_resources ├── binding.png ├── entrance.png ├── highlight.png ├── ppplus.png ├── pptth-response.png ├── pptth.png └── profile.png ├── Tests.Database ├── Program.cs └── Tests.Database.csproj ├── UnitTests ├── ApiUnitTest.cs ├── IncrementFormatTests.cs ├── RegexTest.cs └── UnitTests.csproj └── publish.ps1 /.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 64 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: Build and deploy xfs 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | paths: 7 | - 'Bleatingsheep.NewHydrant/**' 8 | - 'Bleatingsheep.NewHydrant.Bot/**' 9 | - 'Bleatingsheep.NewHydrant.Bot.Common/**' 10 | - 'Bleatingsheep.NewHydrant.Bot.Private/**' 11 | - 'Bleatingsheep.NewHydrant.Bot.Public/**' 12 | - 'Bleatingsheep.NewHydrant.Data/**' 13 | - 'Bleatingsheep.OsuMixedApi/**' 14 | - 'Bleatingsheep.OsuQqBot.Database/**' 15 | - '.github/workflows/dotnet.yml' 16 | pull_request: 17 | branches: [ master ] 18 | 19 | jobs: 20 | build: 21 | 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | - name: Setup .NET 27 | uses: actions/setup-dotnet@v1 28 | with: 29 | dotnet-version: 8.0.x 30 | - name: Build 31 | run: dotnet build -c Release Bleatingsheep.NewHydrant.Bot 32 | - name: Publish 33 | run: dotnet publish --no-build -c Release -o bin/publish Bleatingsheep.NewHydrant.Bot 34 | - name: Upload a Build Artifact 35 | uses: actions/upload-artifact@v4 36 | with: 37 | name: Binary 38 | path: bin/publish 39 | deploy: 40 | if: github.event_name == 'push' 41 | needs: build 42 | runs-on: ubuntu-latest 43 | steps: 44 | - name: Download artifact from build job 45 | uses: actions/download-artifact@v4 46 | with: 47 | name: Binary 48 | path: bin/publish 49 | - name: Push to Other Branches 50 | uses: peaceiris/actions-gh-pages@v3 51 | with: 52 | github_token: ${{ secrets.GITHUB_TOKEN }} 53 | publish_dir: bin/publish 54 | publish_branch: build -------------------------------------------------------------------------------- /.github/workflows/dotnet_datamaintenance.yml: -------------------------------------------------------------------------------- 1 | name: Build and deploy xfs data maintenance service 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | paths: 7 | - 'Bleatingsheep.NewHydrant.Data/**' 8 | - 'Bleatingsheep.NewHydrant.DataMaintenance/**' 9 | - 'Bleatingsheep.OsuQqBot.Database/**' 10 | - '.github/workflows/dotnet_datamaintenance.yml' 11 | pull_request: 12 | branches: [ master ] 13 | 14 | jobs: 15 | build: 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Setup .NET 22 | uses: actions/setup-dotnet@v1 23 | with: 24 | dotnet-version: 8.0.x 25 | - name: Build 26 | run: dotnet build -c Release Bleatingsheep.NewHydrant.DataMaintenance 27 | - name: Publish 28 | run: dotnet publish --no-build -c Release -o bin/publish Bleatingsheep.NewHydrant.DataMaintenance 29 | - name: Upload a Build Artifact 30 | uses: actions/upload-artifact@v4 31 | with: 32 | name: Binary 33 | path: bin/publish 34 | deploy: 35 | if: github.event_name == 'push' 36 | needs: build 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: Download artifact from build job 40 | uses: actions/download-artifact@v4 41 | with: 42 | name: Binary 43 | path: bin/publish 44 | - name: Push to Other Branches 45 | uses: peaceiris/actions-gh-pages@v3 46 | with: 47 | github_token: ${{ secrets.GITHUB_TOKEN }} 48 | publish_dir: bin/publish 49 | publish_branch: build_datamaintenance -------------------------------------------------------------------------------- /.github/workflows/dotnet_web.yml: -------------------------------------------------------------------------------- 1 | name: Build and deploy xfs web 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | paths: 7 | - 'Bleatingsheep.OsuQqBot.Database/**' 8 | - 'NewHydrantApi/**' 9 | - '.github/workflows/dotnet_web.yml' 10 | pull_request: 11 | branches: [ master ] 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Setup .NET 21 | uses: actions/setup-dotnet@v1 22 | with: 23 | dotnet-version: 8.0.x 24 | - name: Build 25 | run: dotnet build -c Release NewHydrantApi 26 | - name: Publish 27 | run: dotnet publish --no-build -c Release -o bin/publish_webapi NewHydrantApi 28 | - name: Upload a Build Artifact 29 | uses: actions/upload-artifact@v4 30 | with: 31 | name: Binary 32 | path: bin/publish_webapi 33 | deploy: 34 | if: github.event_name == 'push' 35 | needs: build 36 | runs-on: ubuntu-latest 37 | steps: 38 | - name: Download artifact from build job 39 | uses: actions/download-artifact@v4 40 | with: 41 | name: Binary 42 | path: bin/publish_webapi 43 | - name: Push to Other Branches 44 | uses: peaceiris/actions-gh-pages@v3 45 | with: 46 | github_token: ${{ secrets.GITHUB_TOKEN }} 47 | publish_dir: bin/publish_webapi 48 | publish_branch: build_webapi -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Common/Bleatingsheep.NewHydrant.Bot.Common.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | Bleatingsheep.NewHydrant 6 | latest 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Common/Chrome.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using PuppeteerSharp; 5 | 6 | namespace Bleatingsheep.NewHydrant; 7 | #nullable enable 8 | public static class Chrome 9 | { 10 | private static readonly SemaphoreSlim s_semaphoreSlim = new(1, 1); 11 | 12 | private static IBrowser? s_browser; 13 | 14 | public static string? ChromePath { get; set; } 15 | 16 | private static Task LaunchBrowser() 17 | { 18 | if (ChromePath == null) 19 | { 20 | throw new InvalidOperationException("未设置 Chrome 浏览器的路径。"); 21 | } 22 | 23 | return Puppeteer.LaunchAsync(new LaunchOptions 24 | { 25 | Headless = true, 26 | ExecutablePath = ChromePath, 27 | DefaultViewport = new ViewPortOptions 28 | { 29 | DeviceScaleFactor = 1, 30 | Width = 360, 31 | Height = 640, 32 | }, 33 | Args = new[] { "--no-sandbox", "--lang=zh-CN" }, 34 | }); 35 | } 36 | 37 | private static Func> GetBrowser { get; set; } = async () => 38 | { 39 | await s_semaphoreSlim.WaitAsync(); 40 | try 41 | { 42 | s_browser ??= await LaunchBrowser(); 43 | GetBrowser = () => ValueTask.FromResult(s_browser); 44 | return s_browser; 45 | } 46 | finally 47 | { 48 | s_semaphoreSlim.Release(); 49 | } 50 | }; 51 | 52 | public static async Task RefreashBrowserAsync() 53 | { 54 | IBrowser browser = await LaunchBrowser(); 55 | var oldBrowser = Interlocked.Exchange(ref s_browser, browser); 56 | if (oldBrowser is not null) 57 | { 58 | await oldBrowser.DisposeAsync().ConfigureAwait(false); 59 | } 60 | } 61 | 62 | public static async Task GetTabsAsync() 63 | => await (await GetBrowser()).DefaultContext.PagesAsync().ConfigureAwait(false); 64 | 65 | private static readonly System.Collections.Generic.Dictionary s_extraHeaders = new() 66 | { 67 | ["Accept-Language"] = "zh-CN", 68 | }; 69 | 70 | public static async Task OpenNewPageAsync() 71 | { 72 | var page = await (await GetBrowser()).NewPageAsync().ConfigureAwait(false); 73 | await page.SetExtraHttpHeadersAsync(s_extraHeaders).ConfigureAwait(false); 74 | return page; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Common/CitedImageUrlUtility.cs: -------------------------------------------------------------------------------- 1 | using Sisters.WudiLib.Posts; 2 | using Sisters.WudiLib; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.Logging; 5 | using MessageContext = Sisters.WudiLib.Posts.Message; 6 | 7 | namespace Bleatingsheep.NewHydrant; 8 | #nullable enable 9 | public static class CitedImageUrlUtility 10 | { 11 | public static async ValueTask GetCitedImageUrlAsync(MessageContext context, HttpApiClient api, ILogger logger) 12 | { 13 | // 获取图片 14 | if (!context.Content.Sections[0].Data.TryGetValue("id", out var strMessageId) || !int.TryParse(strMessageId, out int messageId)) 15 | { 16 | logger.LogError("获取消息 ID 失败,引用消息 ID {MessageId}.", context.MessageId); 17 | await api.SendMessageAsync(context.Endpoint, "获取消息 ID 失败,可能需要重新发送图片。"); 18 | return null; 19 | } 20 | var messageResponse = await api.GetMessage(messageId); 21 | if (messageResponse?.Message is not ReceivedMessage message) 22 | { 23 | logger.LogError("获取消息失败,消息 ID:{messageId}", messageId); 24 | await api.SendMessageAsync(context.Endpoint, "获取消息内容失败,可能需要重新发送图片。"); 25 | return null; 26 | } 27 | if (message.Sections is not [{ Type: "image" } s]) 28 | { 29 | await api.SendMessageAsync(context.Endpoint, "引用的消息不是单张图片,请重新选择。"); 30 | return null; 31 | } 32 | if (!s.Data.TryGetValue("url", out var url)) 33 | { 34 | await api.SendMessageAsync(context.Endpoint, "获取图片 URL 失败。"); 35 | return null; 36 | } 37 | return url; 38 | } 39 | } -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Common/Extensions/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Bleatingsheep.NewHydrant.Extentions 6 | { 7 | public static class EnumerableExtensions 8 | { 9 | private static readonly object s_randomLock = new object(); 10 | private static readonly Random s_random = new Random(); 11 | 12 | public static List Randomize(this IEnumerable source) 13 | { 14 | lock (s_randomLock) 15 | { 16 | checked 17 | { 18 | var result = source.ToList(); 19 | // i 是 Count 到 2 20 | for (int i = result.Count - 1; i > 0; i--) 21 | { 22 | var swap = s_random.Next(i + 1); 23 | if (i != swap) 24 | { 25 | var temp = result[i]; 26 | result[i] = result[swap]; 27 | result[swap] = temp; 28 | } 29 | } 30 | return result; 31 | } 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Common/Osu/LegacyDataProviderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Bleatingsheep.NewHydrant.Core; 4 | using Bleatingsheep.NewHydrant.Data; 5 | using Bleatingsheep.OsuMixedApi; 6 | using Microsoft.Extensions.Caching.Memory; 7 | using UserInfo = Bleatingsheep.OsuMixedApi.UserInfo; 8 | 9 | namespace Bleatingsheep.NewHydrant.Osu 10 | { 11 | public static class LegacyDataProviderExtensions 12 | { 13 | /// 14 | /// 确保。 15 | /// 16 | /// 17 | public static async Task EnsureGetBindingIdAsync(this ILegacyDataProvider dataProvider, long qq) 18 | { 19 | var (success, result) = await dataProvider.GetBindingIdAsync(qq); 20 | ExecutingException.Ensure(success, "哎,获取绑定信息失败了。"); 21 | ExecutingException.Ensure(result != null, "没有绑定 osu! 账号。见https://github.com/bltsheep/OsuQqBotForNewbieGroup/wiki/%E5%B0%86-QQ-%E5%8F%B7%E4%B8%8E-osu!-%E8%B4%A6%E5%8F%B7%E7%BB%91%E5%AE%9A"); 22 | return result.Value; 23 | } 24 | 25 | public static async Task EnsureGetUserInfo(this OsuApiClient osuApi, string name, Bleatingsheep.Osu.Mode mode) 26 | { 27 | var (success, result) = await osuApi.GetUserInfoAsync(name, mode); 28 | ExecutingException.Ensure(success, "网络错误。"); 29 | ExecutingException.Ensure(result != null, "无此用户!"); 30 | return result; 31 | } 32 | 33 | // TODO: Get IMemoryCache from DI. 34 | private static readonly IMemoryCache s_cache = new MemoryCache(new MemoryCacheOptions()); 35 | 36 | private static readonly TimeSpan CacheAvailable = TimeSpan.FromMinutes(10); 37 | 38 | public static async Task<(bool, UserInfo)> GetCachedUserInfo(this OsuApiClient osuApi, int id, Bleatingsheep.Osu.Mode mode) 39 | { 40 | var hasCache = s_cache.TryGetValue((id, mode), out var cachedInfo); 41 | if (hasCache) 42 | { 43 | return (true, cachedInfo); 44 | } 45 | var (success, userInfo) = await osuApi.GetUserInfoAsync(id, mode); 46 | if (success) 47 | { 48 | s_cache.Set((id, mode), userInfo, CacheAvailable); 49 | return (true, userInfo); 50 | } 51 | else 52 | // fail 53 | return default; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Common/Osu/OsuHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace Bleatingsheep.NewHydrant.Osu 6 | { 7 | public static class OsuHelper 8 | { 9 | /// 10 | /// 发现用户名用的模式。 11 | /// 12 | private const string DiscoverPattern = "(?<=^|[^0-9A-Za-z_\\-\\[\\]])" + // 匹配字符串开始,或者任何不能使用在osu! username的字符,或者空格(不能使用在username开头) 13 | "[0-9A-Za-z_\\-\\[\\]][0-9A-Za-z_\\-\\[\\] ]{1,}[0-9A-Za-z_\\-\\[\\]]" + // 匹配ID中可以使用的字符,早期用户没有ID长度限制 14 | "(?=$|[^0-9A-Za-z_\\-\\[\\]])"; // 匹配字符串结束,或者不能使用在osu! username的字符,或者空格(不能使用在username结尾) 15 | /// 16 | /// 可以匹配到用户名的模式。 17 | /// 18 | public const string UsernamePattern = "[0-9A-Za-z_\\-\\[\\]][0-9A-Za-z_\\-\\[\\] ]{1,13}[0-9A-Za-z_\\-\\[\\]]"; // 匹配ID中可以使用的字符,其中ID的长度是3-15 19 | /// 20 | /// 判断是不是用户名的模式。 21 | /// 22 | private const string IsUsernamePattern = "^" + UsernamePattern + "$"; 23 | /// 24 | /// 表示用户名边界的模式。 25 | /// 26 | private const string BorderPattern = "[^0-9A-Za-z_\\-\\[\\]]"; 27 | private static readonly Regex IsUsernameRegex = new Regex(IsUsernamePattern, RegexOptions.Compiled); 28 | private static readonly Regex DiscoverRegex = new Regex(DiscoverPattern, RegexOptions.Compiled); 29 | 30 | public static bool IsUsername(string s) => IsUsernameRegex.IsMatch(s); 31 | 32 | public static IEnumerable DiscoverUsernames(string s) => DiscoverRegex.Matches(s).Select(m => m.Value).ToList(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Private/Admin/AdminVerifier.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Bleatingsheep.NewHydrant.Core; 4 | 5 | namespace Bleatingsheep.NewHydrant.Admin 6 | { 7 | internal class AdminVerifier : IVerifier 8 | { 9 | private static readonly HashSet AdminCollection = new HashSet 10 | { 11 | 962549599, // 咩咩羊 12 | 1208604740, // iron 13 | // 1239219529, // taolex 14 | 1061566571, // dalou 15 | 546748348, // 化学式 16 | // 431600414, // 844 17 | // 2482000231, // 杰克王 18 | // 2541721178, // heisiban 19 | 447503971, // 白季 20 | 944072537, // na-gi 21 | 1340691940, // muzi 22 | 178039743, // whir 23 | 2429299722, // sayori 24 | // 1904603706, // 226 25 | 2636027237, // morika 26 | 3203995073, // happy 27 | 2897010516, // pr1mary 28 | 3228981717, // slyuyuko 29 | 1172482284, // UselessPlayer 30 | 1528769425, // m u s e 31 | 2624161473, // guozi 32 | 630060047, // CYCLC 33 | 365246692, // -Spring Night- 34 | 1120180945, // n0000000000o 35 | 2199188467, // NatsuRin 36 | 524986802, // Dragon-Fox 37 | 411843675, // Sakura Luna 38 | 2105109062, // xxbg 39 | 1584775323, // YRScarlet 40 | 3438313440, // MM 41 | 2733494248, // Molli 42 | }; 43 | 44 | public AdminVerifier() 45 | { 46 | } 47 | 48 | public Task IsAdminAsync(long qq) => Task.FromResult(AdminCollection.Contains(qq)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Private/Admin/IVerifier.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Bleatingsheep.NewHydrant.Admin 4 | { 5 | internal interface IVerifier 6 | { 7 | Task IsAdminAsync(long qq); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Private/Bleatingsheep.NewHydrant.Bot.Private.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | Bleatingsheep.NewHydrant 6 | latest 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Private/Osu/ApiV2.cs: -------------------------------------------------------------------------------- 1 | //using System.IO; 2 | //using System.Reflection; 3 | //using System.Threading.Tasks; 4 | //using Bleatingsheep.NewHydrant.Attributions; 5 | //using Bleatingsheep.NewHydrant.Core; 6 | //using Bleatingsheep.Osu.ApiV2; 7 | 8 | //namespace Bleatingsheep.NewHydrant.Osu 9 | //{ 10 | // [Function("api2_provider")] 11 | // internal class ApiV2 : IInitializable 12 | // { 13 | // public static OsuApiV2Client Client { get; private set; } 14 | 15 | // public string Name { get; } = "apiv2"; 16 | 17 | // public async Task InitializeAsync(ExecutingInfo executingInfo) 18 | // { 19 | // var authPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "..", "config", "authv2.txt"); 20 | // var lines = await File.ReadAllLinesAsync(authPath); 21 | // if (lines.Length != 2) 22 | // return false; 23 | // string username = lines[0]; 24 | // string password = lines[1]; 25 | // Client = new OsuApiV2Client(username, password); 26 | // return true; 27 | // } 28 | // } 29 | //} 30 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Private/Osu/BloodcatUtilities.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Bleatingsheep.OsuMixedApi; 3 | using Sisters.WudiLib; 4 | 5 | namespace Bleatingsheep.NewHydrant.Osu 6 | { 7 | internal static class BloodcatUtilities 8 | { 9 | public static Message GetMusicMessage(OsuApiClient osuApi, BloodcatBeatmapSet set) 10 | { 11 | int setId = set.Id; 12 | string info = string.Empty; 13 | info += set.Beatmaps.Max(b => b.TotalLength) + "s, "; 14 | if (set.Beatmaps.Length > 1) 15 | info += $"{set.Beatmaps.Min(b => b.Stars):0.##}* - {set.Beatmaps.Max(b => b.Stars):0.##}*"; 16 | else 17 | info += $"{set.Beatmaps.Single()?.Stars:0.##}*"; 18 | 19 | // Creator and BPM 20 | info += "\r\n" + $"by {set.Creator}"; 21 | var bpms = set.Beatmaps.Select(b => b.Bpm).Distinct().ToList(); 22 | if (bpms.Count == 1) 23 | { 24 | info += $" ♩{bpms.First():#.##}"; 25 | } 26 | 27 | if (!string.IsNullOrEmpty(set.Source)) 28 | info += "\r\n" + $"From {set.Source}"; 29 | Message message = SendingMessage.MusicCustom(osuApi.PageOfSetOld(setId), osuApi.PreviewAudioOf(setId), $"{set.Title}/{set.Artist}", info, osuApi.ThumbOf(setId)); 30 | return message; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Private/Osu/Newbie/CqExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Newtonsoft.Json; 4 | using Sisters.WudiLib; 5 | 6 | namespace Bleatingsheep.NewHydrant.Osu.Newbie 7 | { 8 | 9 | public partial class LevelInfo 10 | { 11 | [JsonProperty("level")] 12 | public int Level { get; set; } 13 | 14 | [JsonProperty("level_speed")] 15 | public double LevelSpeed { get; set; } 16 | 17 | [JsonProperty("nickname")] 18 | public string Nickname { get; set; } 19 | 20 | [JsonProperty("user_id")] 21 | public long UserId { get; set; } 22 | 23 | [JsonProperty("vip_growth_speed")] 24 | public int VipGrowthSpeed { get; set; } 25 | 26 | [JsonProperty("vip_growth_total")] 27 | public int VipGrowthTotal { get; set; } 28 | 29 | [JsonProperty("vip_level")] 30 | public string VipLevel { get; set; } 31 | } 32 | 33 | internal static class CqHttpApiExtensions 34 | { 35 | public static async Task GetLevelInfo(this HttpApiClient api, long qq) 36 | { 37 | try 38 | { 39 | var levelInfo = await api.CallAsync("_get_vip_info", new { user_id = qq }); 40 | return levelInfo; 41 | } 42 | catch (ApiAccessException aae) 43 | when (aae.InnerException is JsonSerializationException e 44 | && e.InnerException is InvalidCastException) 45 | { 46 | return null; 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Private/Osu/Newbie/INewbieInfoProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | 4 | namespace Bleatingsheep.NewHydrant.Osu.Newbie 5 | { 6 | internal interface INewbieInfoProvider 7 | { 8 | Task ShouldIgnoreAsync(long qq); 9 | Task ShouldIgnorePerformanceAsync(long group, long qq); 10 | IEnumerable MonitoredGroups { get; } 11 | double? PerformanceLimit(long group); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Private/Osu/Newbie/NewbieCardChecker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Bleatingsheep.NewHydrant.Core; 4 | 5 | namespace Bleatingsheep.NewHydrant.Osu.Newbie 6 | { 7 | internal abstract class NewbieCardChecker : Service 8 | { 9 | private NewbieCardChecker() { } 10 | 11 | public static INewbieInfoProvider IgnoreListProvider => HardcodedProvider.GetProvider(); 12 | 13 | /// 14 | /// 获取群名片提示。 15 | /// 16 | /// 17 | /// 18 | /// 19 | public static string GetHintMessage(string name, string card) 20 | { 21 | string hint; 22 | if (OsuHelper.DiscoverUsernames(card).Any(u => u.Equals(name, StringComparison.OrdinalIgnoreCase))) 23 | hint = null; 24 | // 用户名不行。 25 | else if (card.Contains(name, StringComparison.OrdinalIgnoreCase)) 26 | { 27 | // 临时忽略。 28 | hint = "建议修改群名片,不要在用户名前后添加可以被用做用户名的字符,以免混淆。"; 29 | hint += "\r\n" + "建议群名片:" + RecommendCard(card, name); 30 | } 31 | else 32 | { 33 | hint = "为了方便其他人认出您,请修改群名片,必须包括正确的 osu! 用户名。"; 34 | } 35 | 36 | return hint; 37 | } 38 | 39 | /// 40 | /// 根据群名片和用户名推荐群名片 41 | /// 42 | private static string RecommendCard(string card, string username) 43 | { 44 | int firstIndex = card.IndexOf(username, StringComparison.OrdinalIgnoreCase); 45 | if (firstIndex != -1) 46 | { 47 | string recommendCard = card.Substring(0, firstIndex); 48 | if (firstIndex != 0) 49 | recommendCard += "|"; 50 | recommendCard += username; 51 | if (firstIndex + username.Length < card.Length) 52 | { 53 | recommendCard += "|" + card.Substring(firstIndex + username.Length); 54 | } 55 | return recommendCard; 56 | } 57 | return null; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Private/Osu/Newbie/NotifyOnJoinRequest.TrustedUserInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace Bleatingsheep.NewHydrant.Osu.Newbie 4 | { 5 | public partial class NotifyOnJoinRequest 6 | { 7 | private class TrustedUserInfo 8 | { 9 | public int Id { get; set; } 10 | public string Name { get; set; } 11 | public int TotalHits { get; set; } 12 | public double Performance { get; set; } 13 | public int PlayCount { get; set; } 14 | public bool IsBanned { get; set; } 15 | 16 | #nullable enable 17 | [return: NotNullIfNotNull("userInfo")] 18 | public static implicit operator TrustedUserInfo?(OsuMixedApi.UserInfo? userInfo) 19 | => userInfo is null ? null : 20 | new TrustedUserInfo 21 | { 22 | Id = userInfo.Id, 23 | Name = userInfo.Name, 24 | TotalHits = userInfo.TotalHits, 25 | Performance = userInfo.Performance, 26 | PlayCount = userInfo.PlayCount, 27 | IsBanned = false, 28 | }; 29 | #nullable restore 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Private/Osu/PPBeatmapInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Bleatingsheep.NewHydrant.Attributions; 6 | using Bleatingsheep.NewHydrant.Core; 7 | using Bleatingsheep.NewHydrant.Data; 8 | using Bleatingsheep.Osu.PerformancePlus; 9 | using Sisters.WudiLib.Posts; 10 | 11 | namespace Bleatingsheep.NewHydrant.Osu 12 | { 13 | [Component("pp_stars")] 14 | internal class PPBeatmapInfo : Service, IMessageCommand 15 | { 16 | private static readonly PerformancePlusSpider s_spider = new PerformancePlusSpider(); 17 | 18 | public PPBeatmapInfo(ILegacyDataProvider dataProvider, OsuMixedApi.OsuApiClient osuApi) 19 | { 20 | DataProvider = dataProvider; 21 | OsuApi = osuApi; 22 | } 23 | 24 | private ILegacyDataProvider DataProvider { get; } 25 | private OsuMixedApi.OsuApiClient OsuApi { get; } 26 | 27 | public async Task ProcessAsync(Message message, Sisters.WudiLib.HttpApiClient api) 28 | { 29 | long id = message.UserId; 30 | var (networkSuccess, osuResult) = await DataProvider.GetBindingIdAsync(id); 31 | ExecutingException.Ensure(networkSuccess, "无法查询绑定账号。"); 32 | ExecutingException.Ensure(osuResult != null, "未绑定 osu! 游戏账号。"); 33 | 34 | var osuId = osuResult.Value; 35 | var recent = (await OsuApi.GetRecentlyAsync(osuId, OsuMixedApi.Mode.Standard, 1)).FirstOrDefault(); 36 | if (recent == null) 37 | { 38 | await api.SendMessageAsync(message.Endpoint, "没打图!"); 39 | return; 40 | } 41 | 42 | var reply = new List { "/np 给 bleatingsheep,查询更方便!" }; 43 | try 44 | { 45 | var ppBeatmap = await s_spider.GetBeatmapPlusAsync(recent.BeatmapId); 46 | if (ppBeatmap == null) 47 | { 48 | reply.Add("很抱歉,无法查询 Loved 图。也有可能是 PP+ 没有这张图的数据。"); 49 | return; 50 | } 51 | reply.Add($"https://syrin.me/pp+/b/{ppBeatmap.Id}/"); 52 | reply.Add("Stars: " + ppBeatmap.Stars); 53 | reply.Add("Aim (Jump): " + ppBeatmap.AimJump); 54 | reply.Add("Aim (Flow): " + ppBeatmap.AimFlow); 55 | reply.Add("Precision: " + ppBeatmap.Precision); 56 | reply.Add("Speed: " + ppBeatmap.Speed); 57 | reply.Add("Stamina: " + ppBeatmap.Stamina); 58 | reply.Add("Accuracy: " + ppBeatmap.Accuracy); 59 | reply.Add("数据来自 PP+。"); 60 | } 61 | catch (ExceptionPlus) 62 | { 63 | reply.Add("访问 PP+ 网站失败。"); 64 | } 65 | finally 66 | { 67 | await api.SendMessageAsync(message.Endpoint, string.Join("\r\n", reply)); 68 | } 69 | } 70 | 71 | public bool ShouldResponse(Message message) 72 | => message.Content.IsPlaintext 73 | && message.Content.Text.Equals(" pp", StringComparison.OrdinalIgnoreCase); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Private/Osu/Recommendations/Clear.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Bleatingsheep.NewHydrant.Attributions; 6 | using Bleatingsheep.OsuQqBot.Database.Models; 7 | using Sisters.WudiLib; 8 | using Message = Sisters.WudiLib.SendingMessage; 9 | using MessageContext = Sisters.WudiLib.Posts.Message; 10 | 11 | namespace Bleatingsheep.NewHydrant.Osu.Recommendations 12 | { 13 | #nullable enable 14 | [Component("Clear")] 15 | public class Clear : IMessageCommand 16 | { 17 | private readonly NewbieContext _newbieContext; 18 | 19 | public Clear(NewbieContext newbieContext) 20 | { 21 | _newbieContext = newbieContext; 22 | } 23 | 24 | public async Task ProcessAsync(MessageContext context, HttpApiClient api) 25 | { 26 | _newbieContext.RemoveRange(_newbieContext.Recommendations); 27 | await _newbieContext.SaveChangesAsync().ConfigureAwait(false); 28 | await api.SendMessageAsync(context.Endpoint, "清除完成。"); 29 | } 30 | 31 | public bool ShouldResponse(MessageContext context) 32 | => context.UserId == 962549599 && context.Content.Text == "清除数据"; 33 | } 34 | #nullable restore 35 | } 36 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Private/Osu/Snapshots/SyncSchedule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Bleatingsheep.NewHydrant.Attributions; 6 | using Bleatingsheep.NewHydrant.Core; 7 | using Bleatingsheep.NewHydrant.Data; 8 | using Bleatingsheep.Osu; 9 | using Bleatingsheep.OsuQqBot.Database.Models; 10 | using Microsoft.EntityFrameworkCore; 11 | using Microsoft.Extensions.Logging; 12 | using Sisters.WudiLib; 13 | 14 | namespace Bleatingsheep.NewHydrant.Osu.Snapshots; 15 | 16 | #nullable enable 17 | //[Component("SyncSchedule")] 18 | public class SyncSchedule : Service, IRegularAsync 19 | { 20 | private static readonly SemaphoreSlim s_semaphore = new(1); 21 | private readonly IDbContextFactory _dbContextFactory; 22 | private readonly ILogger _logger; 23 | private readonly IDataProvider _dataProvider; 24 | 25 | public TimeSpan? OnUtc => new TimeSpan(19, 30, 0); 26 | 27 | public TimeSpan? Every => null; 28 | 29 | public SyncSchedule(IDbContextFactory dbContextFactory, ILogger logger, IDataProvider dataProvider) 30 | { 31 | _dbContextFactory = dbContextFactory; 32 | _logger = logger; 33 | _dataProvider = dataProvider; 34 | } 35 | 36 | public async Task RunAsync(HttpApiClient api) 37 | { 38 | if (!s_semaphore.Wait(0)) 39 | { 40 | return; 41 | } 42 | try 43 | { 44 | await using var db1 = _dbContextFactory.CreateDbContext(); 45 | var snapshotted = 46 | await db1.UserSnapshots 47 | .Select(s => new { s.UserId, s.Mode }) 48 | .Distinct() 49 | .ToListAsync() 50 | .ConfigureAwait(false); 51 | var binded = await 52 | (from b in db1.Bindings.AsAsyncEnumerable() 53 | from m in new[] { Mode.Standard, Mode.Taiko, Mode.Catch, Mode.Mania }.ToAsyncEnumerable() 54 | select new { UserId = b.OsuId, Mode = m }) 55 | .ToListAsync().ConfigureAwait(false); 56 | var scheduled = 57 | await db1.UpdateSchedules 58 | .Select(s => new { s.UserId, s.Mode }) 59 | .ToListAsync() 60 | .ConfigureAwait(false); 61 | var toSchedule = snapshotted.Intersect(binded).Except(scheduled).Select(i => new UpdateSchedule 62 | { 63 | UserId = i.UserId, 64 | Mode = i.Mode, 65 | NextUpdate = DateTimeOffset.UtcNow, 66 | }).ToList(); 67 | if (toSchedule.Count > 0) 68 | { 69 | _logger.LogDebug("Adding {toSchedule.Count} items to schedule.", toSchedule.Count); 70 | db1.UpdateSchedules.AddRange(toSchedule); 71 | await db1.SaveChangesAsync().ConfigureAwait(false); 72 | } 73 | } 74 | finally 75 | { 76 | s_semaphore.Release(); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Private/Osu/Yearly/YearlyCachePreparation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Bleatingsheep.NewHydrant.Attributions; 5 | using Bleatingsheep.NewHydrant.Data; 6 | using Bleatingsheep.OsuQqBot.Database.Models; 7 | using Microsoft.EntityFrameworkCore; 8 | using Microsoft.Extensions.Logging; 9 | using Sisters.WudiLib; 10 | using Message = Sisters.WudiLib.SendingMessage; 11 | using MessageContext = Sisters.WudiLib.Posts.Message; 12 | 13 | namespace Bleatingsheep.NewHydrant.Osu.Yearly; 14 | #nullable enable 15 | [Component("YearlyCachePreparation")] 16 | public class YearlyCachePreparation : IMessageCommand 17 | { 18 | private readonly IDbContextFactory _dbContextFactory; 19 | private readonly ILogger _logger; 20 | private readonly IDataProvider _dataProvider; 21 | 22 | public YearlyCachePreparation(IDbContextFactory dbContextFactory, IDataProvider dataProvider, ILogger logger) 23 | { 24 | _dbContextFactory = dbContextFactory; 25 | _dataProvider = dataProvider; 26 | _logger = logger; 27 | } 28 | 29 | private async Task CacheBeatmapInfo() 30 | { 31 | await using var db1 = _dbContextFactory.CreateDbContext(); 32 | // cache beatmap information 33 | var played = await db1.UserPlayRecords.AsNoTracking().Select(r => new { r.Record.BeatmapId, r.Mode }).Distinct().ToListAsync().ConfigureAwait(false); 34 | var cached = await db1.BeatmapInfoCache.AsNoTracking().Select(c => new { c.BeatmapId, c.Mode }).Distinct().AsAsyncEnumerable().ToHashSetAsync().ConfigureAwait(false); 35 | var random = new Random(); 36 | var noCache = played.Except(cached).OrderBy(_ => random.Next()).ToList(); 37 | _logger.LogInformation("Need {noCacheBid.Count} new cache.", noCache.Count); 38 | var success = 0; 39 | var failed = 0; 40 | foreach (var beatmap in noCache) 41 | { 42 | try 43 | { 44 | _ = await _dataProvider.GetBeatmapInfoAsync(beatmap.BeatmapId, beatmap.Mode).ConfigureAwait(false); 45 | success++; 46 | } 47 | catch (Exception e) 48 | { 49 | if (failed == 0) 50 | { 51 | _logger.LogError(e, "error"); 52 | } 53 | // ignore 54 | failed++; 55 | } 56 | } 57 | _logger.LogInformation("Caching complete. success {success}, failed {failed}", success, failed); 58 | } 59 | 60 | public bool ShouldResponse(MessageContext context) 61 | { 62 | return context.UserId == 962549599 63 | && context.Content.TryGetPlainText(out var text) 64 | && text == "准备年度osu缓存"; 65 | } 66 | 67 | public async Task ProcessAsync(MessageContext context, HttpApiClient api) 68 | { 69 | await CacheBeatmapInfo().ConfigureAwait(false); 70 | } 71 | } -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Private/Tests/ChromeRelaunch.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Bleatingsheep.NewHydrant.Attributions; 3 | using Sisters.WudiLib; 4 | using Message = Sisters.WudiLib.SendingMessage; 5 | using MessageContext = Sisters.WudiLib.Posts.Message; 6 | 7 | namespace Bleatingsheep.NewHydrant.Tests 8 | { 9 | [Component("chrome_relaunch")] 10 | class ChromeRelaunch : IMessageCommand 11 | { 12 | public async Task ProcessAsync(MessageContext context, HttpApiClient api) 13 | { 14 | await Chrome.RefreashBrowserAsync().ConfigureAwait(false); 15 | await api.SendMessageAsync(context.Endpoint, "重启完毕。").ConfigureAwait(false); 16 | } 17 | 18 | public bool ShouldResponse(MessageContext context) 19 | { 20 | return context.UserId == 962549599 21 | && context.Content.TryGetPlainText(out string text) 22 | && text == "重启浏览器"; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Private/Tests/ChromeTabCountReport.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Bleatingsheep.NewHydrant.Attributions; 4 | using Sisters.WudiLib; 5 | using Message = Sisters.WudiLib.SendingMessage; 6 | using MessageContext = Sisters.WudiLib.Posts.Message; 7 | 8 | namespace Bleatingsheep.NewHydrant.Tests 9 | { 10 | [Component("chrome_tab_count_report")] 11 | public class ChromeTabCountReport : IMessageCommand 12 | { 13 | public async Task ProcessAsync(MessageContext context, HttpApiClient api) 14 | { 15 | var tabs = await Chrome.GetTabsAsync().ConfigureAwait(false); 16 | await api.SendMessageAsync(context.Endpoint, $"已打开了 {tabs.Length} 个标签页。").ConfigureAwait(false); 17 | } 18 | 19 | public bool ShouldResponse(MessageContext context) 20 | => context.UserId == 962549599 21 | && context.Content.TryGetPlainText(out string text) 22 | && string.Equals("tabs", text, StringComparison.OrdinalIgnoreCase); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Private/Tests/ImageTest.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Net.Http; 3 | using System.Text.RegularExpressions; 4 | using System.Threading.Tasks; 5 | using Bleatingsheep.NewHydrant.Attributions; 6 | using Bleatingsheep.NewHydrant.Core; 7 | using Sisters.WudiLib; 8 | using Message = Sisters.WudiLib.SendingMessage; 9 | using MessageContext = Sisters.WudiLib.Posts.Message; 10 | 11 | namespace Bleatingsheep.NewHydrant.Tests 12 | { 13 | [Component("img")] 14 | public class ImageTest : Service, IMessageCommand 15 | { 16 | private static readonly Regex s_regex = new Regex(@"^image (?.*)$", RegexOptions.Compiled); 17 | 18 | [Parameter("url")] 19 | public string Url { get; set; } 20 | 21 | public async Task ProcessAsync(MessageContext context, HttpApiClient api) 22 | { 23 | Logger.Debug($"开始读取 URL {Url} "); 24 | using (var httpClient = new HttpClient()) 25 | { 26 | var data = await httpClient.GetByteArrayAsync(Url); 27 | Logger.Debug($"取到 {data.Length} 字节数据。"); 28 | var sendResponse = await api.SendMessageAsync(context.Endpoint, Message.ByteArrayImage(data)); 29 | Logger.Debug($"发送结果:消息 ID {sendResponse?.MessageId.ToString(CultureInfo.InvariantCulture) ?? "null"}"); 30 | } 31 | } 32 | 33 | public bool ShouldResponse(MessageContext context) 34 | => context.UserId == 962549599 && RegexCommand(s_regex, context.Content); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Private/Tests/LoadAvg.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | using Bleatingsheep.NewHydrant.Attributions; 6 | using Sisters.WudiLib; 7 | using MessageContext = Sisters.WudiLib.Posts.Message; 8 | 9 | namespace Bleatingsheep.NewHydrant.Tests; 10 | 11 | [Component("loadavg")] 12 | class LoadAvg : IMessageCommand 13 | { 14 | public async Task ProcessAsync(MessageContext context, HttpApiClient api) 15 | { 16 | var load = File.ReadAllText("/proc/loadavg"); 17 | var loadArray = load.Split(); 18 | var messageList = new List(); 19 | messageList.Add($"{loadArray[0]} {loadArray[1]} {loadArray[2]}"); 20 | // messageList.Add(context.MessageId.ToString(CultureInfo.InvariantCulture)); 21 | messageList.Add(context.Time.ToOffset(TimeZoneInfo.FindSystemTimeZoneById("America/Toronto").GetUtcOffset(DateTimeOffset.UtcNow)).ToString("H:mm:ss")); 22 | 23 | var pressureio = File.ReadAllText("/proc/pressure/io"); 24 | var pressureioArray = pressureio.Split(); 25 | messageList.Add($"IO Pressure avg300: some {pressureioArray[3][7..]}, full {pressureioArray[8][7..]}"); 26 | 27 | var pressureMem = File.ReadAllText("/proc/pressure/memory"); 28 | var pressureMemArray = pressureMem.Split(); 29 | if (double.Parse(pressureMemArray[3][7..]) > 0) 30 | { 31 | messageList.Add($"Memory Pressure avg300: some {pressureMemArray[3][7..]}, full {pressureMemArray[8][7..]}"); 32 | } 33 | 34 | var sendrsp = await api.SendMessageAsync(context.Endpoint, string.Join("\r\n", messageList)).ConfigureAwait(false); 35 | await Task.Delay(5000).ConfigureAwait(false); 36 | await api.SendMessageAsync(context.Endpoint, sendrsp is null ? "No sent response data." : $"Response message ID: {sendrsp.MessageId}").ConfigureAwait(false); 37 | } 38 | 39 | public bool ShouldResponse(MessageContext context) 40 | => context.UserId == 962549599 && context.Content.TryGetPlainText(out var text) && "loadavg".Equals(text, StringComparison.OrdinalIgnoreCase); 41 | } 42 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Private/啥玩意儿啊/MemePost/MemePostInformation.cs: -------------------------------------------------------------------------------- 1 | namespace Bleatingsheep.NewHydrant.啥玩意儿啊.MemePost; 2 | #nullable enable 3 | internal class MemePostInformation 4 | { 5 | public required MemePostRepositoryInformation Repository { get; init; } 6 | public required string GitHubToken { get; init; } 7 | public required string Path { get; init; } 8 | public string? HomePage { get; init; } 9 | 10 | internal record class MemePostRepositoryInformation(string Owner, string Name); 11 | } 12 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Private/啥玩意儿啊/ShowLocalTime.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text.Json; 4 | using System.Threading.Tasks; 5 | using Bleatingsheep.NewHydrant.Attributions; 6 | using Bleatingsheep.OsuQqBot.Database.Models; 7 | using Microsoft.EntityFrameworkCore; 8 | using Sisters.WudiLib; 9 | using Sisters.WudiLib.Posts; 10 | using MessageContext = Sisters.WudiLib.Posts.Message; 11 | 12 | namespace Bleatingsheep.NewHydrant.啥玩意儿啊; 13 | #nullable enable 14 | [Component("show_local_time")] 15 | public class ShowLocalTime : IMessageCommand 16 | { 17 | private readonly IDbContextFactory _dbContextFactory; 18 | 19 | public ShowLocalTime(IDbContextFactory dbContextFactory) 20 | { 21 | _dbContextFactory = dbContextFactory; 22 | } 23 | 24 | public async Task ProcessAsync(MessageContext context, HttpApiClient api) 25 | { 26 | var g = (GroupMessage)context; 27 | await using var db = _dbContextFactory.CreateDbContext(); 28 | var field = await db.BotGroupFields.FirstOrDefaultAsync(f => f.GroupId == g.GroupId && f.FieldName == "member_timezones").ConfigureAwait(false); 29 | var tzList = field?.Data?.Deserialize(); 30 | if (tzList?.TimeZones.Count is not > 0) 31 | { 32 | await api.SendGroupMessageAsync(g.GroupId, "还没有设置群友时区呢,发送“添加时区”。").ConfigureAwait(false); 33 | return; 34 | } 35 | var now = DateTime.UtcNow; 36 | var resultList = tzList.TimeZones.Select(tz => 37 | { 38 | var tzi = TimeZoneInfo.FindSystemTimeZoneById(tz.TimeZoneId); 39 | return $"{tz.DisplayName}: {TimeZoneInfo.ConvertTimeFromUtc(now, tzi):d, dddd H:mm}"; 40 | }); 41 | var result = string.Join("\r\n", resultList); 42 | await api.SendGroupMessageAsync(g.GroupId, result).ConfigureAwait(false); 43 | } 44 | 45 | public bool ShouldResponse(MessageContext context) 46 | { 47 | return context is GroupMessage g && g.Content.TryGetPlainText(out var text) && text.Trim() == "时差"; 48 | } 49 | } 50 | #nullable restore -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Public/Bleatingsheep.NewHydrant.Bot.Public.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | Bleatingsheep.NewHydrant 6 | latest 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Public/Mahjong/IMajsoulAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Bleatingsheep.NewHydrant.Mahjong; 5 | 6 | #nullable enable 7 | public interface IMajsoulAnalyzer 8 | { 9 | Task AnalyzeAsync(ReadOnlyMemory logJsonBytes, int targetActor, int[] ptList, double deviationThreshold, string id); 10 | 11 | bool IsIdle { get; } 12 | } 13 | #nullable restore -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Public/Mahjong/LocalAkochanReviewer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Bleatingsheep.NewHydrant.Mahjong; 8 | 9 | #nullable enable 10 | public class LocalAkochanReviewer : IMajsoulAnalyzer, IDisposable 11 | { 12 | private readonly SemaphoreSlim _semaphore = new(1, 1); 13 | private readonly string _workingDirectory; 14 | private readonly string _executablePath; 15 | 16 | public LocalAkochanReviewer(string workingDirectory, string executablePath) 17 | { 18 | _workingDirectory = workingDirectory; 19 | _executablePath = executablePath; 20 | } 21 | 22 | public bool IsIdle => _semaphore.CurrentCount == 1; 23 | 24 | public Task AnalyzeAsync(ReadOnlyMemory logJsonBytes, int targetActor, int[] ptList, double deviationThreshold, string id) 25 | { 26 | if (!_semaphore.Wait(0)) 27 | throw new InvalidOperationException("Already analyzing."); 28 | 29 | return Task.Run(async () => 30 | { 31 | try 32 | { 33 | var processStart = new ProcessStartInfo(_executablePath) 34 | { 35 | FileName = _executablePath, 36 | WorkingDirectory = _workingDirectory, 37 | ArgumentList = { "-a", targetActor.ToString(), "--pt", string.Join(',', ptList), "-n", deviationThreshold.ToString(), "-o", "-" }, 38 | RedirectStandardInput = true, 39 | RedirectStandardOutput = true, 40 | UseShellExecute = false, 41 | CreateNoWindow = true, 42 | Environment = { { "LD_LIBRARY_PATH", Path.Combine(_workingDirectory, "akochan") } }, 43 | StandardOutputEncoding = System.Text.Encoding.UTF8, 44 | StandardInputEncoding = System.Text.Encoding.UTF8, 45 | }; 46 | var process = Process.Start(processStart); 47 | if (process is null) 48 | throw new InvalidOperationException("Failed to start process."); 49 | await using (var stdin = process.StandardInput) 50 | await stdin.BaseStream.WriteAsync(logJsonBytes).ConfigureAwait(false); 51 | 52 | var resultStream = new MemoryStream(); 53 | using (var stdout = process.StandardOutput) 54 | await stdout.BaseStream.CopyToAsync(resultStream).ConfigureAwait(false); 55 | 56 | return resultStream.ToArray(); 57 | } 58 | finally 59 | { 60 | _semaphore.Release(); 61 | } 62 | }); 63 | } 64 | 65 | public void Dispose() 66 | { 67 | _semaphore.Dispose(); 68 | } 69 | } 70 | #nullable restore -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Public/Mahjong/MahjongObjectStorage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | 5 | namespace Bleatingsheep.NewHydrant.Mahjong; 6 | 7 | #nullable enable 8 | public class MahjongObjectStorage 9 | { 10 | private readonly string _basePath; 11 | private readonly Uri _baseUrl; 12 | 13 | public MahjongObjectStorage(string basePath, string baseUrl) 14 | { 15 | _basePath = basePath; 16 | _baseUrl = new Uri(baseUrl); 17 | } 18 | 19 | public async Task PutFileAsync(string id, ReadOnlyMemory bytes, bool overwrite = false) 20 | { 21 | var path = Path.Combine(_basePath, id); 22 | if (File.Exists(path) && !overwrite) 23 | return null; 24 | 25 | await using var fileStream = File.Create(path); 26 | await fileStream.WriteAsync(bytes).ConfigureAwait(false); 27 | return new Uri(_baseUrl, id); 28 | } 29 | 30 | public Task GetUriAsync(string id) 31 | { 32 | var path = Path.Combine(_basePath, id); 33 | if (!File.Exists(path)) 34 | return Task.FromResult(null); 35 | 36 | return Task.FromResult(new Uri(_baseUrl, id)); 37 | } 38 | } 39 | #nullable restore -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Public/Mahjong/MahjongOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Bleatingsheep.NewHydrant.Mahjong; 2 | #nullable enable 3 | internal sealed class MahjongOptions 4 | { 5 | public const string Mahjong = "Mahjong"; 6 | 7 | public required string TensoulBase { get; set; } 8 | } -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Public/Osu/Plus/IPlusApi.cs: -------------------------------------------------------------------------------- 1 | using WebApiClient; 2 | using WebApiClient.Attributes; 3 | 4 | namespace Bleatingsheep.NewHydrant.Osu.Plus 5 | { 6 | [HttpHost("https://syrin.me/pp+/api/")] 7 | public interface IPlusApi : IHttpApi 8 | { 9 | [HttpGet("user/{userId}")] 10 | ITask GetUserAsync(int userId); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Public/Osu/Plus/PlusPlus.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Bleatingsheep.NewHydrant.Attributions; 6 | using Bleatingsheep.NewHydrant.Osu; 7 | using Sisters.WudiLib; 8 | using Sisters.WudiLib.Posts; 9 | using HttpApi = WebApiClient.HttpApi; 10 | using Message = Sisters.WudiLib.SendingMessage; 11 | using MessageContext = Sisters.WudiLib.Posts.Message; 12 | using System.Globalization; 13 | using System.Threading; 14 | using Bleatingsheep.NewHydrant.Data; 15 | using Bleatingsheep.NewHydrant.Core; 16 | 17 | namespace Bleatingsheep.NewHydrant.Osu.Plus 18 | { 19 | [Component("plus_plus")] 20 | class PlusPlus : Service, IMessageCommand 21 | { 22 | private static int s_initialized = 0; 23 | private static readonly object s_initializingObject = new object(); 24 | 25 | public PlusPlus(ILegacyDataProvider dataProvider) 26 | { 27 | DataProvider = dataProvider; 28 | } 29 | 30 | public string Name { get; } 31 | private ILegacyDataProvider DataProvider { get; } 32 | 33 | private static void InitializeIfNecessary() 34 | { 35 | if (s_initialized == 0) 36 | { 37 | lock (s_initializingObject) 38 | { 39 | if (s_initialized == 0) 40 | { 41 | HttpApi.Register(); 42 | s_initialized = 1; 43 | } 44 | } 45 | } 46 | } 47 | 48 | public async Task ProcessAsync(MessageContext context, HttpApiClient api) 49 | { 50 | InitializeIfNecessary(); 51 | var id = await DataProvider.EnsureGetBindingIdAsync(context.UserId); 52 | var myWebApi = HttpApi.Resolve(); 53 | var user = await myWebApi.GetUserAsync(id); 54 | var userPlus = user?.Data; 55 | var responseMessage = $@"{userPlus.UserName} 的 PP+ 数据 56 | Performance: {userPlus.Performance} 57 | Aim (Jump): {userPlus.AimJump} 58 | Aim (Flow): {userPlus.AimFlow} 59 | Precision: {userPlus.Precision} 60 | Speed: {userPlus.Speed} 61 | Stamina: {userPlus.Stamina} 62 | Accuracy: {userPlus.Accuracy}"; 63 | await api.SendMessageAsync(context.Endpoint, responseMessage); 64 | } 65 | 66 | public bool ShouldResponse(MessageContext context) 67 | { 68 | if (context is GroupMessage g && g.GroupId == 231094840) 69 | return false; // ignored in newbie group. 70 | return context.Content.TryGetPlainText(out var text) && text == "++"; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Public/Osu/QueryMotherShip.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Bleatingsheep.NewHydrant.Attributions; 4 | using Bleatingsheep.OsuQqBot.Database.Models; 5 | using Microsoft.EntityFrameworkCore; 6 | using Sisters.WudiLib; 7 | using Message = Sisters.WudiLib.SendingMessage; 8 | using MessageContext = Sisters.WudiLib.Posts.Message; 9 | 10 | namespace Bleatingsheep.NewHydrant.Osu 11 | { 12 | [Component("query_mother_ship")] 13 | public class QueryMotherShip : IMessageCommand 14 | { 15 | private readonly Lazy _newbieContext; 16 | 17 | public QueryMotherShip(Lazy newbieContext) 18 | { 19 | _newbieContext = newbieContext; 20 | } 21 | 22 | public async Task ProcessAsync(MessageContext context, HttpApiClient api) 23 | { 24 | using var db = _newbieContext.Value; 25 | var bindingInfo = await db.Bindings.FirstOrDefaultAsync(b => b.UserId == context.UserId).ConfigureAwait(false); 26 | if (bindingInfo is null) 27 | return; 28 | var url = $"https://www.mothership.top/api/v1/stat/{bindingInfo.OsuId}"; 29 | await api.SendMessageAsync(context.Endpoint, Message.NetImage(url, true)).ConfigureAwait(false); 30 | } 31 | 32 | public bool ShouldResponse(MessageContext context) 33 | => context.Content.TryGetPlainText(out string text) 34 | && text is "妈船?" or "妈船?"; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Public/Osu/Snapshots/BotCommandTrigger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Bleatingsheep.NewHydrant.Attributions; 6 | using Bleatingsheep.NewHydrant.Data; 7 | using Bleatingsheep.Osu; 8 | using Sisters.WudiLib; 9 | using Message = Sisters.WudiLib.SendingMessage; 10 | using MessageContext = Sisters.WudiLib.Posts.Message; 11 | 12 | namespace Bleatingsheep.NewHydrant.Osu.Snapshots 13 | { 14 | [Component("speaking_trigger_for_snapshot")] 15 | public class BotCommandTrigger : IMessageMonitor 16 | { 17 | private static readonly IReadOnlyCollection s_baicaiCommands = new List 18 | { 19 | "bpme", 20 | "recent", 21 | "pr", 22 | "statme", 23 | }.AsReadOnly(); 24 | private readonly DataMaintainer _dataMaintainer; 25 | private readonly IDataProvider _dataProvider; 26 | 27 | public BotCommandTrigger(DataMaintainer dataMaintainer, IDataProvider dataProvider) 28 | { 29 | _dataMaintainer = dataMaintainer; 30 | _dataProvider = dataProvider; 31 | } 32 | 33 | public async Task OnMessageAsync(MessageContext message, HttpApiClient api) 34 | { 35 | if (!s_baicaiCommands.Any(c => message.Content.Text.Contains(c))) 36 | { 37 | return; 38 | } 39 | var uid = await _dataProvider.GetOsuIdAsync(message.UserId).ConfigureAwait(false); 40 | Mode? mode = null; 41 | // TODO: Use binding from mothership database first. 42 | if (uid != null) 43 | { 44 | if (mode != null) 45 | { 46 | await _dataMaintainer.UpdateAsync(uid.Value, mode.Value).ConfigureAwait(false); 47 | } 48 | else 49 | { 50 | foreach (Mode m in Enum.GetValues(typeof(Mode))) 51 | { 52 | await _dataMaintainer.UpdateAsync(uid.Value, m).ConfigureAwait(false); 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Public/Osu/Snapshots/SpeakingTrigger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Bleatingsheep.NewHydrant.Attributions; 4 | using Bleatingsheep.NewHydrant.Data; 5 | using Bleatingsheep.Osu; 6 | using Microsoft.Extensions.Caching.Memory; 7 | using Sisters.WudiLib; 8 | using Message = Sisters.WudiLib.SendingMessage; 9 | using MessageContext = Sisters.WudiLib.Posts.Message; 10 | 11 | namespace Bleatingsheep.NewHydrant.Osu.Snapshots 12 | { 13 | //[Component("speaking_trigger_for_snapshot")] 14 | public class SpeakingTrigger : IMessageMonitor 15 | { 16 | private static readonly MemoryCache s_cache = new MemoryCache(new MemoryCacheOptions()); 17 | private readonly DataMaintainer _dataMaintainer; 18 | private readonly IDataProvider _dataProvider; 19 | 20 | public SpeakingTrigger(DataMaintainer dataMaintainer, IDataProvider dataProvider) 21 | { 22 | _dataMaintainer = dataMaintainer; 23 | _dataProvider = dataProvider; 24 | } 25 | 26 | public async Task OnMessageAsync(MessageContext message, HttpApiClient api) 27 | { 28 | if (s_cache.TryGetValue(message.UserId, out _)) 29 | { 30 | return; 31 | } 32 | s_cache.Set(message.UserId, DateTimeOffset.UtcNow, TimeSpan.FromHours(1)); 33 | var uid = await _dataProvider.GetOsuIdAsync(message.UserId).ConfigureAwait(false); 34 | if (uid is null) 35 | return; 36 | var tasks = new[] { 37 | _dataMaintainer.UpdateAsync(uid.Value, Mode.Standard), 38 | _dataMaintainer.UpdateAsync(uid.Value, Mode.Taiko), 39 | _dataMaintainer.UpdateAsync(uid.Value, Mode.Catch), 40 | _dataMaintainer.UpdateAsync(uid.Value, Mode.Mania), 41 | }; 42 | await Task.WhenAll(tasks).ConfigureAwait(false); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Public/Osu/Snapshots/UserParameter.cs: -------------------------------------------------------------------------------- 1 | using Bleatingsheep.Osu; 2 | 3 | namespace Bleatingsheep.NewHydrant.Osu.Snapshots 4 | { 5 | internal struct UserParameter 6 | { 7 | public UserParameter(int userId, Mode mode) 8 | { 9 | UserId = userId; 10 | Mode = mode; 11 | } 12 | 13 | public int UserId { get; set; } 14 | public Mode Mode { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Public/Utilities/DateUtility.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Bleatingsheep.NewHydrant.Utilities 4 | { 5 | public static class DateUtility 6 | { 7 | public static TimeSpan GetError(DateTimeOffset wanted, DateTimeOffset actual) 8 | { 9 | var error = wanted - actual; 10 | if (error < TimeSpan.Zero) 11 | error = -error; 12 | return error; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Public/Utilities/IncrementUtility.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Bleatingsheep.NewHydrant.Utilities 4 | { 5 | public static class IncrementUtility 6 | { 7 | public static string FormatIncrement(double? increment, string format = "####.##") 8 | => FormatIncrement(increment, $"+{format};;+", $"-{format};;-", string.Empty); 9 | 10 | public static string FormatIncrement(double? increment, char incrementPrefix, char decrementPrefix, string format = "####.##") 11 | => FormatIncrement(increment, $"{incrementPrefix}{format};;{incrementPrefix}", $"{decrementPrefix}{format};;{decrementPrefix}", string.Empty); 12 | 13 | private static string FormatIncrement(double? increment, string incrementFormat, string decrementFormat, string invarientDisplay = "") 14 | { 15 | var v = (increment ?? 0) switch 16 | { 17 | > 0 => increment?.ToString(incrementFormat), 18 | < 0 => (-increment)?.ToString(decrementFormat), 19 | 0 => invarientDisplay, 20 | double.NaN => throw new ArgumentException("Increment must not be double.NaN.", nameof(increment)), 21 | }; 22 | if (!string.IsNullOrEmpty(v)) 23 | { 24 | v = $" ({v})"; 25 | } 26 | return v; 27 | } 28 | 29 | public static string FormatIncrementPercentage(double? increment) 30 | => FormatIncrement(increment, "#.##%"); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Public/啥玩意儿啊/Exchange/BocRateClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Bleatingsheep.NewHydrant.啥玩意儿啊.Exchange 9 | { 10 | #nullable enable 11 | public static class BocRateClient 12 | { 13 | public static async Task GetExchangeSellingRateAsync(string currencyName) 14 | { 15 | using var httpClient = new HttpClient(); 16 | string html = await httpClient.GetStringAsync("https://www.boc.cn/sourcedb/whpj/sjmfx_1621.html").ConfigureAwait(false); 17 | var doc = new HtmlAgilityPack.HtmlDocument(); 18 | doc.LoadHtml(html); 19 | var nodes = doc.DocumentNode.SelectNodes("/html/body/article/div/table/tbody/tr"); 20 | var currency = nodes.Select(n => new 21 | { 22 | Name = n.SelectSingleNode("td[1]").InnerText, 23 | ExchangeBuy = n.SelectSingleNode("td[2]").InnerText, 24 | CashBuy = n.SelectSingleNode("td[3]").InnerText, 25 | ExchangeSell = n.SelectSingleNode("td[4]").InnerText, 26 | CashSell = n.SelectSingleNode("td[5]").InnerText, 27 | }).FirstOrDefault(i => currencyName.Equals(i.Name, StringComparison.OrdinalIgnoreCase)); 28 | return currency is null 29 | ? null 30 | : decimal.TryParse((string)currency.ExchangeSell, out var rate) 31 | ? rate / 100 32 | : null; 33 | } 34 | } 35 | #nullable restore 36 | } 37 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Public/啥玩意儿啊/Exchange/CibRate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | 4 | namespace Bleatingsheep.NewHydrant.啥玩意儿啊.Exchange 5 | { 6 | 7 | public partial class CibRate 8 | { 9 | [JsonProperty("page")] 10 | public int Page { get; set; } 11 | 12 | [JsonProperty("records")] 13 | public int Records { get; set; } 14 | 15 | [JsonProperty("sidx")] 16 | public string Sidx { get; set; } 17 | 18 | [JsonProperty("sord")] 19 | public string Sord { get; set; } 20 | 21 | [JsonProperty("total")] 22 | public int Total { get; set; } 23 | 24 | [JsonProperty("rows")] 25 | public CibRateData[] Rows { get; set; } 26 | 27 | public decimal? this[string Currency] 28 | { 29 | get 30 | { 31 | var data = Array.Find(Rows, d => string.Equals(Currency, d.EnglishName, StringComparison.OrdinalIgnoreCase)); 32 | return data?.BuyPrice / data?.Unit; 33 | } 34 | } 35 | } 36 | 37 | public class CibRateData 38 | { 39 | [JsonProperty("cell")] 40 | public string[] Cell { get; set; } 41 | 42 | [JsonProperty("id")] 43 | public long Id { get; set; } 44 | 45 | public decimal? Unit => Cell?.Length == 7 && decimal.TryParse(Cell[2], out var result) ? result : default; 46 | 47 | public decimal? BuyPrice => Cell?.Length == 7 && decimal.TryParse(Cell[4], out var result) ? result : default; 48 | 49 | public string EnglishName => Cell?.Length == 7 ? Cell[1] : string.Empty; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Public/啥玩意儿啊/Exchange/CmbcRate.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace Bleatingsheep.NewHydrant.啥玩意儿啊.Exchange 4 | { 5 | public class CmbcRate 6 | { 7 | [JsonProperty("retCode")] 8 | public string RetCode { get; set; } 9 | 10 | [JsonProperty("msg")] 11 | public string Msg { get; set; } 12 | 13 | [JsonProperty("data")] 14 | public CmbcRateData[] Data { get; set; } 15 | } 16 | 17 | public class CmbcRateData 18 | { 19 | [JsonProperty("price")] 20 | public decimal Price { get; set; } 21 | 22 | [JsonProperty("wapUrl")] 23 | public string WapUrl { get; set; } 24 | 25 | [JsonProperty("remark")] 26 | public string Remark { get; set; } 27 | 28 | [JsonProperty("name")] 29 | public string Name { get; set; } 30 | 31 | [JsonProperty("value")] 32 | public int Value { get; set; } 33 | 34 | [JsonProperty("pcUrl")] 35 | public string PcUrl { get; set; } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Public/啥玩意儿啊/Exchange/ExchangeResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Converters; 5 | 6 | namespace Bleatingsheep.NewHydrant.啥玩意儿啊.Exchange 7 | { 8 | public class ExchangeResponse 9 | { 10 | [JsonProperty("base")] 11 | public string Base { get; set; } 12 | 13 | [JsonProperty("date")] 14 | public DateTime Date { get; set; } 15 | 16 | [JsonProperty("time_last_updated")] 17 | [JsonConverter(typeof(UnixDateTimeConverter))] 18 | public DateTimeOffset TimeLastUpdated { get; set; } 19 | 20 | [JsonProperty("rates")] 21 | public Dictionary Rates { get; set; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Public/啥玩意儿啊/Exchange/ICibRate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | using WebApiClient; 5 | using WebApiClient.Attributes; 6 | using WebApiClient.DataAnnotations; 7 | 8 | namespace Bleatingsheep.NewHydrant.啥玩意儿啊.Exchange 9 | { 10 | internal interface ICibRate : IHttpApi 11 | { 12 | [HttpGet("https://personalbank.cib.com.cn/pers/main/pubinfo/ifxQuotationQuery.do")] 13 | [Cache(20 * 60_000)] 14 | Task Prepare(); 15 | 16 | [HttpGet("https://personalbank.cib.com.cn/pers/main/pubinfo/ifxQuotationQuery!list.do?_search=false&dataSet.rows=80&dataSet.page=1&dataSet.sidx=&dataSet.sord=asc")] 17 | [JsonReturn] 18 | [Cache(2 * 20 * 60_000)] 19 | Task GetRates([AliasAs("dataSet.nd")][PathQuery] long timestamp); 20 | } 21 | 22 | static class CibRateExtensions 23 | { 24 | public async static Task GetRates(this ICibRate cibRate) 25 | { 26 | await cibRate.Prepare().ConfigureAwait(false); 27 | return await cibRate.GetRates(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()).ConfigureAwait(false); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Public/啥玩意儿啊/Exchange/IExchangeRate.cs: -------------------------------------------------------------------------------- 1 | using WebApiClient; 2 | using WebApiClient.Attributes; 3 | 4 | namespace Bleatingsheep.NewHydrant.啥玩意儿啊.Exchange 5 | { 6 | [HttpHost("https://api.exchangerate-api.com/v4/")] 7 | public interface IExchangeRate : IHttpApi 8 | { 9 | [HttpGet("latest/{base}")] 10 | [Cache(20 * 60_000)] 11 | ITask GetExchangeRates(string @base); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Public/啥玩意儿啊/IP.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Text.RegularExpressions; 3 | using System.Threading.Tasks; 4 | using Bleatingsheep.NewHydrant.Attributions; 5 | using Bleatingsheep.NewHydrant.Core; 6 | using Sisters.WudiLib; 7 | using Message = Sisters.WudiLib.SendingMessage; 8 | using MessageContext = Sisters.WudiLib.Posts.Message; 9 | 10 | namespace Bleatingsheep.NewHydrant.啥玩意儿啊 11 | { 12 | [Component("ip")] 13 | public class IP : Service, IMessageCommand 14 | { 15 | private static readonly Regex s_regex = new Regex(@"^\s*ip\s+(?\S+)\s*$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); 16 | 17 | public async Task ProcessAsync(MessageContext context, HttpApiClient api) 18 | { 19 | var result = await IPLocation.IPLocator.Default.GetLocationAsync(_address).ConfigureAwait(false); 20 | await api.SendMessageAsync(context.Endpoint, result switch 21 | { 22 | (false, _) => "查询失败。", 23 | (true, null) => "未找到结果。", 24 | (true, var l) => l.ToString(), 25 | }).ConfigureAwait(false); 26 | } 27 | 28 | private IPAddress _address; 29 | 30 | [Parameter("ip")] 31 | public string IPString { get; set; } 32 | 33 | public bool ShouldResponse(MessageContext context) 34 | { 35 | return context.Content.TryGetPlainText(out string text) && RegexCommand(s_regex, text) 36 | && IPAddress.TryParse(IPString, out _address); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Public/啥玩意儿啊/Moebooru/Api.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Net.Http; 6 | using System.Threading.Tasks; 7 | 8 | namespace Bleatingsheep.NewHydrant.啥玩意儿啊.Moebooru 9 | { 10 | class Api 11 | { 12 | private const string SafeRating = "s"; 13 | private const string QuestionableRating = "q"; 14 | private const string ExplicitRating = "e"; 15 | 16 | private const string PopularPath = "/post/popular_recent.json"; 17 | private string Popular => domain + PopularPath; 18 | 19 | readonly string domain; 20 | 21 | public Api(string domain) 22 | { 23 | this.domain = domain.TrimEnd('/'); 24 | } 25 | 26 | public bool EnableR18 { get; set; } = false; 27 | 28 | private static async Task GetTAsync(string url) 29 | { 30 | try 31 | { 32 | using (var client = new HttpClient()) 33 | { 34 | var s = await client.GetStringAsync(url); 35 | T result = JsonConvert.DeserializeObject(s); 36 | return result; 37 | } 38 | } 39 | catch (Exception) { return default(T); } 40 | } 41 | 42 | public async Task> PopularRecentAsync() 43 | { 44 | IEnumerable result = await GetTAsync(Popular); 45 | 46 | if (!EnableR18) result = result?.Where(p => p.rating == SafeRating); 47 | return result; 48 | } 49 | 50 | internal async Task<(IEnumerable result, string info)> PopularRecentDebugAsync() 51 | { 52 | IEnumerable result = await GetTAsync(Popular); 53 | var groups = result.GroupBy(p => p.rating); 54 | var infos = new LinkedList(); 55 | foreach (var group in groups) 56 | { 57 | infos.AddLast($"{group.Key}: {group.Count()}"); 58 | } 59 | string info = string.Join("\r\n", infos); 60 | if (!EnableR18) result = result.Where(p => p.rating == SafeRating); 61 | return (result, info); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Public/啥玩意儿啊/Moebooru/Konachan.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Bleatingsheep.NewHydrant.Attributions; 6 | using Bleatingsheep.NewHydrant.Core; 7 | using Sisters.WudiLib; 8 | using Message = Sisters.WudiLib.Posts.Message; 9 | 10 | namespace Bleatingsheep.NewHydrant.啥玩意儿啊.Moebooru 11 | { 12 | //[Function("konachan")] 13 | class Konachan : IMessageCommand 14 | { 15 | internal static readonly ISet DissTags = new HashSet 16 | { 17 | "bikini", 18 | "panties", 19 | "nude", 20 | "bikini_top", 21 | "breast_hold", 22 | "breasts", 23 | "cleavage", 24 | "all_male", 25 | "see_through", 26 | "ass", 27 | "underboob", 28 | "swimsuit", 29 | "barefoot", 30 | "pantyhose", 31 | "garter_belt", 32 | "bodysuit", 33 | "onsen", 34 | "nopan", 35 | "white", 36 | "sideboob", 37 | }; 38 | 39 | public async Task ProcessAsync(Message message, HttpApiClient api) 40 | { 41 | var k = new Api("https://konachan.net"); 42 | var recent = await k.PopularRecentAsync(); 43 | if (recent == null) 44 | return; 45 | 46 | recent = recent.Where(p => !p.tags.Split().Intersect(DissTags).Any()).Take(1); 47 | foreach (var post in recent) 48 | { 49 | await api.SendMessageAsync(message.Endpoint, SendingMessage.NetImage(post.JpegUrl)); 50 | } 51 | } 52 | 53 | public bool ShouldResponse(Message message) 54 | { 55 | return message.Content.Text.StartsWith("健康konachan", StringComparison.InvariantCultureIgnoreCase); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Public/啥玩意儿啊/Moebooru/Post.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | #pragma warning disable IDE1006 // 命名样式 4 | namespace Bleatingsheep.NewHydrant.啥玩意儿啊.Moebooru 5 | { 6 | 7 | public class Post 8 | { 9 | private const string ProtocolPrefix = "https:"; 10 | 11 | public int id { get; set; } 12 | public string tags { get; set; } 13 | public int created_at { get; set; } 14 | public int creator_id { get; set; } 15 | public string author { get; set; } 16 | public int change { get; set; } 17 | public string source { get; set; } 18 | public int score { get; set; } 19 | public string md5 { get; set; } 20 | public int file_size { get; set; } 21 | [JsonProperty] 22 | private string file_url { get; set; } 23 | [JsonIgnore] 24 | public string FileUrl => UrlFormat(file_url); 25 | public bool is_shown_in_index { get; set; } 26 | [JsonProperty] 27 | private string preview_url { get; set; } 28 | [JsonIgnore] 29 | public string PreviewUrl => UrlFormat(preview_url); 30 | public int preview_width { get; set; } 31 | public int preview_height { get; set; } 32 | public int actual_preview_width { get; set; } 33 | public int actual_preview_height { get; set; } 34 | [JsonProperty] 35 | private string sample_url { get; set; } 36 | [JsonIgnore] 37 | public string SampleUrl => UrlFormat(sample_url); 38 | public int sample_width { get; set; } 39 | public int sample_height { get; set; } 40 | public int sample_file_size { get; set; } 41 | [JsonProperty] 42 | private string jpeg_url { get; set; } 43 | [JsonIgnore] 44 | public string JpegUrl => UrlFormat(jpeg_url); 45 | public int jpeg_width { get; set; } 46 | public int jpeg_height { get; set; } 47 | public int jpeg_file_size { get; set; } 48 | public string rating { get; set; } 49 | public bool has_children { get; set; } 50 | public object parent_id { get; set; } 51 | public string status { get; set; } 52 | public int width { get; set; } 53 | public int height { get; set; } 54 | public bool is_held { get; set; } 55 | public string frames_pending_string { get; set; } 56 | public object[] frames_pending { get; set; } 57 | public string frames_string { get; set; } 58 | public object[] frames { get; set; } 59 | public object flag_detail { get; set; } 60 | 61 | private static string UrlFormat(string ori) 62 | { 63 | if (ori.StartsWith("//")) return ProtocolPrefix + ori; 64 | return ori; 65 | } 66 | } 67 | } 68 | #pragma warning restore IDE1006 // 命名样式 -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Public/啥玩意儿啊/Pixiv.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.ServiceModel.Syndication; 3 | using System.Text.RegularExpressions; 4 | using System.Threading.Tasks; 5 | using System.Xml; 6 | using Bleatingsheep.NewHydrant.Attributions; 7 | using Bleatingsheep.NewHydrant.Extentions; 8 | using HtmlAgilityPack; 9 | using Sisters.WudiLib; 10 | using Message = Sisters.WudiLib.SendingMessage; 11 | using MessageContext = Sisters.WudiLib.Posts.Message; 12 | 13 | namespace Bleatingsheep.NewHydrant.啥玩意儿啊 14 | { 15 | [Component("pixiv")] 16 | public class Pixiv : IMessageCommand 17 | { 18 | public async Task ProcessAsync(MessageContext context, HttpApiClient api) 19 | { 20 | string url = "https://rss.bleatingsheep.org/pixiv/ranking/day"; 21 | var xmlReader = XmlReader.Create(url); 22 | var feed = SyndicationFeed.Load(xmlReader); 23 | var tuple = feed.Items.Select(i => 24 | { 25 | var doc = new HtmlDocument(); 26 | doc.LoadHtml(i.Summary.Text); 27 | return (item: i, nodes: doc.DocumentNode.SelectNodes("//p/img")); 28 | }).Randomize().FirstOrDefault(t => t.nodes.Count == 1); 29 | var (item, imgNode) = (tuple.item, tuple.nodes?.First()); 30 | if (imgNode != null) 31 | { 32 | var imgUrl = imgNode.Attributes["src"].Value; 33 | // Chagne URL host name to xfs-proxy-pixiv.b11p.com whatever the original host is 34 | // imgUrl = Regex.Replace(imgUrl, @"^https?://.+?/", "https://xfs-proxy-pixiv.b11p.com/"); 35 | if (await api.SendMessageAsync( 36 | endpoint: context.Endpoint, 37 | message: new Message(item.Title.Text + "\r\n") 38 | + Message.NetImage(imgUrl) 39 | + new Message("\r\n" + item.Links.FirstOrDefault().Uri) 40 | ) == null) 41 | { 42 | await api.SendMessageAsync(context.Endpoint, "图片发送失败。"); 43 | } 44 | } 45 | else 46 | { 47 | await api.SendMessageAsync(context.Endpoint, "没有符合要求的图片。"); 48 | } 49 | } 50 | 51 | public bool ShouldResponse(MessageContext context) 52 | => context.Content.TryGetPlainText(out string text) && text.Trim() == "ピクシブ"; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Public/啥玩意儿啊/加拿大新冠数据.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Bleatingsheep.NewHydrant.Attributions; 4 | using PuppeteerSharp; 5 | using Sisters.WudiLib; 6 | using Message = Sisters.WudiLib.SendingMessage; 7 | using MessageContext = Sisters.WudiLib.Posts.Message; 8 | 9 | namespace Bleatingsheep.NewHydrant.啥玩意儿啊; 10 | 11 | [Component("canada_covid_19")] 12 | internal class 加拿大新冠数据 : IMessageCommand 13 | { 14 | public async Task ProcessAsync(MessageContext context, HttpApiClient api) 15 | { 16 | using var page = await Chrome.OpenNewPageAsync().ConfigureAwait(false); 17 | await page.SetViewportAsync(new ViewPortOptions 18 | { 19 | DeviceScaleFactor = 3, 20 | Width = 360, 21 | Height = 8000, 22 | }).ConfigureAwait(false); 23 | await page.GoToAsync("https://en.wikipedia.org/wiki/Template:COVID-19_pandemic_data/Canada_medical_cases_by_province").ConfigureAwait(false); 24 | var element = await page.QuerySelectorAsync("#mw-content-text > div.mw-parser-output > table").ConfigureAwait(false); 25 | await page.SetViewportAsync(new ViewPortOptions 26 | { 27 | DeviceScaleFactor = 2, 28 | Width = 1024, 29 | Height = 4000, 30 | }).ConfigureAwait(false); 31 | var data2 = await element.ScreenshotDataAsync(new ElementScreenshotOptions 32 | { 33 | Type = ScreenshotType.Jpeg, 34 | Quality = 100, 35 | }).ConfigureAwait(false); 36 | await api.SendMessageAsync(context.Endpoint, Message.ByteArrayImage(data2)).ConfigureAwait(false); 37 | } 38 | 39 | public bool ShouldResponse(MessageContext context) 40 | => context.Content.TryGetPlainText(out var text) && "加拿大完了吗".Equals(text, StringComparison.Ordinal); 41 | } 42 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Public/啥玩意儿啊/帮助.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Bleatingsheep.NewHydrant.Attributions; 6 | using Sisters.WudiLib; 7 | using Message = Sisters.WudiLib.SendingMessage; 8 | using MessageContext = Sisters.WudiLib.Posts.Message; 9 | 10 | namespace Bleatingsheep.NewHydrant.啥玩意儿啊 11 | { 12 | #nullable enable 13 | [Component("帮助")] 14 | public class 帮助 : IMessageCommand 15 | { 16 | public async Task ProcessAsync(MessageContext context, HttpApiClient api) 17 | { 18 | await api.SendMessageAsync( 19 | context.Endpoint, 20 | "请去 https://help.b11p.com/" + 21 | " 查看 osu! 相关帮助。页面右侧可以查看其他相关帮助。" 22 | ).ConfigureAwait(false); 23 | } 24 | 25 | public bool ShouldResponse(MessageContext context) 26 | => context.Content.TryGetPlainText(out var text) 27 | && text == "帮助"; 28 | } 29 | #nullable restore 30 | } 31 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Public/啥玩意儿啊/日本新冠数据.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Bleatingsheep.NewHydrant.Attributions; 4 | using PuppeteerSharp; 5 | using Sisters.WudiLib; 6 | using Message = Sisters.WudiLib.SendingMessage; 7 | using MessageContext = Sisters.WudiLib.Posts.Message; 8 | 9 | namespace Bleatingsheep.NewHydrant.啥玩意儿啊 10 | { 11 | [Component("japan_covid_19")] 12 | internal class 日本新冠数据 : IMessageCommand 13 | { 14 | public async Task ProcessAsync(MessageContext context, HttpApiClient api) 15 | { 16 | using var page = await Chrome.OpenNewPageAsync().ConfigureAwait(false); 17 | await page.SetViewportAsync(new ViewPortOptions 18 | { 19 | DeviceScaleFactor = 3, 20 | Width = 360, 21 | Height = 8000, 22 | }).ConfigureAwait(false); 23 | await page.GoToAsync("https://toyokeizai.net/sp/visual/tko/covid19/index.html").ConfigureAwait(false); 24 | await page.WaitForSelectorAsync("#main-block > div:nth-child(2) > div:nth-child(1) > div > div.charts-wrapper > div.main-chart-wrapper > div > canvas").ConfigureAwait(false); 25 | var element = await page.QuerySelectorAsync("#main-block > div:nth-child(2)").ConfigureAwait(false); 26 | await page.SetViewportAsync(new ViewPortOptions 27 | { 28 | DeviceScaleFactor = 2, 29 | Width = 1024, 30 | Height = 4000, 31 | }).ConfigureAwait(false); 32 | var data2 = await element.ScreenshotDataAsync(new ElementScreenshotOptions 33 | { 34 | Type = ScreenshotType.Jpeg, 35 | Quality = 100, 36 | }).ConfigureAwait(false); 37 | await api.SendMessageAsync(context.Endpoint, Message.ByteArrayImage(data2)).ConfigureAwait(false); 38 | } 39 | 40 | public bool ShouldResponse(MessageContext context) 41 | => context.Content.TryGetPlainText(out var text) && "日本完了吗".Equals(text, StringComparison.Ordinal); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Public/啥玩意儿啊/标普500期货.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Bleatingsheep.NewHydrant.Attributions; 3 | using PuppeteerSharp; 4 | using Sisters.WudiLib; 5 | using Message = Sisters.WudiLib.SendingMessage; 6 | using MessageContext = Sisters.WudiLib.Posts.Message; 7 | 8 | namespace Bleatingsheep.NewHydrant.啥玩意儿啊 9 | { 10 | [Component("标普500期货")] 11 | internal class 标普500期货 : IMessageCommand 12 | { 13 | public async Task ProcessAsync(MessageContext context, HttpApiClient api) 14 | { 15 | using var page = await Chrome.OpenNewPageAsync().ConfigureAwait(false); 16 | await page.SetViewportAsync(new ViewPortOptions 17 | { 18 | DeviceScaleFactor = 3.5, 19 | Width = 800, 20 | Height = 1000, 21 | }).ConfigureAwait(false); 22 | await page.GoToAsync("https://www.investing.com/indices/us-spx-500-futures").ConfigureAwait(false); 23 | const string selector = "#quotes_summary_current_data"; 24 | var element = await page.QuerySelectorAsync(selector).ConfigureAwait(false); 25 | var data = await element.ScreenshotDataAsync(new ElementScreenshotOptions 26 | { 27 | Type = ScreenshotType.Png, 28 | }).ConfigureAwait(false); 29 | bool inLoop = true; 30 | int retry = 3; 31 | do 32 | { 33 | var mesResponse = await api.SendMessageAsync(context.Endpoint, Message.ByteArrayImage(data)).ConfigureAwait(false); 34 | inLoop = mesResponse == null; 35 | } while (inLoop && --retry > 0); 36 | } 37 | 38 | public bool ShouldResponse(MessageContext context) 39 | => context.Content.TryGetPlainText(out string text) && text == "标普500"; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Public/啥玩意儿啊/知乎日报.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.ServiceModel.Syndication; 3 | using System.Threading.Tasks; 4 | using System.Xml; 5 | using Bleatingsheep.NewHydrant.Attributions; 6 | using Bleatingsheep.NewHydrant.Core; 7 | using HtmlAgilityPack; 8 | using PuppeteerSharp; 9 | using Sisters.WudiLib; 10 | using Message = Sisters.WudiLib.SendingMessage; 11 | using MessageContext = Sisters.WudiLib.Posts.Message; 12 | 13 | namespace Bleatingsheep.NewHydrant.啥玩意儿啊 14 | { 15 | //[Function("zhihu_daily")] 16 | public class 知乎日报 : Service, IMessageCommand 17 | { 18 | public async Task ProcessAsync(MessageContext context, HttpApiClient api) 19 | { 20 | string url = "https://rss.bleatingsheep.org/zhihu/daily"; 21 | var xmlReader = XmlReader.Create(url); 22 | var feed = SyndicationFeed.Load(xmlReader); 23 | xmlReader.Close(); 24 | var item = feed.Items.LastOrDefault(); 25 | if (item != default) 26 | { 27 | bool modified = false; 28 | var doc = new HtmlDocument(); 29 | var htmlText = item.Summary.Text; 30 | doc.LoadHtml(htmlText); 31 | foreach (var imgNode in doc.DocumentNode.SelectNodes("//div/div/div/div/div/p/img")?.Where(n => n.GetClasses().FirstOrDefault() == "content-image") ?? Enumerable.Empty()) 32 | { 33 | imgNode.SetAttributeValue("width", "100%"); 34 | modified = true; 35 | } 36 | if (modified) 37 | { 38 | htmlText = doc.DocumentNode.InnerHtml; 39 | } 40 | 41 | using (var page = await Chrome.OpenNewPageAsync()) 42 | { 43 | await page.SetContentAsync(htmlText); 44 | await page.SetViewportAsync(new ViewPortOptions 45 | { 46 | DeviceScaleFactor = 1.5, 47 | Width = 360, 48 | Height = 640, 49 | }); 50 | var data = await page.ScreenshotDataAsync(new ScreenshotOptions 51 | { 52 | FullPage = true, 53 | }); 54 | await Task.Delay(100); 55 | var sendCode = await api.SendMessageAsync(context.Endpoint, Message.ByteArrayImage(data)); 56 | if (sendCode == null) 57 | { 58 | Logger.Info("知乎日报发送失败。"); 59 | } 60 | } 61 | } 62 | } 63 | 64 | public bool ShouldResponse(MessageContext context) 65 | => context.Content.TryGetPlainText(out string text) && text == "知乎日报"; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Public/啥玩意儿啊/美国新冠数据.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Bleatingsheep.NewHydrant.Attributions; 4 | using PuppeteerSharp; 5 | using Sisters.WudiLib; 6 | using Message = Sisters.WudiLib.SendingMessage; 7 | using MessageContext = Sisters.WudiLib.Posts.Message; 8 | 9 | namespace Bleatingsheep.NewHydrant.啥玩意儿啊; 10 | 11 | [Component("us_covid_19")] 12 | internal class 美国新冠数据 : IMessageCommand 13 | { 14 | public async Task ProcessAsync(MessageContext context, HttpApiClient api) 15 | { 16 | using var page = await Chrome.OpenNewPageAsync().ConfigureAwait(false); 17 | await page.SetViewportAsync(new ViewPortOptions 18 | { 19 | DeviceScaleFactor = 3, 20 | Width = 360, 21 | Height = 8000, 22 | }).ConfigureAwait(false); 23 | await page.GoToAsync("https://www.nytimes.com/interactive/2021/us/covid-cases.html").ConfigureAwait(false); 24 | await page.WaitForSelectorAsync("#us-covid-cases > div > div > main > div.g-columns-outer.svelte-hfvvmm > div:nth-child(2) > section:nth-child(1)").ConfigureAwait(false); 25 | var deleteElement = await page.QuerySelectorAsync("#standalone-footer > div > div").ConfigureAwait(false); 26 | await deleteElement.EvaluateFunctionAsync("b => b.remove()").ConfigureAwait(false); 27 | var element = await page.QuerySelectorAsync("#__covidtracker__ > main > div.g-columns-outer.svelte-hfvvmm > div:nth-child(1) > section:nth-child(1)").ConfigureAwait(false); 28 | await page.SetViewportAsync(new ViewPortOptions 29 | { 30 | DeviceScaleFactor = 2, 31 | Width = 1024, 32 | Height = 4000, 33 | }).ConfigureAwait(false); 34 | var data2 = await element.ScreenshotDataAsync(new ElementScreenshotOptions 35 | { 36 | Type = ScreenshotType.Jpeg, 37 | Quality = 100, 38 | }).ConfigureAwait(false); 39 | await api.SendMessageAsync(context.Endpoint, Message.ByteArrayImage(data2)).ConfigureAwait(false); 40 | } 41 | 42 | public bool ShouldResponse(MessageContext context) 43 | => context.Content.TryGetPlainText(out var text) && "美国完了吗".Equals(text, StringComparison.Ordinal); 44 | } 45 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot.Public/啥玩意儿啊/获取图片链接.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text.RegularExpressions; 4 | using System.Threading.Tasks; 5 | using Bleatingsheep.NewHydrant.Attributions; 6 | using Microsoft.Extensions.Logging; 7 | using Sisters.WudiLib; 8 | using Sisters.WudiLib.Posts; 9 | using MessageContext = Sisters.WudiLib.Posts.Message; 10 | 11 | namespace Bleatingsheep.NewHydrant.啥玩意儿啊; 12 | #nullable enable 13 | [Component(nameof(获取图片链接))] 14 | internal partial class 获取图片链接 : IMessageCommand 15 | { 16 | private readonly ILogger<获取图片链接> _logger; 17 | 18 | public 获取图片链接(ILogger<获取图片链接> logger) 19 | { 20 | _logger = logger; 21 | } 22 | 23 | private string _command = default!; 24 | 25 | public async Task ProcessAsync(MessageContext context, HttpApiClient api) 26 | { 27 | var url = await CitedImageUrlUtility.GetCitedImageUrlAsync(context, api, _logger); 28 | if (url != null) 29 | { 30 | await api.SendMessageAsync(context.Endpoint, url); 31 | } 32 | } 33 | 34 | public bool ShouldResponse(MessageContext context) 35 | { 36 | var regex = GetCommandRegex(); 37 | var command = context switch 38 | { 39 | GroupMessage g => g.Content.MergeContinuousTextSections().Sections.Where(s => s.Type != Section.TextType || !string.IsNullOrWhiteSpace(s.Data[Section.TextParamName])).ToList() switch 40 | { 41 | [{ Type: "reply" }, { Type: "at" }, { Type: "at" }, { Type: "text" } s, ..] => s.Data["text"], 42 | [{ Type: "reply" }, { Type: "at" }, { Type: "text" } s, ..] => s.Data["text"], 43 | [{ Type: "reply" }, { Type: "text" } s, ..] => s.Data["text"], 44 | _ => default, 45 | }, 46 | _ => default, 47 | }; 48 | if (string.IsNullOrWhiteSpace(command)) 49 | { 50 | if (context.Content.Raw.Contains("/url", StringComparison.InvariantCultureIgnoreCase)) 51 | { 52 | // only for debug. 53 | return true; 54 | } 55 | return false; 56 | } 57 | if (regex.IsMatch(command)) 58 | { 59 | _command = command; 60 | return true; 61 | } 62 | return false; 63 | } 64 | 65 | [GeneratedRegex(@"^\s*/url(?:\s|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled)] 66 | private static partial Regex GetCommandRegex(); 67 | } 68 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot/Bleatingsheep.NewHydrant.Bot.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | Bleatingsheep.NewHydrant 7 | latest 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | Always 36 | 37 | 38 | Always 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot/NLog.config: -------------------------------------------------------------------------------- 1 |  2 | 4 | 5 | 6 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot/ReplicaConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Bleatingsheep.NewHydrant 8 | { 9 | public class ReplicaConfig 10 | { 11 | public bool DisablePrivate { get; set; } 12 | 13 | public static ReplicaConfig Parse(string accessToken) 14 | { 15 | var result = new ReplicaConfig(); 16 | var index = accessToken.IndexOf(':'); 17 | if (index == -1) 18 | return result; 19 | string[] kvps = accessToken[(index + 1)..].Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); 20 | foreach (var item in kvps) 21 | { 22 | var iEqual = item.IndexOf('='); 23 | var (name, value) = iEqual == -1 ? (item, string.Empty) : (item[..iEqual], item[(iEqual + 1)..]); 24 | var property = typeof(ReplicaConfig).GetProperty(name); 25 | if (property is null) 26 | continue; 27 | if (property.PropertyType == typeof(bool)) 28 | { 29 | if (string.IsNullOrWhiteSpace(value)) 30 | property.SetValue(result, true); 31 | try 32 | { 33 | property.SetValue(result, Convert.ToBoolean(value)); 34 | } 35 | #pragma warning disable RCS1075 // Avoid empty catch clause that catches System.Exception. 36 | catch (Exception) 37 | #pragma warning restore RCS1075 // Avoid empty catch clause that catches System.Exception. 38 | {// ignored 39 | } 40 | } 41 | try 42 | { 43 | property.SetValue(result, Convert.ChangeType(value, property.PropertyType)); 44 | } 45 | #pragma warning disable RCS1075 // Avoid empty catch clause that catches System.Exception. 46 | catch (Exception) 47 | #pragma warning restore RCS1075 // Avoid empty catch clause that catches System.Exception. 48 | {// ignored 49 | } 50 | } 51 | return result; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Bot/appsettings.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "Hydrant": { 3 | "ApiKey": "osu! API v1 key", 4 | "SuperAdmin": 0, 5 | "ServerPort": 8800, 6 | "ServerAccessToken": "reverse ws token.", 7 | "Chrome": { 8 | "Path": null 9 | } 10 | }, 11 | "ConnectionStrings": { 12 | "NewbieDatabase_Postgres": "Server=localhost;Port=5432;Database=xfs;User Id=xfs;Password=123456;" 13 | }, 14 | "Mahjong": { 15 | "TensoulBase": "https://tensoul.b11p.com/convert" 16 | }, 17 | "Services": { 18 | "random.org": "40af626d-27b3-4dae-b7f5-4cd73ac436b4" 19 | } 20 | } -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Data/Bleatingsheep.NewHydrant.Data.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | latest 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Data/IDataProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Bleatingsheep.Osu; 4 | using Bleatingsheep.Osu.ApiClient; 5 | using Bleatingsheep.OsuQqBot.Database.Models; 6 | 7 | namespace Bleatingsheep.NewHydrant.Data 8 | { 9 | /// 10 | /// Not thread-safe. 11 | /// 12 | public interface IDataProvider 13 | { 14 | Task GetUserBestRetryAsync(int userId, Mode mode, CancellationToken cancellationToken = default); 15 | 16 | Task GetUserBestLimitRetryAsync(int userId, Mode mode, int limit, CancellationToken cancellationToken = default); 17 | 18 | Task GetUserInfoRetryAsync(int userId, Mode mode, CancellationToken cancellationToken = default); 19 | 20 | ValueTask GetBindingInfoAsync(long qq); 21 | 22 | Task GetOsuIdAsync(long qq); 23 | 24 | ValueTask GetBeatmapInfoAsync(int beatmapId, Mode mode, CancellationToken cancellationToken = default); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Data/ILegacyDataProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Bleatingsheep.NewHydrant.Data 4 | { 5 | public interface ILegacyDataProvider 6 | { 7 | Task<(bool success, int? result)> GetBindingIdAsync(long qq); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Data/IOsuDataUpdator.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Bleatingsheep.Osu.PerformancePlus; 3 | using Bleatingsheep.OsuQqBot.Database.Models; 4 | 5 | namespace Bleatingsheep.NewHydrant.Data; 6 | public interface IOsuDataUpdator 7 | { 8 | public ValueTask<(bool isChanged, int? oldOsuId, BindingInfo newBindingInfo)> AddOrUpdateBindingInfoAsync(long qq, int osuId, string osuName, string source, long? operatorId, string operatorName, string reason = "", bool allowOverwrite = false); 9 | ValueTask> AddPlusHistoryAsync(IUserPlus userPlus); 10 | } 11 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Data/Results/IXfsDataResult.cs: -------------------------------------------------------------------------------- 1 | namespace Bleatingsheep.NewHydrant.Data.Results; 2 | public interface IXfsDataResult 3 | { 4 | bool IsOk { get; } 5 | TResult OkResult { get; } 6 | TError Error { get; } 7 | } 8 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Data/Results/XfsDataError.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Bleatingsheep.NewHydrant.Data.Results; 4 | public sealed class XfsDataError 5 | { 6 | public XfsDataError(ErrorKind kind, Exception? exception = default) 7 | { 8 | Kind = kind; 9 | Exception = exception; 10 | } 11 | 12 | public ErrorKind Kind { get; } 13 | public Exception? Exception { get; } 14 | 15 | public enum ErrorKind 16 | { 17 | NoBinding, 18 | DatabaseError, 19 | DatabaseConcurrencyError, 20 | ExternalError, 21 | } 22 | } -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Data/Results/XfsDataResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Bleatingsheep.NewHydrant.Data.Results; 4 | public static class XfsDataResult 5 | { 6 | public static XfsDataResult Ok(TResult result) 7 | { 8 | return new XfsDataResult(result); 9 | } 10 | 11 | public static XfsDataResult Ok(TResult result) 12 | { 13 | return new XfsDataResult(result); 14 | } 15 | 16 | public static XfsDataResult Error(TError error) 17 | { 18 | return new XfsDataResult(error); 19 | } 20 | 21 | public static XfsDataResult Error(TError error) 22 | { 23 | return new XfsDataResult(error); 24 | } 25 | } 26 | 27 | public readonly struct XfsDataResult : IXfsDataResult 28 | { 29 | private readonly bool _isOk; 30 | private readonly TResult _result; 31 | private readonly TError _error; 32 | 33 | public XfsDataResult(TResult result) 34 | { 35 | _result = result; 36 | _isOk = true; 37 | _error = default!; 38 | } 39 | 40 | public XfsDataResult(TError error) 41 | { 42 | _error = error; 43 | _isOk = false; 44 | _result = default!; 45 | } 46 | 47 | bool IXfsDataResult.IsOk => _isOk; 48 | 49 | TResult IXfsDataResult.OkResult 50 | { 51 | get 52 | { 53 | return _isOk ? _result : throw new InvalidOperationException("The result is not of success."); 54 | } 55 | } 56 | 57 | TError IXfsDataResult.Error 58 | { 59 | get 60 | { 61 | return _isOk 62 | ? throw new InvalidOperationException("The result is of success") 63 | : _error; 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Data/Results/XfsDataResultExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace Bleatingsheep.NewHydrant.Data.Results; 5 | 6 | public static class XfsDataResultExtensions 7 | { 8 | public static bool TryGetResult(this IXfsDataResult result, [MaybeNullWhen(false)] out TResult okResult) 9 | { 10 | if (result.IsOk) 11 | { 12 | okResult = result.OkResult; 13 | return true; 14 | } 15 | okResult = default; 16 | return false; 17 | } 18 | 19 | public static bool TryGetError(this IXfsDataResult result, [MaybeNullWhen(false)] out TError error) 20 | { 21 | if (result.IsOk) 22 | { 23 | error = default; 24 | return false; 25 | } 26 | error = result.Error; 27 | return true; 28 | } 29 | 30 | public static void Match(this IXfsDataResult result, Action whenOk, Action whenError) 31 | { 32 | if (result.IsOk) 33 | { 34 | whenOk?.Invoke(result.OkResult); 35 | } 36 | else 37 | { 38 | whenError?.Invoke(result.Error); 39 | } 40 | } 41 | 42 | public static T Match(this IXfsDataResult result, Func whenOk, Func whenError) 43 | { 44 | if (result.IsOk) 45 | { 46 | ArgumentNullException.ThrowIfNull(whenOk); 47 | return whenOk.Invoke(result.OkResult); 48 | } 49 | else 50 | { 51 | ArgumentNullException.ThrowIfNull(whenError); 52 | return whenError.Invoke(result.Error); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.Data/global.cs: -------------------------------------------------------------------------------- 1 | global using Bleatingsheep.NewHydrant.Data.Results; 2 | global using Result = Bleatingsheep.NewHydrant.Data.Results.XfsDataResult; -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.DataMaintenance/Bleatingsheep.NewHydrant.DataMaintenance.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | dotnet-Bleatingsheep.NewHydrant.DataMaintenance-833e4f4f-89e1-42b7-b57f-6ee67cd154aa 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Always 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.DataMaintenance/NLog.config: -------------------------------------------------------------------------------- 1 |  2 | 4 | 5 | 6 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.DataMaintenance/Program.cs: -------------------------------------------------------------------------------- 1 | using Bleatingsheep.NewHydrant.Data; 2 | using Bleatingsheep.NewHydrant.DataMaintenance; 3 | using Bleatingsheep.Osu.ApiClient; 4 | using Bleatingsheep.OsuQqBot.Database.Models; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore.Diagnostics; 7 | using NLog.Extensions.Hosting; 8 | using WebApiClient; 9 | 10 | IHost host = Host.CreateDefaultBuilder(args) 11 | .ConfigureServices((ctx, services) => 12 | { 13 | services.AddHostedService(); 14 | services.AddHostedService(); 15 | services.AddHostedService(); 16 | 17 | string? connectionString = ctx.Configuration.GetConnectionString("NewbieDatabase_Postgres"); 18 | var dataSource = NewbieContext.GetDataSource(connectionString); 19 | services.AddDbContext(optionsBuilder => 20 | optionsBuilder.UseNpgsql( 21 | dataSource, 22 | options => options.EnableRetryOnFailure().CommandTimeout(600)) 23 | .ConfigureWarnings(c => c.Log((RelationalEventId.CommandExecuting, LogLevel.Debug), 24 | (RelationalEventId.CommandExecuted, LogLevel.Debug))), 25 | ServiceLifetime.Transient); 26 | services.AddDbContextFactory(optionsBuilder => 27 | optionsBuilder.UseNpgsql( 28 | dataSource, 29 | options => options.EnableRetryOnFailure().CommandTimeout(600)) 30 | .ConfigureWarnings(c => c.Log((RelationalEventId.CommandExecuting, LogLevel.Debug), 31 | (RelationalEventId.CommandExecuted, LogLevel.Debug)))); 32 | services.AddTransient(); 33 | 34 | var factory = OsuApiClientFactory.CreateFactory(ctx.Configuration.GetSection("Hydrant")["ApiKey"]); 35 | services.AddSingleton>(factory); 36 | services.AddScoped(c => c.GetRequiredService>().CreateHttpApi()); 37 | }) 38 | .ConfigureLogging(logging => 39 | { 40 | logging.ClearProviders(); 41 | logging.SetMinimumLevel(LogLevel.Trace); 42 | }) 43 | .UseNLog() 44 | .Build(); 45 | 46 | await host.RunAsync().ConfigureAwait(false); 47 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.DataMaintenance/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Bleatingsheep.NewHydrant.DataMaintenance": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": true, 6 | "environmentVariables": { 7 | "DOTNET_ENVIRONMENT": "Development" 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.DataMaintenance/SyncScheduleService.cs: -------------------------------------------------------------------------------- 1 | using Bleatingsheep.Osu; 2 | using Bleatingsheep.OsuQqBot.Database.Models; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace Bleatingsheep.NewHydrant.DataMaintenance; 6 | /// 7 | /// Sync snapshot schedule from binded users to update schedules. 8 | /// 9 | public sealed class SyncScheduleService : BackgroundService 10 | { 11 | private static readonly SemaphoreSlim s_semaphore = new(1); 12 | private readonly IDbContextFactory _dbContextFactory; 13 | private readonly ILogger _logger; 14 | 15 | public static TimeSpan OnUtc => new(19, 29, 59); 16 | 17 | public SyncScheduleService(IDbContextFactory dbContextFactory, ILogger logger) 18 | { 19 | _dbContextFactory = dbContextFactory; 20 | _logger = logger; 21 | } 22 | 23 | public async Task RunAsync() 24 | { 25 | _logger.LogInformation("Start SyncScheduleService"); 26 | await using var db1 = _dbContextFactory.CreateDbContext(); 27 | db1.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; 28 | var snapshotted = 29 | await db1.UserSnapshots 30 | .Select(s => new { s.UserId, s.Mode }) 31 | .Distinct() 32 | .ToListAsync() 33 | .ConfigureAwait(false); 34 | var binded = await 35 | (from b in db1.Bindings.AsAsyncEnumerable() 36 | from m in new[] { Mode.Standard, Mode.Taiko, Mode.Catch, Mode.Mania }.ToAsyncEnumerable() 37 | select new { UserId = b.OsuId, Mode = m }) 38 | .ToListAsync().ConfigureAwait(false); 39 | var scheduled = 40 | await db1.UpdateSchedules 41 | .Select(s => new { s.UserId, s.Mode }) 42 | .ToListAsync() 43 | .ConfigureAwait(false); 44 | var toSchedule = snapshotted.Union(binded).Except(scheduled).Select(i => new UpdateSchedule 45 | { 46 | UserId = i.UserId, 47 | Mode = i.Mode, 48 | NextUpdate = DateTimeOffset.UtcNow, 49 | }).ToList(); 50 | if (toSchedule.Count > 0) 51 | { 52 | _logger.LogInformation("Adding {toSchedule.Count} items to schedule.", toSchedule.Count); 53 | db1.UpdateSchedules.AddRange(toSchedule); 54 | await db1.SaveChangesAsync().ConfigureAwait(false); 55 | } 56 | _logger.LogInformation("End SyncScheduleService"); 57 | } 58 | 59 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 60 | { 61 | while (!stoppingToken.IsCancellationRequested) 62 | { 63 | var timeToNextRun = new TimeSpan(24, 0, 0) - (DateTimeOffset.UtcNow - OnUtc).TimeOfDay; 64 | _logger.LogInformation("Will trigger at {OnUtc}, waiting {timeToNextRun}", OnUtc, timeToNextRun); 65 | await Task.Delay(timeToNextRun, stoppingToken).ConfigureAwait(false); 66 | try 67 | { 68 | await RunAsync().ConfigureAwait(false); 69 | } 70 | catch (Exception e) 71 | { 72 | _logger.LogError(e, "error"); 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.DataMaintenance/Worker.cs: -------------------------------------------------------------------------------- 1 | namespace Bleatingsheep.NewHydrant.DataMaintenance; 2 | 3 | public class Worker : BackgroundService 4 | { 5 | private readonly ILogger _logger; 6 | 7 | public Worker(ILogger logger) 8 | { 9 | _logger = logger; 10 | } 11 | 12 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 13 | { 14 | while (!stoppingToken.IsCancellationRequested) 15 | { 16 | //_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); 17 | await Task.Delay(1000, stoppingToken); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant.DataMaintenance/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.Hosting.Lifetime": "Information" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant/Attributions/ComponentAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Bleatingsheep.NewHydrant.Attributions 4 | { 5 | [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] 6 | public sealed class ComponentAttribute : Attribute 7 | { 8 | public ComponentAttribute(string name) => Name = name; 9 | 10 | public string Name { get; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant/Attributions/IInitializable.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Bleatingsheep.NewHydrant.Core; 3 | 4 | namespace Bleatingsheep.NewHydrant.Attributions 5 | { 6 | /// 7 | /// 实现此接口的功能会自动调用方法。并可能在需要的时候多次调用方法重新初始化。 8 | /// 9 | public interface IInitializable 10 | { 11 | /// 12 | /// 名称,用于指令中识别。 13 | /// 14 | string? Name { get; } 15 | 16 | /// 17 | /// 初始化,或者重新初始化。 18 | /// 19 | /// 是否成功。 20 | Task InitializeAsync(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant/Attributions/IMessageCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Bleatingsheep.NewHydrant.Core; 3 | using MessageContext = Sisters.WudiLib.Posts.Message; 4 | 5 | namespace Bleatingsheep.NewHydrant.Attributions 6 | { 7 | public interface IMessageCommand 8 | { 9 | bool ShouldResponse(MessageContext context); 10 | Task ProcessAsync(MessageContext context, Sisters.WudiLib.HttpApiClient api); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant/Attributions/IMessageMonitor.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Sisters.WudiLib.Posts; 3 | 4 | namespace Bleatingsheep.NewHydrant.Attributions 5 | { 6 | public interface IMessageMonitor 7 | { 8 | Task OnMessageAsync(Message message, Sisters.WudiLib.HttpApiClient api); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant/Attributions/IRegularAsync.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Bleatingsheep.NewHydrant.Core; 4 | using Sisters.WudiLib; 5 | 6 | namespace Bleatingsheep.NewHydrant.Attributions 7 | { 8 | public interface IRegularAsync 9 | { 10 | TimeSpan? OnUtc { get; } 11 | TimeSpan? Every { get; } 12 | Task RunAsync(HttpApiClient api); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant/Attributions/ParameterAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Bleatingsheep.NewHydrant.Attributions 4 | { 5 | [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] 6 | public sealed class ParameterAttribute : Attribute 7 | { 8 | 9 | public ParameterAttribute(string groupName) 10 | => GroupName = groupName; 11 | 12 | public string GroupName { get; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant/Bleatingsheep.NewHydrant.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0;netstandard2.1 5 | latest 6 | 7 | 8 | 9 | enable 10 | CS8600;CS8602;CS8603;CS8618 11 | 12 | 13 | 14 | annotations 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant/Core/ExecutingException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace Bleatingsheep.NewHydrant.Core 5 | { 6 | 7 | [Serializable] 8 | public class ExecutingException : Exception 9 | { 10 | public ExecutingException(string message) : base(message) { } 11 | public ExecutingException(string message, Exception inner) : base(message, inner) { } 12 | protected ExecutingException( 13 | System.Runtime.Serialization.SerializationInfo info, 14 | System.Runtime.Serialization.StreamingContext context) : base(info, context) { } 15 | 16 | public static void Ensure(string onFalse, params bool[] success) 17 | { 18 | if (success.Contains(false)) throw new ExecutingException(onFalse); 19 | } 20 | 21 | public static void Ensure(bool success, string onFalse) 22 | { 23 | if (!success) throw new ExecutingException(onFalse); 24 | } 25 | 26 | public static void Ensure(Func test, string onFalse) => Ensure(test(), onFalse); 27 | 28 | public static void Cannot(bool isFail, string onFail) => Ensure(!isFail, onFail); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant/Core/IHydrantStartup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace Bleatingsheep.NewHydrant.Core 4 | { 5 | public interface IHydrantStartup 6 | { 7 | void ConfigureServices(IServiceCollection services); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant/Core/ScheduleInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Bleatingsheep.NewHydrant.Attributions; 3 | 4 | namespace Bleatingsheep.NewHydrant.Core 5 | { 6 | internal class ScheduleInfo 7 | { 8 | private static readonly TimeSpan Close = new TimeSpan(0, 0, 1); 9 | 10 | public ScheduleType Type { get; } 11 | public TimeSpan Time { get; } 12 | public IRegularAsync Action { get; } 13 | public DateTime NextRun { get; set; } 14 | public TimeSpan WaitTime 15 | { 16 | get 17 | { 18 | var result = NextRun - DateTime.UtcNow; 19 | return result < TimeSpan.Zero ? TimeSpan.Zero : result; 20 | } 21 | } 22 | public ScheduleInfo(ScheduleType type, TimeSpan time, IRegularAsync action) 23 | { 24 | Type = type; 25 | Time = time; 26 | Action = action; 27 | switch (Type) 28 | { 29 | case ScheduleType.ByInterval: 30 | NextRun = DateTime.UtcNow; 31 | break; 32 | case ScheduleType.Daily: 33 | NextRun = DateTime.UtcNow.Date + time; 34 | if (ShouldRun()) 35 | NextRun = NextRun.AddDays(1); 36 | break; 37 | default: 38 | throw new ArgumentException("类型不对", nameof(type)); 39 | } 40 | } 41 | 42 | public bool ShouldRun() => NextRun - DateTime.UtcNow < Close; 43 | 44 | public void Next() 45 | { 46 | switch (Type) 47 | { 48 | case ScheduleType.ByInterval: 49 | NextRun += Time; 50 | if (ShouldRun()) NextRun = DateTime.UtcNow; 51 | break; 52 | case ScheduleType.Daily: 53 | do 54 | { 55 | NextRun = NextRun.AddDays(1); 56 | } while (ShouldRun()); 57 | break; 58 | default: 59 | break; 60 | } 61 | } 62 | } 63 | 64 | internal enum ScheduleType 65 | { 66 | ByInterval = 0, 67 | Daily = 1, 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant/Core/TypeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace Bleatingsheep.NewHydrant.Core 5 | { 6 | internal static class TypeExtensions 7 | { 8 | public static object CreateInstance(this Type type, IServiceScope scope) 9 | { 10 | if (type is null || scope is null) 11 | { 12 | throw new ArgumentNullException(nameof(type)); 13 | } 14 | 15 | return scope.ServiceProvider.GetService(type) ?? throw new InvalidOperationException("The type is not registered."); 16 | } 17 | 18 | /// 19 | /// 20 | public static T CreateInstance(this Type type, IServiceScope scope) where T : notnull 21 | => (T)type.CreateInstance(scope); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant/Extentions/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | 4 | namespace System.Linq 5 | { 6 | internal static class EnumerableExtensions 7 | { 8 | public static void ForEach(this IEnumerable source, Action action) 9 | { 10 | foreach (T item in source) 11 | { 12 | action(item); 13 | } 14 | } 15 | 16 | public static async Task ForEachAsync(this IEnumerable source, Func action) 17 | { 18 | foreach (T item in source) 19 | { 20 | await action(item); 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Bleatingsheep 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant/TODO.md: -------------------------------------------------------------------------------- 1 | 重构: 2 | Assembly/Service? 3 | Service/Assembly? 4 | 5 | ProcedureException 6 | 7 | motd (message of the day) 8 | 将消防栓的群名片改为今日消息 9 | 10 | 一个想法: 11 | 利用 C# 里的插值字符串,实现类似 ASP .NET 的 View 功能。 12 | 13 | 用下面的方式处理命令: 14 | ```c# 15 | [RegexCommand("here is regex")] // 或者 [Command.Regex("")] ?其中RegexAttribute是嵌套类。 16 | public async Task SomeCommandX(string param1, int param2, ...) // 也许可以增加自定义类型转换功能。 17 | { 18 | /* 19 | * Regex 处理流程: 20 | * 纯文本:正常处理 21 | * 单个 CQ 码:不处理(是否可以增加处理单个 CQ 码的机制?处理分享什么的,单个表情默认排除?某些情况下可以被识别为命令?) 22 | * 文本使用转义后的,记录下 CQ 码的下标起止,如果匹配会撕裂 CQ 码,则视为失败。 23 | * 如果想匹配 CQ 码,也许可以尝试在 CQ 码前面加某个符号(比如'$'?),加的数量比消息中连续'$'的数量多,然后每条消息分别生成一个正则。 24 | * (也许替换成单个字符进行匹配?表情/图片->私人使用区,设计一个表情到私人使用区的映射,每张不同的图片都用一个不同的码位) 25 | * 手机QQ就是用的这种方法实现的,但用电脑发表示表情的特殊字符,电脑上还是特殊字符,手机上显示表情(酷Q疑似收到特殊字符而不是表情/CQ码) 26 | */ 27 | 28 | /* 29 | * 转换器想法: 30 | * 默认转换器:只转换纯文本,除非是 Message 类型,否则视为匹配失败 31 | * 自定义转换器:可接收 Message 或 string 类型,如果接收的是 string,且不是纯文本,则视为匹配失败。 32 | */ 33 | } 34 | 35 | [Command("command {param1} {param2}")] 36 | [Command.MultiLine(@"command 37 | {param1} 38 | {param2}")] // 还没想好多行的怎么实现 39 | public async Task SomeCommandX(int param1, Message/[CQType("image")]MessageSegment param2) // 可以保留表情等等 40 | { 41 | 42 | } 43 | ``` -------------------------------------------------------------------------------- /Bleatingsheep.NewHydrant/Template.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Bleatingsheep.NewHydrant.Attributions; 5 | using Message = Sisters.WudiLib.SendingMessage; 6 | using MessageContext = Sisters.WudiLib.Posts.Message; 7 | 8 | namespace $rootnamespace$ 9 | { 10 | #nullable enable 11 | [Component("$itemname$")] 12 | public class $safeitemname$ 13 | { 14 | } 15 | #nullable restore 16 | } 17 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuApiClient/BestPerformance.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace Bleatingsheep.OsuMixedApi 4 | { 5 | public class BestPerformance : PlayRecord 6 | { 7 | [JsonProperty("pp")] 8 | public double PP { get; private set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuApiClient/Bleatingsheep.OsuMixedApi.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 8.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuApiClient/Diagnostics.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace Bleatingsheep.OsuMixedApi 7 | { 8 | public static class Diagnostics 9 | { 10 | public static event Action OnRequestFinished; 11 | 12 | internal static async void FinishRequest(string url, long milliseconds, Exception exception) 13 | { 14 | await Task.Run(() => 15 | { 16 | try 17 | { 18 | OnRequestFinished?.Invoke(url, milliseconds, exception); 19 | } 20 | catch (Exception) 21 | { 22 | } 23 | }); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuApiClient/Execute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | 6 | namespace Bleatingsheep.OsuMixedApi 7 | { 8 | internal static class Execute 9 | { 10 | internal static T Do(Func func, string messageOnFail) 11 | { 12 | try 13 | { 14 | return func(); 15 | } 16 | catch (Exception e) 17 | { 18 | throw new OsuApiFailedException(messageOnFail, e); 19 | } 20 | } 21 | 22 | internal static void Do(Action action, string messageOnFail) 23 | { 24 | try 25 | { 26 | action(); 27 | } 28 | catch (Exception e) 29 | { 30 | throw new OsuApiFailedException(messageOnFail, e); 31 | } 32 | } 33 | 34 | internal static async Task DoAsync(Func> func, string messageOnFail) 35 | { 36 | try 37 | { 38 | return await func(); 39 | } 40 | catch (Exception e) 41 | { 42 | throw new OsuApiFailedException(messageOnFail, e); 43 | } 44 | } 45 | 46 | internal static async Task DoAsync(Func func, string messageOnFail) 47 | { 48 | try 49 | { 50 | await func(); 51 | } 52 | catch (Exception e) 53 | { 54 | throw new OsuApiFailedException(messageOnFail, e); 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuApiClient/HttpMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | using System.Web; 6 | using Newtonsoft.Json; 7 | 8 | namespace Bleatingsheep.OsuMixedApi 9 | { 10 | internal static class HttpMethods 11 | { 12 | private static HttpClient s_httpClient = new HttpClient(); 13 | private static long s_httpClientCreateDate = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); 14 | 15 | /// 16 | /// Get array with specified URL and arguments. 17 | /// 18 | /// Type of elements in the array. 19 | /// Request URL. 20 | /// Arguments. 21 | /// Required array. null if network failed. Empty if no result. 22 | internal static async Task GetJsonArrayDeserializeAsync(string url, params (string key, string value)[] ps) 23 | { 24 | var (success, result) = await GetJsonDeserializeAsync(url, ps); 25 | if (!success) return null; 26 | return result; 27 | } 28 | 29 | internal static async Task<(bool success, T result)> GetJsonDeserializeAsync(string url, params (string key, string value)[] ps) 30 | { 31 | string json = await GetAsync(url, ps); 32 | if (json == null) return (false, default(T)); 33 | T result = JsonConvert.DeserializeObject(json, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); 34 | return (true, result); 35 | } 36 | 37 | private static async Task GetAsync(string url, params (string key, string value)[] ps) 38 | { 39 | if (url == null) throw new ArgumentNullException(nameof(url)); 40 | char needed = '?'; 41 | foreach (var (key, value) in ps) 42 | { 43 | url += needed + key + "=" + HttpUtility.UrlEncode(value); 44 | needed = '&'; 45 | } 46 | 47 | UpdateClientInstanceIfNecessary(); 48 | var client = s_httpClient; 49 | string result = null; 50 | var stopwatch = Stopwatch.StartNew(); 51 | Exception exception = null; 52 | try 53 | { 54 | result = await client.GetStringAsync(url); 55 | } 56 | catch (Exception e) 57 | { 58 | exception = e; 59 | } 60 | Diagnostics.FinishRequest(url, stopwatch.ElapsedMilliseconds, exception); 61 | return result; 62 | } 63 | 64 | private static void UpdateClientInstanceIfNecessary() 65 | { 66 | int outdated = 1800; 67 | var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); 68 | if (now - s_httpClientCreateDate > outdated) 69 | { 70 | s_httpClient = new HttpClient(); 71 | s_httpClientCreateDate = now; 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuApiClient/Mode.cs: -------------------------------------------------------------------------------- 1 | namespace Bleatingsheep.OsuMixedApi 2 | { 3 | public enum Mode 4 | { 5 | Standard = 0, 6 | Taiko = 1, 7 | Ctb = 2, 8 | Mania = 3, 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuApiClient/ModeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Linq; 5 | 6 | namespace Bleatingsheep.OsuMixedApi 7 | { 8 | public static class ModeExtensions 9 | { 10 | private const string ModeInfo = @"0,std,osu,osu!,standard 11 | 1,taiko,osu!taiko 12 | 2,ctb,catch,osu!catch 13 | 3,mania,osu!mania"; 14 | 15 | private static readonly IReadOnlyDictionary pairs; 16 | 17 | static ModeExtensions() 18 | { 19 | void ConcatLine(string line, Mode mode, ref IEnumerable> toAdd) 20 | { 21 | var alias = line.Split(',').Select(s => new KeyValuePair(s, mode)); 22 | toAdd = toAdd.Concat(alias); 23 | } 24 | 25 | IEnumerable> maps = new Dictionary(); 26 | 27 | var aliases = ModeInfo.ToUpperInvariant().Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); 28 | 29 | for (int i = 0; i < aliases.Length; i++) 30 | { 31 | ConcatLine(aliases[i], (Mode)i, ref maps); 32 | } 33 | 34 | pairs = new ReadOnlyDictionary(maps.ToDictionary(p => p.Key, p => p.Value)); 35 | } 36 | 37 | /// 38 | /// Comvert a to . 39 | /// 40 | /// A string containing a mode to convert. 41 | /// s is null. 42 | /// s is not a valid mode string. 43 | /// 44 | public static Mode Parse(string s) 45 | { 46 | s = s.ToUpperInvariant(); 47 | return pairs.TryGetValue(s, out Mode result) ? result : throw new ArgumentException("Invalid mode string.", nameof(s)); 48 | } 49 | 50 | public static string GetShortModeString(this Mode mode) 51 | { 52 | switch (mode) 53 | { 54 | case Mode.Standard: 55 | return "osu!"; 56 | case Mode.Taiko: 57 | return "taiko"; 58 | case Mode.Ctb: 59 | return "catch"; 60 | case Mode.Mania: 61 | return "mania"; 62 | default: 63 | return null; 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuApiClient/Mods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Bleatingsheep.OsuMixedApi 4 | { 5 | [Flags] 6 | public enum Mods 7 | { 8 | None = 0, 9 | NoFail = 1, 10 | Easy = 2, 11 | NoVideo = 4, // Not used anymore, but can be found on old plays like Mesita on b/78239 12 | Hidden = 8, 13 | HardRock = 16, 14 | SuddenDeath = 32, 15 | DoubleTime = 64, 16 | Relax = 128, 17 | HalfTime = 256, 18 | Nightcore = 512, // Only set along with DoubleTime. i.e: NC only gives 576 19 | Flashlight = 1024, 20 | Autoplay = 2048, 21 | SpunOut = 4096, 22 | Relax2 = 8192, // Autopilot? 23 | Perfect = 16384, // Only set along with SuddenDeath. i.e: PF only gives 16416 24 | Key4 = 32768, 25 | Key5 = 65536, 26 | Key6 = 131072, 27 | Key7 = 262144, 28 | Key8 = 524288, 29 | keyMod = Key4 | Key5 | Key6 | Key7 | Key8, 30 | FadeIn = 1048576, 31 | Random = 2097152, 32 | LastMod = 4194304, 33 | FreeModAllowed = NoFail | Easy | Hidden | HardRock | SuddenDeath | Flashlight | FadeIn | Relax | Relax2 | SpunOut | keyMod, 34 | Key9 = 16777216, 35 | Key10 = 33554432, 36 | Key1 = 67108864, 37 | Key3 = 134217728, 38 | Key2 = 268435456, 39 | ScoreV2 = 536870912, 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuApiClient/MotherShip/MotherShipApiClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Bleatingsheep.OsuMixedApi.MotherShip 5 | { 6 | public class MotherShipApiClient 7 | { 8 | public const string DefaultHost = "https://www.mothership.top/"; 9 | public const string LegacyInsecureHost = "http://www.mothership.top:8080/"; 10 | private readonly string _host; 11 | 12 | public MotherShipApiClient(string host) 13 | { 14 | if (!host?.EndsWith("/", StringComparison.Ordinal) ?? throw new ArgumentNullException(nameof(host))) 15 | host += '/'; 16 | _host = host; 17 | } 18 | 19 | private string UserInfoUrl(long qqId) => _host + $"api/v1/user/qq/{qqId}"; 20 | private string UserYesterdayInfoUrl(int osuId) => _host + $"api/v1/userinfo/nearest/{osuId}"; 21 | 22 | public string GetStatUrl(int osuId) => _host + $"api/v1/stat/{osuId}"; 23 | 24 | /// 25 | /// 26 | /// 27 | /// 访问网络失败。 28 | /// 29 | /// 30 | public virtual async Task> GetUserInfoAsync(long qqId) 31 | { 32 | var (success, result) = await Execute.Do(async () => 33 | { 34 | return await HttpMethods.GetJsonDeserializeAsync>(UserInfoUrl(qqId)); 35 | }, "Network error."); 36 | if (!success) 37 | throw new OsuApiFailedException("Network error."); 38 | return result; 39 | } 40 | 41 | /// 42 | /// 43 | /// 44 | /// 45 | /// 46 | /// 47 | public virtual async Task> GetYesterdayInfo(int osuId) 48 | { 49 | var (success, result) = await Execute.Do(async () => 50 | { 51 | return await HttpMethods.GetJsonDeserializeAsync>(UserYesterdayInfoUrl(osuId)); 52 | }, "Network error."); 53 | if (!success) 54 | throw new OsuApiFailedException("Network error."); 55 | return result; 56 | } 57 | 58 | /// 59 | /// 60 | /// 61 | /// 62 | /// 访问网络失败。 63 | /// 64 | public async Task GetUserBindAsync(long qqId) 65 | { 66 | var response = await GetUserInfoAsync(qqId); 67 | return response?.IsSuccessStatusCode() == true ? (int?)response.Data.OsuId : null; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuApiClient/MotherShip/MotherShipResponse.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace Bleatingsheep.OsuMixedApi.MotherShip 4 | { 5 | public class MotherShipResponse 6 | { 7 | public const int SuccessStatusCode = 0; 8 | public const string SuccessStatusMessage = "success"; 9 | 10 | [JsonProperty("code")] 11 | public int StatusCode { get; private set; } 12 | [JsonProperty("status")] 13 | public string Message { get; private set; } 14 | [JsonProperty("data")] 15 | public T Data { get; private set; } 16 | 17 | /// 18 | /// Throws if is not . 19 | /// 20 | /// 21 | public void EnsureSuccess() 22 | { 23 | if (!IsSuccessStatusCode()) throw new OsuApiFailedException(); 24 | } 25 | 26 | public bool IsSuccessStatusCode() => StatusCode == SuccessStatusCode; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuApiClient/MotherShip/MotherShipUserInfo.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | 4 | namespace Bleatingsheep.OsuMixedApi.MotherShip 5 | { 6 | [JsonObject(MemberSerialization.OptIn)] 7 | public class MotherShipUserInfo 8 | { 9 | #pragma warning disable CS0649 10 | [JsonProperty("userId")] 11 | public int OsuId { get; private set; } 12 | [JsonProperty("role")] 13 | private string role; 14 | public string[] Roles => role.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); 15 | [JsonProperty("qq")] 16 | public long QqId { get; private set; } 17 | [JsonProperty("legacyUname")] 18 | private string legacyName; 19 | public string[] LegacyNames => JsonConvert.DeserializeObject(legacyName); 20 | [JsonProperty("currentUname")] 21 | public string Name { get; private set; } 22 | [JsonProperty("banned")] 23 | public bool IsBanned { get; private set; } 24 | //[JsonProperty("mode")] 25 | //public Mode Mode { get; private set; } 26 | [JsonProperty("repeatCount")] 27 | public int RepeatCount { get; private set; } 28 | [JsonProperty("speakingCount")] 29 | public int SpeakingCount { get; private set; } 30 | #pragma warning restore CS0649 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuApiClient/MotherShip/UserHistory.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | 4 | namespace Bleatingsheep.OsuMixedApi.MotherShip 5 | { 6 | #pragma warning disable CS0649 7 | [JsonObject(MemberSerialization.OptIn)] 8 | public class UserHistory 9 | { 10 | [JsonProperty("username")] 11 | public string Username { get; private set; } 12 | [JsonProperty("mode")] 13 | public Mode Mode { get; private set; } 14 | [JsonProperty("userId")] 15 | public int Id { get; private set; } 16 | [JsonProperty("count300")] 17 | public int Count300 { get; set; } 18 | [JsonProperty("count100")] 19 | public int Count100 { get; set; } 20 | [JsonProperty("count50")] 21 | public int Count50 { get; set; } 22 | public int TotalHits => Count300 + Count100 + Count50; 23 | [JsonProperty("playcount")] 24 | public int PlayCount { get; private set; } 25 | [JsonProperty("accuracy")] 26 | private double _accuracy; 27 | public double Accuracy => _accuracy / 100.0; 28 | [JsonProperty("ppRaw")] 29 | public double PP { get; private set; } 30 | [JsonProperty("rankedScore")] 31 | public long RankedScore { get; private set; } 32 | [JsonProperty("totalScore")] 33 | public long TotalScore { get; private set; } 34 | [JsonProperty("level")] 35 | public double Level { get; private set; } 36 | [JsonProperty("ppRank")] 37 | public int Rank { get; private set; } 38 | [JsonProperty("countRankSs")] 39 | public int CountSs { get; private set; } 40 | [JsonProperty("countRankSsh")] 41 | public int CountSsh { get; private set; } 42 | [JsonProperty("countRankS")] 43 | public int CountS { get; private set; } 44 | [JsonProperty("countRankSh")] 45 | public int CountSh { get; private set; } 46 | [JsonProperty("countRankA")] 47 | public int CountA { get; private set; } 48 | [JsonProperty("queryDate")] 49 | public Querydate QueryDate { get; private set; } 50 | } 51 | 52 | [JsonObject(MemberSerialization.OptIn)] 53 | public class Querydate 54 | { 55 | [JsonProperty("year")] 56 | public int Year { get; private set; } 57 | [JsonProperty("month")] 58 | public int Month { get; private set; } 59 | [JsonProperty("day")] 60 | public int Day { get; private set; } 61 | public DateTime Date => new DateTime(Year, Month, Day); 62 | } 63 | #pragma warning restore CS0649 64 | } 65 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuApiClient/OsuApiFailedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Bleatingsheep.OsuMixedApi 4 | { 5 | [Serializable] 6 | public class OsuApiFailedException : Exception 7 | { 8 | public OsuApiFailedException() { } 9 | public OsuApiFailedException(string message) : base(message) { } 10 | public OsuApiFailedException(string message, Exception inner) : base(message, inner) { } 11 | protected OsuApiFailedException( 12 | System.Runtime.Serialization.SerializationInfo info, 13 | System.Runtime.Serialization.StreamingContext context) : base(info, context) { } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuApiClient/System.Collections.Generic/CollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace System.Collections.Generic 6 | { 7 | internal static class CollectionExtensions 8 | { 9 | public static TValue GetValueOrDefault(this IReadOnlyDictionary dictionary, TKey key) 10 | { 11 | return dictionary.GetValueOrDefault(key, default(TValue)); 12 | } 13 | 14 | public static TValue GetValueOrDefault(this IReadOnlyDictionary dictionary, TKey key, TValue defaultValue) 15 | { 16 | if (dictionary == null) 17 | { 18 | throw new ArgumentNullException(nameof(dictionary)); 19 | } 20 | 21 | TValue value; 22 | return dictionary.TryGetValue(key, out value) ? value : defaultValue; 23 | } 24 | 25 | public static bool TryAdd(this IDictionary dictionary, TKey key, TValue value) 26 | { 27 | if (dictionary == null) 28 | { 29 | throw new ArgumentNullException(nameof(dictionary)); 30 | } 31 | 32 | if (!dictionary.ContainsKey(key)) 33 | { 34 | dictionary.Add(key, value); 35 | return true; 36 | } 37 | 38 | return false; 39 | } 40 | 41 | public static bool Remove(this IDictionary dictionary, TKey key, out TValue value) 42 | { 43 | if (dictionary == null) 44 | { 45 | throw new ArgumentNullException(nameof(dictionary)); 46 | } 47 | 48 | if (dictionary.TryGetValue(key, out value)) 49 | { 50 | dictionary.Remove(key); 51 | return true; 52 | } 53 | 54 | value = default(TValue); 55 | return false; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuApiClient/ThreadSafeRandom.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security.Cryptography; 3 | using System.Threading; 4 | 5 | namespace Bleatingsheep.OsuMixedApi 6 | { 7 | internal class ThreadSafeRandom : Random 8 | { 9 | //This is the seed provider 10 | private static readonly RNGCryptoServiceProvider _global = new RNGCryptoServiceProvider(); 11 | 12 | //This is the provider of randomness. 13 | //There is going to be one instance of Random per thread 14 | //because it is declared as ThreadLocal 15 | private readonly ThreadLocal _local = new ThreadLocal(() => 16 | { 17 | //This is the valueFactory function 18 | //This code will run for each thread to initialize each independent instance of Random. 19 | var buffer = new byte[4]; 20 | //Calls the GetBytes method for RNGCryptoServiceProvider because this class is thread-safe 21 | //for this usage. 22 | _global.GetBytes(buffer); 23 | //Return the new thread-local Random instance initialized with the generated seed. 24 | return new Random(BitConverter.ToInt32(buffer, 0)); 25 | }); 26 | 27 | public override int Next() 28 | { 29 | return _local.Value.Next(); 30 | } 31 | 32 | public override int Next(int maxValue) 33 | { 34 | return _local.Value.Next(maxValue); 35 | } 36 | 37 | public override int Next(int minValue, int maxValue) 38 | { 39 | return _local.Value.Next(minValue, maxValue); 40 | } 41 | 42 | public override double NextDouble() 43 | { 44 | return _local.Value.NextDouble(); 45 | } 46 | 47 | public override void NextBytes(byte[] buffer) 48 | { 49 | _local.Value.NextBytes(buffer); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuQqBot.Database/Bleatingsheep.OsuQqBot.Database.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | latest 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuQqBot.Database/Migrations/20220805141925_AddUserField.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | using Microsoft.EntityFrameworkCore.Migrations; 4 | 5 | #nullable disable 6 | 7 | namespace Bleatingsheep.OsuQqBot.Database.Migrations 8 | { 9 | public partial class AddUserField : Migration 10 | { 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.CreateTable( 14 | name: "BotUserFields", 15 | columns: table => new 16 | { 17 | UserId = table.Column(type: "bigint", nullable: false), 18 | FieldName = table.Column(type: "text", nullable: false), 19 | Data = table.Column(type: "jsonb", nullable: true), 20 | Version = table.Column(type: "bytea", rowVersion: true, nullable: false) 21 | }, 22 | constraints: table => 23 | { 24 | table.PrimaryKey("PK_BotUserFields", x => new { x.UserId, x.FieldName }); 25 | }); 26 | 27 | migrationBuilder.CreateIndex( 28 | name: "IX_BotUserFields_FieldName", 29 | table: "BotUserFields", 30 | column: "FieldName"); 31 | } 32 | 33 | protected override void Down(MigrationBuilder migrationBuilder) 34 | { 35 | migrationBuilder.DropTable( 36 | name: "BotUserFields"); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuQqBot.Database/Migrations/20220830195158_AddGroupField.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | using Microsoft.EntityFrameworkCore.Migrations; 4 | 5 | #nullable disable 6 | 7 | namespace Bleatingsheep.OsuQqBot.Database.Migrations 8 | { 9 | public partial class AddGroupField : Migration 10 | { 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.CreateTable( 14 | name: "BotGroupFields", 15 | columns: table => new 16 | { 17 | GroupId = table.Column(type: "bigint", nullable: false), 18 | FieldName = table.Column(type: "text", nullable: false), 19 | Data = table.Column(type: "jsonb", nullable: true), 20 | Version = table.Column(type: "bytea", rowVersion: true, nullable: false) 21 | }, 22 | constraints: table => 23 | { 24 | table.PrimaryKey("PK_BotGroupFields", x => new { x.GroupId, x.FieldName }); 25 | }); 26 | 27 | migrationBuilder.CreateIndex( 28 | name: "IX_BotGroupFields_FieldName", 29 | table: "BotGroupFields", 30 | column: "FieldName"); 31 | } 32 | 33 | protected override void Down(MigrationBuilder migrationBuilder) 34 | { 35 | migrationBuilder.DropTable( 36 | name: "BotGroupFields"); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuQqBot.Database/Migrations/20220830202628_FixNullConstraint.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace Bleatingsheep.OsuQqBot.Database.Migrations 7 | { 8 | public partial class FixNullConstraint : Migration 9 | { 10 | protected override void Up(MigrationBuilder migrationBuilder) 11 | { 12 | migrationBuilder.AlterColumn( 13 | name: "Version", 14 | table: "BotUserFields", 15 | type: "bytea", 16 | rowVersion: true, 17 | nullable: true, 18 | oldClrType: typeof(byte[]), 19 | oldType: "bytea", 20 | oldRowVersion: true); 21 | 22 | migrationBuilder.AlterColumn( 23 | name: "Version", 24 | table: "BotGroupFields", 25 | type: "bytea", 26 | rowVersion: true, 27 | nullable: true, 28 | oldClrType: typeof(byte[]), 29 | oldType: "bytea", 30 | oldRowVersion: true); 31 | } 32 | 33 | protected override void Down(MigrationBuilder migrationBuilder) 34 | { 35 | migrationBuilder.AlterColumn( 36 | name: "Version", 37 | table: "BotUserFields", 38 | type: "bytea", 39 | rowVersion: true, 40 | nullable: false, 41 | defaultValue: new byte[0], 42 | oldClrType: typeof(byte[]), 43 | oldType: "bytea", 44 | oldRowVersion: true, 45 | oldNullable: true); 46 | 47 | migrationBuilder.AlterColumn( 48 | name: "Version", 49 | table: "BotGroupFields", 50 | type: "bytea", 51 | rowVersion: true, 52 | nullable: false, 53 | defaultValue: new byte[0], 54 | oldClrType: typeof(byte[]), 55 | oldType: "bytea", 56 | oldRowVersion: true, 57 | oldNullable: true); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuQqBot.Database/Migrations/20221210013516_AddBeatmapInfoCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Bleatingsheep.Osu.ApiClient; 3 | using Microsoft.EntityFrameworkCore.Migrations; 4 | 5 | #nullable disable 6 | 7 | namespace Bleatingsheep.OsuQqBot.Database.Migrations 8 | { 9 | /// 10 | public partial class AddBeatmapInfoCache : Migration 11 | { 12 | /// 13 | protected override void Up(MigrationBuilder migrationBuilder) 14 | { 15 | migrationBuilder.CreateTable( 16 | name: "BeatmapInfoCache", 17 | columns: table => new 18 | { 19 | BeatmapId = table.Column(type: "integer", nullable: false), 20 | Mode = table.Column(type: "integer", nullable: false), 21 | CacheDate = table.Column(type: "timestamp with time zone", nullable: false), 22 | BeatmapInfo = table.Column(type: "jsonb", nullable: false) 23 | }, 24 | constraints: table => 25 | { 26 | table.PrimaryKey("PK_BeatmapInfoCache", x => new { x.BeatmapId, x.Mode }); 27 | }); 28 | } 29 | 30 | /// 31 | protected override void Down(MigrationBuilder migrationBuilder) 32 | { 33 | migrationBuilder.DropTable( 34 | name: "BeatmapInfoCache"); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuQqBot.Database/Migrations/20221223165347_AllowNullBeatmapInfoCache,AddExpirationDate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Bleatingsheep.Osu.ApiClient; 3 | using Microsoft.EntityFrameworkCore.Migrations; 4 | 5 | #nullable disable 6 | 7 | namespace Bleatingsheep.OsuQqBot.Database.Migrations 8 | { 9 | /// 10 | public partial class AllowNullBeatmapInfoCacheAddExpirationDate : Migration 11 | { 12 | /// 13 | protected override void Up(MigrationBuilder migrationBuilder) 14 | { 15 | migrationBuilder.AlterColumn( 16 | name: "BeatmapInfo", 17 | table: "BeatmapInfoCache", 18 | type: "jsonb", 19 | nullable: true, 20 | oldClrType: typeof(BeatmapInfo), 21 | oldType: "jsonb"); 22 | 23 | migrationBuilder.AddColumn( 24 | name: "ExpirationDate", 25 | table: "BeatmapInfoCache", 26 | type: "timestamp with time zone", 27 | nullable: true); 28 | 29 | migrationBuilder.CreateIndex( 30 | name: "IX_BeatmapInfoCache_ExpirationDate", 31 | table: "BeatmapInfoCache", 32 | column: "ExpirationDate"); 33 | } 34 | 35 | /// 36 | protected override void Down(MigrationBuilder migrationBuilder) 37 | { 38 | migrationBuilder.DropIndex( 39 | name: "IX_BeatmapInfoCache_ExpirationDate", 40 | table: "BeatmapInfoCache"); 41 | 42 | migrationBuilder.DropColumn( 43 | name: "ExpirationDate", 44 | table: "BeatmapInfoCache"); 45 | 46 | migrationBuilder.AlterColumn( 47 | name: "BeatmapInfo", 48 | table: "BeatmapInfoCache", 49 | type: "jsonb", 50 | nullable: false, 51 | oldClrType: typeof(BeatmapInfo), 52 | oldType: "jsonb", 53 | oldNullable: true); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuQqBot.Database/Migrations/20230315032711_RemoveOldConcurrencyCheckFieldDueToType.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace Bleatingsheep.OsuQqBot.Database.Migrations 6 | { 7 | /// 8 | public partial class RemoveOldConcurrencyCheckFieldDueToType : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.DropColumn( 14 | name: "Version", 15 | table: "UpdateSchedules"); 16 | 17 | migrationBuilder.DropColumn( 18 | name: "Version", 19 | table: "BotUserFields"); 20 | 21 | migrationBuilder.DropColumn( 22 | name: "Version", 23 | table: "BotGroupFields"); 24 | } 25 | 26 | /// 27 | protected override void Down(MigrationBuilder migrationBuilder) 28 | { 29 | migrationBuilder.AddColumn( 30 | name: "Version", 31 | table: "UpdateSchedules", 32 | type: "bytea", 33 | rowVersion: true, 34 | nullable: true); 35 | 36 | migrationBuilder.AddColumn( 37 | name: "Version", 38 | table: "BotUserFields", 39 | type: "bytea", 40 | rowVersion: true, 41 | nullable: true); 42 | 43 | migrationBuilder.AddColumn( 44 | name: "Version", 45 | table: "BotGroupFields", 46 | type: "bytea", 47 | rowVersion: true, 48 | nullable: true); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuQqBot.Database/Migrations/20230624200901_AddRecommendationPP.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace Bleatingsheep.OsuQqBot.Database.Migrations 6 | { 7 | /// 8 | public partial class AddRecommendationPP : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "xmin", 15 | table: "UpdateSchedules", 16 | type: "xid", 17 | rowVersion: true, 18 | nullable: false, 19 | defaultValue: 0u); 20 | 21 | migrationBuilder.AddColumn( 22 | name: "Performance", 23 | table: "Recommendations", 24 | type: "double precision", 25 | nullable: false, 26 | defaultValue: 0.0); 27 | 28 | migrationBuilder.AddColumn( 29 | name: "xmin", 30 | table: "BotUserFields", 31 | type: "xid", 32 | rowVersion: true, 33 | nullable: false, 34 | defaultValue: 0u); 35 | 36 | migrationBuilder.AddColumn( 37 | name: "xmin", 38 | table: "BotGroupFields", 39 | type: "xid", 40 | rowVersion: true, 41 | nullable: false, 42 | defaultValue: 0u); 43 | } 44 | 45 | /// 46 | protected override void Down(MigrationBuilder migrationBuilder) 47 | { 48 | migrationBuilder.DropColumn( 49 | name: "xmin", 50 | table: "UpdateSchedules"); 51 | 52 | migrationBuilder.DropColumn( 53 | name: "Performance", 54 | table: "Recommendations"); 55 | 56 | migrationBuilder.DropColumn( 57 | name: "xmin", 58 | table: "BotUserFields"); 59 | 60 | migrationBuilder.DropColumn( 61 | name: "xmin", 62 | table: "BotGroupFields"); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuQqBot.Database/Migrations/20230805223750_AddUserPlayRecordId.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 3 | 4 | #nullable disable 5 | 6 | namespace Bleatingsheep.OsuQqBot.Database.Migrations 7 | { 8 | /// 9 | public partial class AddUserPlayRecordId : Migration 10 | { 11 | /// 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.DropPrimaryKey( 15 | name: "PK_UserPlayRecords", 16 | table: "UserPlayRecords"); 17 | 18 | migrationBuilder.AddColumn( 19 | name: "Id", 20 | table: "UserPlayRecords", 21 | type: "bigint", 22 | nullable: false, 23 | defaultValue: 0L) 24 | .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); 25 | 26 | migrationBuilder.AddPrimaryKey( 27 | name: "PK_UserPlayRecords", 28 | table: "UserPlayRecords", 29 | column: "Id"); 30 | 31 | migrationBuilder.CreateIndex( 32 | name: "IX_UserPlayRecords_UserId_Mode_PlayNumber", 33 | table: "UserPlayRecords", 34 | columns: new[] { "UserId", "Mode", "PlayNumber" }); 35 | } 36 | 37 | /// 38 | protected override void Down(MigrationBuilder migrationBuilder) 39 | { 40 | migrationBuilder.DropPrimaryKey( 41 | name: "PK_UserPlayRecords", 42 | table: "UserPlayRecords"); 43 | 44 | migrationBuilder.DropIndex( 45 | name: "IX_UserPlayRecords_UserId_Mode_PlayNumber", 46 | table: "UserPlayRecords"); 47 | 48 | migrationBuilder.DropColumn( 49 | name: "Id", 50 | table: "UserPlayRecords"); 51 | 52 | migrationBuilder.AddPrimaryKey( 53 | name: "PK_UserPlayRecords", 54 | table: "UserPlayRecords", 55 | columns: new[] { "UserId", "Mode", "PlayNumber" }); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuQqBot.Database/Models/BeatmapInfoCacheEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | using Bleatingsheep.Osu; 4 | using Bleatingsheep.Osu.ApiClient; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace Bleatingsheep.OsuQqBot.Database.Models; 8 | #nullable enable 9 | [Index(nameof(ExpirationDate))] 10 | public class BeatmapInfoCacheEntry 11 | { 12 | public int BeatmapId { get; set; } 13 | public Mode Mode { get; set; } 14 | public DateTimeOffset CacheDate { get; set; } 15 | public DateTimeOffset? ExpirationDate { get; set; } 16 | [Column(TypeName = "jsonb")] 17 | public required BeatmapInfo? BeatmapInfo { get; set; } 18 | } 19 | #nullable restore -------------------------------------------------------------------------------- /Bleatingsheep.OsuQqBot.Database/Models/BindingInfo.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | 4 | namespace Bleatingsheep.OsuQqBot.Database.Models 5 | { 6 | /// 7 | /// 一个QQ绑定的osu!账号的数据 8 | /// 9 | public class BindingInfo 10 | { 11 | [Key] 12 | [DatabaseGenerated(DatabaseGeneratedOption.None)] 13 | public long UserId { get; set; } 14 | [ConcurrencyCheck] 15 | public int OsuId { get; set; } 16 | [Required] 17 | public string Source { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuQqBot.Database/Models/BotGroupField.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.Text.Json; 4 | 5 | namespace Bleatingsheep.OsuQqBot.Database.Models; 6 | #nullable enable 7 | public sealed class BotGroupField : IDisposable 8 | { 9 | public long GroupId { get; set; } 10 | public string FieldName { get; set; } = null!; 11 | public JsonDocument? Data { get; set; } 12 | [Timestamp] 13 | public uint Version { get; set; } 14 | 15 | public void Dispose() 16 | { 17 | Data?.Dispose(); 18 | } 19 | } 20 | #nullable restore -------------------------------------------------------------------------------- /Bleatingsheep.OsuQqBot.Database/Models/BotUserField.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.Text.Json; 4 | 5 | namespace Bleatingsheep.OsuQqBot.Database.Models; 6 | #nullable enable 7 | public sealed class BotUserField : IDisposable 8 | { 9 | public long UserId { get; set; } 10 | public string FieldName { get; set; } = null!; 11 | public JsonDocument? Data { get; set; } 12 | [Timestamp] 13 | public uint Version { get; set; } 14 | 15 | public void Dispose() 16 | { 17 | Data?.Dispose(); 18 | } 19 | } 20 | #nullable restore -------------------------------------------------------------------------------- /Bleatingsheep.OsuQqBot.Database/Models/DuplicateAuthentication.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace Bleatingsheep.OsuQqBot.Database.Models 6 | { 7 | [Index(nameof(SelfId), IsUnique = true)] 8 | public class DuplicateAuthentication 9 | { 10 | public int Id { get; set; } 11 | public long SelfId { get; set; } 12 | [Required] 13 | public string AccessToken { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuQqBot.Database/Models/MessageEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace Bleatingsheep.OsuQqBot.Database.Models 5 | { 6 | public class MessageEntry 7 | { 8 | public virtual int Id { get; set; } 9 | public virtual DateTimeOffset Date { get; set; } 10 | public virtual long GroupId { get; set; } 11 | public virtual long UserId { get; set; } 12 | [Required] 13 | public virtual string Raw { get; set; } 14 | [Required] 15 | public virtual string Text { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuQqBot.Database/Models/OperationHistory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace Bleatingsheep.OsuQqBot.Database.Models 5 | { 6 | public class OperationHistory 7 | { 8 | private DateTime _date = DateTime.UtcNow; 9 | 10 | public int Id { get; private set; } 11 | public long UserId { get; set; } 12 | public string User { get; set; } 13 | public Operation Operation { get; set; } 14 | public long? OperatorId { get; set; } 15 | public string Operator { get; set; } 16 | 17 | public DateTime Date 18 | { 19 | get => _date; 20 | set => _date = value.ToUniversalTime(); 21 | } 22 | 23 | [Required] 24 | public string Remark { get; set; } 25 | } 26 | 27 | public enum Operation 28 | { 29 | Requesting = 0, 30 | Joining = 1, 31 | Binding = 2, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuQqBot.Database/Models/PlayRecordQueryTemp.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | using Bleatingsheep.Osu; 4 | 5 | namespace Bleatingsheep.OsuQqBot.Database.Models 6 | { 7 | public class PlayRecordQueryTemp 8 | { 9 | [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 10 | public Guid Id { get; set; } 11 | public int UserId { get; set; } 12 | public Mode Mode { get; set; } 13 | public int StartNumber { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuQqBot.Database/Models/PlusHistory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Bleatingsheep.Osu.PerformancePlus; 3 | 4 | namespace Bleatingsheep.OsuQqBot.Database.Models 5 | { 6 | public class PlusHistory : UserPlus 7 | { 8 | private PlusHistory() 9 | { 10 | } 11 | 12 | public PlusHistory(IUserPlus history) 13 | { 14 | Id = history.Id; 15 | Name = history.Name; 16 | Performance = history.Performance; 17 | AimTotal = history.AimTotal; 18 | AimJump = history.AimJump; 19 | AimFlow = history.AimFlow; 20 | Precision = history.Precision; 21 | Speed = history.Speed; 22 | Stamina = history.Stamina; 23 | Accuracy = history.Accuracy; 24 | } 25 | 26 | public DateTimeOffset Date { get; private set; } = DateTimeOffset.UtcNow; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuQqBot.Database/Models/RecommendationEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Bleatingsheep.Osu; 4 | using Bleatingsheep.Osu.ApiClient; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using static Bleatingsheep.Osu.Mods; 8 | 9 | namespace Bleatingsheep.OsuQqBot.Database.Models 10 | { 11 | public class RecommendationEntry 12 | { 13 | public int Id { get; set; } 14 | public Mode Mode { get; set; } 15 | public RecommendationBeatmapId Left { get; set; } 16 | public RecommendationBeatmapId Recommendation { get; set; } 17 | public double RecommendationDegree { get; set; } 18 | public double Performance { get; set; } 19 | } 20 | 21 | [Owned] 22 | public sealed class RecommendationBeatmapId : IEquatable, IComparable 23 | { 24 | private static readonly IList s_modFilters = new Mods[4] 25 | { 26 | DoubleTime | HalfTime | Easy | HardRock | Hidden | Flashlight | TouchDevice, 27 | DoubleTime, 28 | DoubleTime, 29 | DoubleTime, 30 | }; 31 | 32 | public static readonly ValueConverter ValueConverter = new( 33 | v => ((long)v.BeatmapId << 32) | (long)v.ValidMods, 34 | v => new RecommendationBeatmapId 35 | { 36 | BeatmapId = (int)(v >> 32), 37 | ValidMods = (Mods)(v & 0xffffff), 38 | }); 39 | 40 | public static RecommendationBeatmapId Create(UserBest best, Mode mode) 41 | { 42 | return new RecommendationBeatmapId 43 | { 44 | BeatmapId = best.BeatmapId, 45 | ValidMods = best.EnabledMods & s_modFilters[(int)mode], 46 | }; 47 | } 48 | 49 | public int BeatmapId { get; set; } 50 | public Mods ValidMods { get; set; } 51 | 52 | public override bool Equals(object obj) => Equals(obj as RecommendationBeatmapId); 53 | public bool Equals(RecommendationBeatmapId other) => other != null && BeatmapId == other.BeatmapId && ValidMods == other.ValidMods; 54 | public override int GetHashCode() => HashCode.Combine(BeatmapId, ValidMods); 55 | 56 | public int CompareTo(RecommendationBeatmapId other) 57 | { 58 | if (other is null) 59 | return 1; 60 | if (ReferenceEquals(this, other)) 61 | return 0; 62 | return (BeatmapId.CompareTo(other.BeatmapId), ValidMods.CompareTo(other.ValidMods)) switch 63 | { 64 | ( > 0, _) => 1, 65 | ( < 0, _) => -1, 66 | (_, > 0) => 1, 67 | (_, < 0) => -1, 68 | _ => 0, 69 | }; 70 | } 71 | 72 | public static bool operator ==(RecommendationBeatmapId left, RecommendationBeatmapId right) => EqualityComparer.Default.Equals(left, right); 73 | public static bool operator !=(RecommendationBeatmapId left, RecommendationBeatmapId right) => !(left == right); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuQqBot.Database/Models/RelationshipInfo.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Bleatingsheep.OsuQqBot.Database.Models 4 | { 5 | public class RelationshipInfo 6 | { 7 | public long UserId { get; set; } 8 | public string Relationship { get; set; } 9 | [ConcurrencyCheck] 10 | public int Target { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuQqBot.Database/Models/UpdateSchedule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using Bleatingsheep.Osu; 4 | 5 | namespace Bleatingsheep.OsuQqBot.Database.Models 6 | { 7 | public class UpdateSchedule 8 | { 9 | public int UserId { get; set; } 10 | public Mode Mode { get; set; } 11 | public DateTimeOffset NextUpdate { get; set; } 12 | public int ActiveIndex { get; set; } 13 | [Timestamp] 14 | public uint Version { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuQqBot.Database/Models/UserPlayRecord.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Bleatingsheep.Osu.ApiClient; 3 | using Microsoft.EntityFrameworkCore; 4 | using Mode = Bleatingsheep.Osu.Mode; 5 | 6 | namespace Bleatingsheep.OsuQqBot.Database.Models 7 | { 8 | [Index(nameof(UserId), nameof(Mode), nameof(PlayNumber))] 9 | public class UserPlayRecord 10 | { 11 | public long Id { get; set; } 12 | public int UserId { get; set; } 13 | public int PlayNumber { get; set; } 14 | public Mode Mode { get; set; } 15 | [Required] 16 | public UserRecent Record { get; set; } 17 | 18 | public static UserPlayRecord Create(int osuId, Mode mode, int playNumber, UserRecent userRecent) 19 | { 20 | return new UserPlayRecord 21 | { 22 | UserId = osuId, 23 | Mode = mode, 24 | PlayNumber = playNumber, 25 | Record = userRecent, 26 | }; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuQqBot.Database/Models/UserSnapshot.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | using Bleatingsheep.Osu; 5 | using Bleatingsheep.Osu.ApiClient; 6 | 7 | namespace Bleatingsheep.OsuQqBot.Database.Models 8 | { 9 | public class UserSnapshot 10 | { 11 | public long Id { get; set; } 12 | public int UserId { get; set; } 13 | public Mode Mode { get; set; } 14 | [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 15 | public DateTimeOffset Date { get; set; } = DateTimeOffset.UtcNow; 16 | [Required] 17 | public UserInfo UserInfo { get; set; } 18 | 19 | public static UserSnapshot Create(int osuId, Mode mode, UserInfo userInfo) 20 | { 21 | return new UserSnapshot 22 | { 23 | UserId = osuId, 24 | Mode = mode, 25 | UserInfo = userInfo, 26 | }; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Bleatingsheep.OsuQqBot.Database/Models/WebLog.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Text; 5 | 6 | namespace Bleatingsheep.OsuQqBot.Database.Models 7 | { 8 | public class WebLog 9 | { 10 | public long Id { get; set; } 11 | 12 | public string User { get; set; } 13 | 14 | public string Token { get; set; } 15 | 16 | public DateTimeOffset Time { get; set; } = DateTimeOffset.UtcNow; 17 | 18 | [Required] 19 | public string IPAddress { get; set; } 20 | 21 | public string Location { get; set; } 22 | 23 | [Required] 24 | public string Kind { get; set; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /NewHydrantApi/Controllers/BiliLiveAddController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Newtonsoft.Json; 9 | using Newtonsoft.Json.Linq; 10 | 11 | namespace NewHydrantApi.Controllers 12 | { 13 | [Route("api/[controller]")] 14 | [ApiController] 15 | public class BiliLiveAddController : ControllerBase 16 | { 17 | [HttpGet("{roomId}")] 18 | public async Task> GetStreamAddress(int roomId) 19 | { 20 | // 检测指定参数错误 21 | if (roomId <= 0) 22 | { 23 | return BadRequest(); 24 | } 25 | 26 | using (var httpClient = new HttpClient()) 27 | { 28 | var json = await httpClient.GetStringAsync($"https://api.live.bilibili.com/room/v1/Room/playUrl?cid={roomId}&otype=json&quality=0&platform=web"); 29 | var obj = JsonConvert.DeserializeObject(json); 30 | return obj?["data"]?["durl"]?[0]?["url"]?.ToObject(); 31 | } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /NewHydrantApi/Controllers/BindingController.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Threading.Tasks; 3 | using Bleatingsheep.OsuQqBot.Database.Models; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace NewHydrantApi.Controllers 9 | { 10 | [Route("api/[controller]")] 11 | [ApiController] 12 | public class BindingController : ControllerBase 13 | { 14 | // Note that this is a legacy class, which is different with NewbieContext. 15 | private readonly NewbieContext _dbContext; 16 | 17 | private readonly ILogger _logger; 18 | 19 | public BindingController(ILogger logger, NewbieContext newbieContext) 20 | { 21 | _logger = logger; 22 | _dbContext = newbieContext; 23 | } 24 | 25 | [HttpGet("{qq}", Name = "GetBinding")] 26 | public async Task> GetByQq(long qq) 27 | { 28 | BindingInfo result = null; 29 | try 30 | { 31 | result = await _dbContext.Bindings.SingleOrDefaultAsync(b => b.UserId == qq).ConfigureAwait(false); 32 | if (result != null) 33 | return result; 34 | return NotFound(); 35 | } 36 | finally 37 | { 38 | _logger.LogInformation("来自 [{0}] 请求查询 {1},结果为 {2},结果来源 {3}", 39 | HttpContext.Connection.RemoteIpAddress, 40 | qq.ToString(CultureInfo.InvariantCulture), 41 | result?.OsuId.ToString(CultureInfo.InvariantCulture) ?? "null", 42 | result?.Source); 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /NewHydrantApi/Controllers/IPController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Mvc; 8 | 9 | namespace NewHydrantApi.Controllers 10 | { 11 | [Route("api/[controller]")] 12 | [ApiController] 13 | public class IPController : ControllerBase 14 | { 15 | private readonly IHttpContextAccessor _httpContextAccessor; 16 | 17 | public IPController(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor; 18 | 19 | [HttpGet] 20 | public ActionResult AutoRoute(string ip) 21 | { 22 | try 23 | { 24 | var isEmpty = string.IsNullOrWhiteSpace(ip); 25 | IPAddress ipAddress = isEmpty 26 | ? HttpContext.Connection.RemoteIpAddress ?? IPAddress.IPv6None 27 | : Bleatingsheep.IPLocation.IPv6Locator.GetPureIP(IPAddress.Parse(ip)); 28 | 29 | HttpContext.Response.Redirect(ipAddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork 30 | ? $"https://www.ipip.net/ip/{ipAddress}.html" 31 | : $"https://ip.zxinc.org/ipquery/?ip={System.Web.HttpUtility.UrlEncode(ipAddress.ToString())}", !isEmpty); 32 | } 33 | catch (Exception) 34 | { 35 | HttpContext.Response.Redirect(string.IsNullOrWhiteSpace(ip) ? "https://www.ipip.net/ip.html" : $"https://www.ipip.net/ip/{ip}.html", true); 36 | } 37 | return string.Empty; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /NewHydrantApi/Controllers/MyIPController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.AspNetCore.Mvc; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace NewHydrantApi.Controllers 12 | { 13 | [Route("api/[controller]")] 14 | [ApiController] 15 | public class MyIPController : ControllerBase 16 | { 17 | private readonly IHttpContextAccessor _httpContextAccessor; 18 | private readonly ILogger _logger; 19 | 20 | public MyIPController(IHttpContextAccessor httpContextAccessor, ILogger logger) 21 | { 22 | _httpContextAccessor = httpContextAccessor; 23 | _logger = logger; 24 | } 25 | 26 | [HttpGet] 27 | public ActionResult Get() 28 | { 29 | _logger.LogInformation(new EventId(12, "myipevent"), "get myip"); 30 | _logger.LogInformation("get myip pure"); 31 | _logger.LogInformation("access from {0}", HttpContext.Connection.RemoteIpAddress); 32 | 33 | var sb = new StringBuilder(); 34 | sb.AppendLine("-----direct-----"); 35 | 36 | var connection = HttpContext.Connection; 37 | sb.AppendLine(new IPEndPoint(connection.RemoteIpAddress ?? IPAddress.IPv6None, connection.RemotePort).ToString()); 38 | 39 | var request = HttpContext.Request; 40 | sb.AppendLine(request.Scheme); 41 | sb.AppendLine(request.Host.ToString()); 42 | sb.AppendLine(request.Protocol); 43 | sb.AppendLine(request.IsHttps.ToString()); 44 | sb.AppendLine(request.Method); 45 | 46 | sb.AppendLine("-----forwarded-----"); 47 | 48 | var headers = request.Headers; 49 | sb.AppendLine(headers["X-Forwarded-For"]); 50 | sb.AppendLine(headers["X-Forwarded-Proto"]); 51 | sb.AppendLine(headers["X-Forwarded-Host"]); 52 | 53 | sb.AppendLine("-----original-----"); 54 | sb.AppendLine(headers["X-Original-For"]); 55 | sb.AppendLine(headers["X-Original-Proto"]); 56 | sb.AppendLine(headers["X-Original-Host"]); 57 | 58 | return sb.ToString(); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /NewHydrantApi/Controllers/UserPlays.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Bleatingsheep.Osu; 5 | using Bleatingsheep.OsuQqBot.Database.Models; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.EntityFrameworkCore; 8 | 9 | namespace NewHydrantApi.Controllers 10 | { 11 | [Route("api/[controller]")] 12 | [ApiController] 13 | public class UserPlays : ControllerBase 14 | { 15 | private readonly NewbieContext _context; 16 | 17 | public UserPlays(NewbieContext context) 18 | { 19 | _context = context; 20 | } 21 | 22 | /// 23 | /// Get user play records. 24 | /// 25 | /// User ID. 26 | /// Mode to query. An integer from 0 to 3. 27 | /// Query play records starting at this number. 28 | /// Maximum count in results. 29 | /// Play records. 30 | [HttpGet] 31 | public async Task>> GetUserPlays(int userId, Mode mode, int start, int limit = 100) 32 | { 33 | return await _context.UserPlayRecords 34 | .Where(r => r.UserId == userId && r.Mode == mode && r.PlayNumber >= start && r.PlayNumber < start + 100) 35 | .ToListAsync().ConfigureAwait(false); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /NewHydrantApi/Controllers/UserSnapshotController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Bleatingsheep.Osu; 5 | using Bleatingsheep.OsuQqBot.Database.Models; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.EntityFrameworkCore; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace NewHydrantApi.Controllers; 12 | 13 | [Route("api/[controller]")] 14 | [ApiController] 15 | public class UserSnapshotController : ControllerBase 16 | { 17 | private readonly NewbieContext _dbContext; 18 | private readonly ILogger _logger; 19 | 20 | public UserSnapshotController(NewbieContext dbContext, ILogger logger) 21 | { 22 | _dbContext = dbContext; 23 | _logger = logger; 24 | } 25 | 26 | private static TimeSpan GetError(DateTimeOffset wanted, DateTimeOffset actual) 27 | { 28 | var error = wanted - actual; 29 | if (error < TimeSpan.Zero) 30 | error = -error; 31 | return error; 32 | } 33 | 34 | /// 35 | /// Get the snapshot of user info 24 hours ago. 36 | /// 37 | /// User osu! ID. 38 | /// Game mode. 39 | /// NotFound if the user has no snapshot data in the past 36 hours. If found, the snapshot taken closest to the moment 24 hours ago. 40 | [HttpGet("{userId}", Name = "GetUserInfo")] 41 | public async Task> Get(int userId, Mode mode) 42 | { 43 | // TODO: The code is temporarily copied from QueryHelper.cs. Should refactor later. 44 | DateTimeOffset now = DateTimeOffset.UtcNow; 45 | var comparedDate = now.AddHours(-36); 46 | try 47 | { 48 | var snapshots = await _dbContext.UserSnapshots.AsNoTracking() 49 | .Where(s => s.UserId == userId && s.Mode == mode && s.Date > comparedDate) 50 | .ToListAsync(); 51 | var history = snapshots 52 | .MinBy(s => GetError(now - TimeSpan.FromHours(24), s.Date)); 53 | if (history == null) 54 | { 55 | return NotFound(); 56 | } 57 | return history; 58 | } 59 | catch (Exception e) 60 | { 61 | _logger.LogError(e, "查询用户最新快照时失败。"); 62 | return StatusCode(StatusCodes.Status500InternalServerError); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /NewHydrantApi/NewHydrantApi.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | d0e2233d-0e2d-4989-a443-69e7fe448d44 6 | warnings 7 | CS8600;CS8602;CS8603 8 | 9 | 10 | 11 | true 12 | $(NoWarn);1591 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Always 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /NewHydrantApi/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Hosting; 4 | using Microsoft.Extensions.Logging; 5 | using NLog; 6 | using NLog.Web; 7 | 8 | namespace NewHydrantApi 9 | { 10 | public class Program 11 | { 12 | public static void Main(string[] args) 13 | { 14 | var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger(); 15 | try 16 | { 17 | logger.Debug("init main"); 18 | CreateHostBuilder(args).Build().Run(); 19 | } 20 | catch (Exception ex) 21 | { 22 | //NLog: catch setup errors 23 | logger.Error(ex, "Stopped program because of exception"); 24 | throw; 25 | } 26 | finally 27 | { 28 | // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux) 29 | NLog.LogManager.Shutdown(); 30 | } 31 | } 32 | 33 | public static IHostBuilder CreateHostBuilder(string[] args) => 34 | Host.CreateDefaultBuilder(args) 35 | .ConfigureWebHostDefaults(webBuilder => 36 | { 37 | webBuilder 38 | .UseStartup(); 39 | }) 40 | .ConfigureLogging(logging => 41 | { 42 | logging.ClearProviders(); 43 | logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace); 44 | }) 45 | .UseNLog(); // NLog: setup NLog for Dependency injection 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /NewHydrantApi/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /NewHydrantApi/appsettings.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Trace", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "ConnectionStrings": { 11 | "NewbieDatabase": "server=x;port=123;database=db;user=u;pwd=pw;" 12 | } 13 | } -------------------------------------------------------------------------------- /NewHydrantApi/nlog.config: -------------------------------------------------------------------------------- 1 |  2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # osu! 新人群 Bot 2 | 为 osu! 新人群提供各种功能服务,在其他群也可以使用一些基本的功能。 3 | 4 | > **Warning** 5 | > 6 | > 如果你只是想使用消防栓,可以用自己的账号创建一个分身,参考[消防栓分身](https://xfs.b11p.com/fenshen/)。 7 | > 8 | > 本页内容仅在贡献代码或二次开发时才需要。请注意遵守开源协议。 9 | 10 | ## 环境及依赖 (Prerequisites) 11 | - .NET 7.0 12 | - [go-cqhttp](https://github.com/Mrs4s/go-cqhttp) 或任何 OneBot v11 实现 13 | - PostgreSQL 14 | - Chromium-based browser 15 | 16 | ## 本地开发及调试 (Development) 17 | ### 开发环境 (Environments) 18 | 1. 配置 Prerequisites 中所需的工具。注意 .NET 必须安装 SDK。 19 | 2. 安装最新版本的 Visual Studio,确保选中 “.NET Core 跨平台开发”。 20 | 21 | ### 克隆 (Clone) 22 | 1. 克隆本 repo。 23 | 24 | ### 修改配置文件 (Configuration) 25 | 1. 将“Bleatingsheep.NewHydrant.Bot/appsettings.json.template”文件复制在相同目录下,重命名为“appsettings.json”。 26 | 2. 编辑“appsettings.json”,将 `NewbieDatabase_Postgres` 修改为 PostgreSQL 的[连接字符串](https://www.connectionstrings.com/npgsql/)。 27 | 3. 将 `ApiKey` 修改为 osu! API v1 key。 28 | 4. 将 `SuperAdmin` 修改为你的 QQ 号(非 bot 账号,该账号具有最高权限)。 29 | 5. 将 `ServerPort` 修改为反向 ws 监听端口。 30 | 6. 将 `Chrome` 下的 `Path` 修改为 Chrome 浏览器的路径,如果未正确设置,部分功能可能无法使用。 31 | 32 | #### 参考 (Reference) 33 | > [go-cqhttp 的配置文件](https://github.com/Mrs4s/go-cqhttp/blob/master/docs/config.md) 34 | 35 | ### 本地运行及测试 (Test) 36 | 尝试编译`Bleatingsheep.NewHydrant.Bot`项目,根据提示消除编译错误。 37 | 38 | 使用 go-cqhttp 的“反向 WebSocket”模式连接 `ServerPort` 中配置的端口。 39 | 40 | ## 部署 (Deployment) 41 | 接下来将说明如果部署至生产环境。请注意本文只提供基本方法,关于你服务器上的 42 | 43 | ### 服务器准备 (Server Environments) 44 | 1. 首先安装 Prerequisites 环境,.NET 安装 runtime 即可。 45 | 46 | ### 编译 (Compilation) 47 | 接下来将编译 48 | 49 | 1. 右键单击 `Bleatingsheep.NewHydrant.Bot` 项目,点“发布”。 50 | 2. 目标选“文件夹”,然后选择合适的文件夹。 51 | 3. 发布后,把该文件夹的文件全部复制到服务器上。 52 | 53 | 如果使用命令行,则运行以下命令(和上面的步骤二选一): 54 | 55 | ```sh 56 | dotnet publish Bleatingsheep.NewHydrant.Bot -c Release -o 57 | ``` 58 | 59 | ### 配置 60 | 1. 创建“appsettings.json”文件,并按上方相同方法配置。
此文件应该与“Bleatingsheep.NewHydrant.Bot.dll”放在同一目录。 61 | 2. 如果希望提供公共服务,将 `ServerAccessToken` 修改为要求客户端(OneBot 实现)设置的 Token。 62 | 63 | ### 添加账号权限 64 | 先在服务器上运行一次,创建数据库结构,然后连接数据库,在 `DuplicateAuthentication` 表中添加 Bot 账号以及 AccessToken,请注意务必与公开 Token 不同。该账号使用此 Token 将具有高权限。 65 | 66 | ### 运行 (Run) 67 | 在服务器上打开 Powershell(或 bash 等任何 shell),运行 68 | ```Powershell 69 | dotnet Bleatingsheep.NewHydrant.Bot.dll 70 | ``` 71 | 72 | 要连接 go-cqhttp,请使用“反向 WebSocket”模式,并按照数据库中的 Token 进行配置。 73 | 74 | ## 高权限和低权限有什么区别? 75 | 高权限的 Bot 账号可以使用绑定功能,低权限没有,这是为了防止伪造绑定和管理请求。 76 | 77 | ## 开源协议 (LICENSE) 78 | 项目主体部分使用 AGPL 协议授权,框架部分(Bleatingsheep.NewHydrant 文件夹)使用 MIT 协议授权。 79 | 80 | ### 其他项目 81 | 本项目可能用到了其他项目的代码,遵守其协议,在此列出。 82 | 83 | |链接|协议| 84 | |-|-| 85 | |https://github.com/dotnet/runtime|MIT| -------------------------------------------------------------------------------- /README_resources/binding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b11p/OsuQqBotForNewbieGroup/339665cbca6b3120f8530790a323c38c493ad321/README_resources/binding.png -------------------------------------------------------------------------------- /README_resources/entrance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b11p/OsuQqBotForNewbieGroup/339665cbca6b3120f8530790a323c38c493ad321/README_resources/entrance.png -------------------------------------------------------------------------------- /README_resources/highlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b11p/OsuQqBotForNewbieGroup/339665cbca6b3120f8530790a323c38c493ad321/README_resources/highlight.png -------------------------------------------------------------------------------- /README_resources/ppplus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b11p/OsuQqBotForNewbieGroup/339665cbca6b3120f8530790a323c38c493ad321/README_resources/ppplus.png -------------------------------------------------------------------------------- /README_resources/pptth-response.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b11p/OsuQqBotForNewbieGroup/339665cbca6b3120f8530790a323c38c493ad321/README_resources/pptth-response.png -------------------------------------------------------------------------------- /README_resources/pptth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b11p/OsuQqBotForNewbieGroup/339665cbca6b3120f8530790a323c38c493ad321/README_resources/pptth.png -------------------------------------------------------------------------------- /README_resources/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b11p/OsuQqBotForNewbieGroup/339665cbca6b3120f8530790a323c38c493ad321/README_resources/profile.png -------------------------------------------------------------------------------- /Tests.Database/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Bleatingsheep.OsuQqBot.Database.Models; 6 | using Microsoft.EntityFrameworkCore; 7 | 8 | namespace Tests.Database 9 | { 10 | internal static class Program 11 | { 12 | static void Main(string[] args) 13 | { 14 | using var newbieContext = new NewbieContext(); 15 | // This works, w/o any Update() statements. 16 | //var first = newbieContext.UpdateSchedules.First(); 17 | //first.ActiveIndex++; 18 | //newbieContext.SaveChanges(); 19 | using var newbieContext2 = new NewbieContext(); 20 | var strategy1 = newbieContext.Database.CreateExecutionStrategy(); 21 | strategy1.Execute(() => 22 | { 23 | using var transaction1 = newbieContext.Database.BeginTransaction(IsolationLevel.ReadCommitted); 24 | using var transaction2 = newbieContext2.Database.BeginTransaction(IsolationLevel.ReadCommitted); 25 | var first = newbieContext.UpdateSchedules.First(); 26 | first.ActiveIndex += 10; 27 | var first2 = newbieContext2.UpdateSchedules.First(); 28 | first2.ActiveIndex += 20; 29 | newbieContext2.SaveChanges(); 30 | Task.Run(() => 31 | { 32 | Task.Delay(10_000).Wait(); 33 | transaction2.Commit(); 34 | }); 35 | newbieContext.SaveChanges(); // Throws? 36 | }); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tests.Database/Tests.Database.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | latest 7 | enable 8 | CS8600;CS8602;CS8603;CS8618 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /UnitTests/ApiUnitTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Xunit; 4 | 5 | namespace UnitTests 6 | { 7 | public class ApiUnitTest 8 | { 9 | [Fact] 10 | public void PlusMapTest() 11 | { 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /UnitTests/IncrementFormatTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Bleatingsheep.NewHydrant.Utilities; 5 | using Xunit; 6 | 7 | namespace UnitTests 8 | { 9 | public class IncrementFormatTests 10 | { 11 | 12 | [Theory] 13 | [InlineData(0, "")] 14 | [InlineData(0.03, " (+3%)")] 15 | [InlineData(0.001, " (+.1%)")] 16 | [InlineData(0.00001, " (+)")] 17 | [InlineData(-0.9, " (-90%)")] 18 | [InlineData(-0.001, " (-.1%)")] 19 | [InlineData(-0.00015, " (-.02%)")] 20 | [InlineData(-0.00004, " (-)")] 21 | [InlineData(-0.00005, " (-.01%)")] 22 | public void PercentageTests(double percent, string expected) 23 | => Assert.Equal(expected, IncrementUtility.FormatIncrementPercentage(percent)); 24 | 25 | [Theory] 26 | [InlineData(0, "")] 27 | [InlineData(1, " (↓1)")] 28 | [InlineData(-1, " (↑1)")] 29 | public void DifferentSymbolTests(double increment, string expected) 30 | => Assert.Equal(expected, IncrementUtility.FormatIncrement(increment, '↓', '↑')); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /UnitTests/RegexTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Text.RegularExpressions; 5 | using Bleatingsheep.NewHydrant.Attributions; 6 | using Bleatingsheep.NewHydrant.Core; 7 | using Xunit; 8 | 9 | namespace UnitTests 10 | { 11 | public class RegexTest 12 | { 13 | [Fact] 14 | public void NullTest() 15 | { 16 | Assert.Throws(() => 17 | { 18 | new NullService().Test(""); 19 | }); 20 | } 21 | 22 | [Fact] 23 | public void DuplicateTest() 24 | { 25 | Assert.Throws(() => 26 | { 27 | new DuplicateService().Test(""); 28 | }); 29 | } 30 | 31 | [Fact] 32 | public void CompTest() 33 | { 34 | var service = new MyService(); 35 | var text = "3.5|this is text|467"; 36 | 37 | var result = service.Test(text); 38 | 39 | Assert.True(result); 40 | Assert.Equal(3.5, service.Real); 41 | Assert.Equal("this is text", service.TextProperty); 42 | Assert.Equal(467, service.MyProperty); 43 | } 44 | 45 | public class MyService : Service 46 | { 47 | [Parameter("1")] 48 | public int MyProperty { get; set; } 49 | 50 | [Parameter("t")] 51 | public string TextProperty { get; set; } 52 | 53 | [Parameter("r")] 54 | public double Real { get; set; } 55 | 56 | public bool Test(string text) 57 | { 58 | return RegexCommand(new Regex(@"^(?.+?)\|(?.+)\|(.+?)$"), text); 59 | } 60 | } 61 | 62 | public class NullService : Service 63 | { 64 | [Parameter(null)] 65 | public int MyProperty { get; set; } 66 | 67 | public bool Test(string text) 68 | { 69 | return RegexCommand(new Regex(""), text); 70 | } 71 | } 72 | 73 | public class DuplicateService : Service 74 | { 75 | [Parameter("ss")] 76 | public int MyProperty { get; set; } 77 | 78 | [Parameter("ss")] 79 | public int MyProperty2 { get; set; } 80 | 81 | public bool Test(string text) 82 | { 83 | return RegexCommand(new Regex(""), text); 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /UnitTests/UnitTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /publish.ps1: -------------------------------------------------------------------------------- 1 | dotnet build -c Release .\Bleatingsheep.NewHydrant.Bot\Bleatingsheep.NewHydrant.Bot.csproj 2 | dotnet publish --no-build -c Release -o $HOME/Desktop/newhydrant_publish/ .\Bleatingsheep.NewHydrant.Bot\Bleatingsheep.NewHydrant.Bot.csproj --------------------------------------------------------------------------------