├── .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
--------------------------------------------------------------------------------