├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── DocumentationProject ├── Content │ └── Welcome.aml ├── ContentLayout.content ├── DocumentationProject.shfbproj ├── icons │ └── Help.png └── images │ └── wcl-generator-classes.png ├── LICENSE.txt ├── NOTICE.txt ├── Playground ├── GeoSearch.workbook ├── README.md └── Rolling in the files.workbook ├── README.md ├── Samples ├── ConsoleTestApplication1 │ ├── ConsoleTestApplication1.csproj │ └── Program.cs ├── LinqToCargo │ ├── LinqToCargo.csproj │ └── Program.cs ├── ScribuntoInteractive │ ├── Program.cs │ └── ScribuntoInteractive.csproj └── WpfTestApplication1 │ ├── App.xaml │ ├── App.xaml.cs │ ├── MainWindow.xaml │ ├── MainWindow.xaml.cs │ ├── Properties │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ ├── Resources.resx │ ├── Settings.Designer.cs │ └── Settings.settings │ ├── WikiPageTemplate.html │ └── WpfTestApplication1.csproj ├── UnitTestProject1 ├── Assembly.cs ├── Attributes.cs ├── CredentialManager.cs ├── DemoImages │ ├── 1.jpg │ ├── 1.txt │ ├── 2.jpg │ └── 2.txt ├── Endpoints.cs ├── Fixtures │ └── WikiSiteProvider.cs ├── GlobalSuppressions.cs ├── Properties │ ├── Resources.Designer.cs │ ├── Resources.resx │ └── WikibaseP3.json ├── TestOutputLogger.cs ├── Tests │ ├── AbuseFilterTests.cs │ ├── CargoTests.cs │ ├── FlowTests.cs │ ├── GeneratorTests1.cs │ ├── GeneratorTests2.cs │ ├── InfrastructureTests.cs │ ├── MediaWikiVersionTests.cs │ ├── PageHelperTests.cs │ ├── PageTests.cs │ ├── PageTestsDirty.cs │ ├── RenderingTests.cs │ ├── RevisionGeneratorTests.cs │ ├── ScribuntoTests.cs │ ├── SiteTests.cs │ ├── SiteTokenTests.cs │ ├── ValidationTests.cs │ ├── WebClientTests.cs │ ├── WikiClientHelperTests.cs │ ├── WikiFamilyTests.cs │ ├── WikiLinkTests.cs │ ├── WikiaApiTests.cs │ └── WikibaseTests.cs ├── UnitTestProject1.csproj ├── Utility.cs └── WikiSiteTestsBase.cs ├── WikiClientLibrary.Cargo ├── CargoQueryParameters.cs ├── CargoWikiSiteExtensions.cs ├── Linq │ ├── CargoFunctions.cs │ ├── CargoModel.cs │ ├── CargoModelProperty.cs │ ├── CargoModelUtility.cs │ ├── CargoQueryContext.cs │ ├── CargoQueryProvider.cs │ ├── CargoRecordConverter.cs │ ├── CargoRecordQueryable.cs │ ├── CargoRecordQueryableExtensions.cs │ ├── CargoRecordSet.cs │ ├── ExpressionVisitors │ │ ├── CargoPostEvaluationTranslator.cs │ │ ├── CargoPreEvaluationTranslator.cs │ │ ├── CargoQueryClauseBuilder.cs │ │ ├── CargoQueryExpressionReducer.cs │ │ ├── ExpressionEqualityComparer.cs │ │ ├── ExpressionTreePartialEvaluator.cs │ │ ├── ExpressionVisitorUtility.cs │ │ └── MemberAccessExpressionReplacer.cs │ └── IntermediateExpressions │ │ ├── CargoBinaryOperationExpression.cs │ │ ├── CargoFunctionExpression.cs │ │ ├── CargoQueryExpression.cs │ │ ├── CargoSqlExpression.cs │ │ ├── FieldRefExpression.cs │ │ ├── OrderByExpression.cs │ │ ├── ProjectionExpression.cs │ │ └── TableProjectionExpression.cs ├── Schema │ ├── CargoSpecialColumnNames.cs │ ├── CargoTableDefinition.cs │ ├── CargoTableFieldDefinition.cs │ └── DataAnnotations │ │ └── CargoListAttribute.cs └── WikiClientLibrary.Cargo.csproj ├── WikiClientLibrary.Commons ├── CI │ ├── Build.ps1 │ ├── BuildSecret.ps1 │ ├── CollectArtifacts.ps1 │ ├── PrepEnv.ps1 │ ├── Secret.bin │ └── Test.ps1 ├── Packaging.ps1 ├── WikiClientLibrary.Commons.props ├── WikiClientLibrary.Packages.props └── WikiClientLibrary.snk ├── WikiClientLibrary.Flow ├── Board.cs ├── Enums.cs ├── FlowNamespaces.cs ├── FlowRequestHelper.cs ├── FlowUtility.cs ├── NamespaceDocs.cs ├── Post.cs ├── Revision.cs ├── Topic.cs └── WikiClientLibrary.Flow.csproj ├── WikiClientLibrary.Wikia ├── Discussions │ ├── Board.cs │ ├── DiscussionsExtensions.cs │ └── Post.cs ├── NamespaceDocs.cs ├── RequestHelper.cs ├── Sites │ ├── WikiVariables.cs │ └── WikiaSite.cs ├── Utility.cs ├── WikiClientLibrary.Wikia.csproj ├── WikiaApi │ ├── LocalWikiSearchList.cs │ ├── LocalWikiSearchResultItem.cs │ ├── RelatedPageItem.cs │ ├── UserInfo.cs │ └── WikiaSiteWikiaApiExtensions.cs ├── WikiaApiException.cs ├── WikiaHtmlResponseParser.cs ├── WikiaJsonResponseMessageParser.cs ├── WikiaNamespaces.cs ├── WikiaQueryRequestMessage.cs └── WikiaSiteOptions.cs ├── WikiClientLibrary.Wikibase ├── Claim.cs ├── Contracts │ └── Entity.cs ├── DataTypes │ ├── WbGlobeCoordinate.cs │ ├── WbMonolingualText.cs │ ├── WbMonolingualTextCollection.cs │ ├── WbQuantity.cs │ ├── WbTime.cs │ └── WikibaseDataType.cs ├── Entity.Editing.cs ├── Entity.cs ├── EntityEditEntry.cs ├── EntityExtensions.cs ├── IEntity.cs ├── Infrastructures │ ├── UnorderedKeyedCollection.cs │ └── UnorderedKeyedMultiCollection.cs ├── NamespaceDocs.cs ├── SerializableEntity.cs ├── Utility.cs ├── WikiClientLibrary.Wikibase.csproj ├── WikibaseRequestHelper.cs ├── WikibaseSiteInfo.cs ├── WikibaseTabularData.cs └── WikibaseUriFactory.cs ├── WikiClientLibrary.sln ├── WikiClientLibrary.sln.DotSettings ├── WikiClientLibrary ├── AbuseFilters │ ├── AbuseFilter.cs │ └── AbuseFilterList.cs ├── Client │ ├── IWikiClient.cs │ ├── MediaWikiFormRequestMessage.cs │ ├── MediaWikiJsonResponseParser.cs │ ├── WikiClient.cs │ ├── WikiClientHelper.cs │ ├── WikiRequestMessage.cs │ ├── WikiResponseMessageParser.cs │ └── WikiResponseParsingContext.cs ├── Constants.cs ├── Exceptions.cs ├── Files │ ├── ChunkedUploadSource.cs │ ├── FileRevision.cs │ ├── UploadResult.cs │ ├── WikiSiteExtensions.cs │ └── WikiUploadSource.cs ├── Generators │ ├── AllCategoriesGenerator.cs │ ├── AllPagesGenerator.cs │ ├── BacklinksGenerator.cs │ ├── CategoriesGenerator.cs │ ├── CategoryMembersGenerator.cs │ ├── FileUsageGenerator.cs │ ├── FilesGenerator.cs │ ├── GeoSearchGenerator.cs │ ├── GeoSearchResultItem.cs │ ├── LinksGenerator.cs │ ├── LogEventsList.cs │ ├── MyWatchlistGenerator.cs │ ├── MyWatchlistResultItem.cs │ ├── PagesWithPropGenerator.cs │ ├── PagesWithPropResultItem.cs │ ├── Primitive │ │ ├── WikiList.cs │ │ ├── WikiListCompatibilityOptions.cs │ │ ├── WikiPageGenerator.cs │ │ ├── WikiPagePropertyGenerator.cs │ │ └── WikiPagePropertyList.cs │ ├── QueryPageGenerator.cs │ ├── RandomPageGenerator.cs │ ├── RecentChangeItem.cs │ ├── RecentChangesGenerator.cs │ ├── RevisionsGenerator.cs │ ├── SearchGenerator.cs │ ├── SearchResultItem.cs │ ├── TranscludedInGenerator.cs │ ├── TransclusionsGenerator.cs │ └── WikiPageExtensions.cs ├── GeoCoordinate.cs ├── IWikiClientAsyncInitialization.cs ├── IWikiClientLoggable.cs ├── Infrastructures │ ├── AsyncInitializationHelper.cs │ ├── ExecutionContextStash.cs │ ├── HttpContentEx.cs │ ├── Json.cs │ ├── JsonContractAttribute.cs │ ├── JsonHelper.cs │ ├── JsonNodeEqualityComparer.cs │ ├── Logging │ │ └── WikiLoggingHelper.cs │ ├── MediaWikiHelper.cs │ ├── OrderedKeyValuePairs.cs │ ├── Polyfill.cs │ ├── TaskUtility.cs │ ├── Throttler.cs │ ├── TokensManager.cs │ ├── WikiJsonElementHelper.cs │ └── WikiReadOnlyDictionary.cs ├── MediaWikiUtility.cs ├── MediaWikiVersion.cs ├── NamespaceDocs.cs ├── Pages │ ├── PageFactory.cs │ ├── PageHelper.cs │ ├── Parsing │ │ ├── ParsedContentInfo.cs │ │ ├── ParsingOptions.cs │ │ └── WikiSiteExtensions.cs │ ├── PurgeFailureInfo.cs │ ├── Queries │ │ ├── Properties │ │ │ ├── CategoriesPropertyProvider.cs │ │ │ ├── CategoryInfoPropertyProvider.cs │ │ │ ├── ExtractsPropertyProvider.cs │ │ │ ├── FileInfoPropertyProvider.cs │ │ │ ├── GeoCoordinatesPropertyProvider.cs │ │ │ ├── LanguageLinksPropertyProvider.cs │ │ │ ├── PageImagesPropertyProvider.cs │ │ │ ├── PageInfoPropertyProvider.cs │ │ │ ├── PagePropertiesPropertyProvider.cs │ │ │ ├── RevisionsPropertyProvider.cs │ │ │ ├── WikiPagePropertyGroup.cs │ │ │ └── WikiPagePropertyProvider.cs │ │ └── WikiPageQueryProvider.cs │ ├── Revision.cs │ ├── WikiPage.cs │ ├── WikiPageExtensions.cs │ └── WikiPageStub.cs ├── Prompts.Designer.cs ├── Prompts.resx ├── Prompts.zh.resx ├── RequestHelper.cs ├── Scribunto │ ├── ScribuntoConsole.cs │ ├── ScribuntoConsoleException.cs │ └── ScribuntoWikiSiteExtensions.cs ├── Sites │ ├── AccountInfo.cs │ ├── SiteInfo.cs │ ├── SiteOptions.cs │ ├── WikiFamily.cs │ ├── WikiSite.Query.cs │ ├── WikiSite.cs │ └── WikiSiteToken.cs ├── UserStub.cs ├── Utility.cs ├── WikiClientLibrary.csproj └── WikiLink.cs └── nuget.config /.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/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: CXuesong 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | custom: # Replace with a single custom sponsorship URL 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths-ignore: 7 | - '*.md' 8 | - '*.txt' 9 | pull_request: 10 | branches: 11 | - master 12 | paths-ignore: 13 | - '*.md' 14 | - '*.txt' 15 | schedule: 16 | - cron: "7 0 * * MON" 17 | - cron: "10 6 * * WED" 18 | - cron: "30 12 * * FRI" 19 | 20 | jobs: 21 | build_linux: 22 | name: Linux Build & Test 23 | runs-on: ubuntu-latest 24 | timeout-minutes: 30 25 | env: 26 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 27 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 28 | WCL_IS_CI_BUILD: 1 29 | BUILD_SECRET_KEY: "${{ secrets.BUILD_SECRET }}" 30 | steps: 31 | - uses: actions/setup-dotnet@v4 32 | with: 33 | dotnet-version: | 34 | 6.x 35 | 8.x 36 | 9.x 37 | dotnet-quality: 'preview' 38 | - uses: actions/checkout@v4 39 | with: 40 | submodules: true 41 | - name: PrepEnv 42 | shell: pwsh 43 | run: ./WikiClientLibrary.Commons/CI/PrepEnv.ps1 44 | - name: Build 45 | shell: pwsh 46 | run: ./WikiClientLibrary.Commons/CI/Build.ps1 47 | - name: CollectArtifacts 48 | shell: pwsh 49 | run: './WikiClientLibrary.Commons/CI/CollectArtifacts -Configuration Release' 50 | - uses: actions/upload-artifact@master 51 | with: 52 | name: BuildArtifacts-Linux-Release 53 | path: ./CollectedArtifacts 54 | - name: Test 55 | shell: pwsh 56 | run: ./WikiClientLibrary.Commons/CI/Test.ps1 57 | build_windows: 58 | if: success() 59 | name: Windows Build & Test 60 | runs-on: windows-latest 61 | timeout-minutes: 30 62 | strategy: 63 | matrix: 64 | config: ["Debug", "Release"] 65 | env: 66 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 67 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 68 | WCL_IS_CI_BUILD: 1 69 | BUILD_SECRET_KEY: "${{ secrets.BUILD_SECRET }}" 70 | steps: 71 | - uses: actions/setup-dotnet@v4 72 | with: 73 | dotnet-version: | 74 | 6.x 75 | 8.x 76 | 9.x 77 | dotnet-quality: 'preview' 78 | - uses: actions/checkout@v4 79 | with: 80 | submodules: true 81 | - name: PrepEnv 82 | shell: pwsh 83 | run: './WikiClientLibrary.Commons/CI/PrepEnv.ps1' 84 | - name: Build 85 | shell: pwsh 86 | run: './WikiClientLibrary.Commons/CI/Build.ps1 -Configuration ${{ matrix.config }}' 87 | - name: CollectArtifacts 88 | shell: pwsh 89 | run: './WikiClientLibrary.Commons/CI/CollectArtifacts -Configuration ${{ matrix.config }}' 90 | - uses: actions/upload-artifact@master 91 | with: 92 | name: BuildArtifacts-Windows-${{ matrix.config }} 93 | path: ./CollectedArtifacts 94 | - name: Test 95 | shell: pwsh 96 | run: './WikiClientLibrary.Commons/CI/Test.ps1 -Configuration ${{ matrix.config }}' 97 | -------------------------------------------------------------------------------- /DocumentationProject/Content/Welcome.aml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Welcome to Wiki Client Library! 6 | 7 | Wiki Client Library is a hand-crafted asynchronous 8 | MediaWiki 9 | https://www.mediawiki.org/ 10 | API client library for wiki sites (including 11 | Wikipedia 12 | https://www.wikipedia.org/ 13 | and its sister projects, as well as 14 | Wikia 15 | community.wikia.com 16 | ). This library aims for human users, as well as bots. 17 | 18 | This package is now available on NuGet. You may install the package using the following command in the Package Management Console 19 | 20 | 21 | Install-Package CXuesong.MW.WikiClientLibrary -Pre 22 | 23 | 24 | 25 |
26 | About the help 27 | 28 | This documentation contains the auto-generated API reference for WCL. For more conceptual topics, see 29 | "Wiki" tab on GitHub 30 | https://github.com/CXuesong/WikiClientLibrary/wiki 31 | . To help improving this API reference, you may add XML documentation to the respective members in the source code, 32 | and open a 33 | pull request 34 | https://github.com/CXuesong/WikiClientLibrary/pulls 35 | . 36 | 37 | 38 |
39 | 40 | 41 | 42 | Wiki Client Library on GitHub 43 | https://github.com/CXuesong/WikiClientLibrary 44 | 45 | 46 |
47 |
48 | -------------------------------------------------------------------------------- /DocumentationProject/ContentLayout.content: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DocumentationProject/icons/Help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CXuesong/WikiClientLibrary/6bb0095713c41cab297dfc58cf35aee9445117c9/DocumentationProject/icons/Help.png -------------------------------------------------------------------------------- /DocumentationProject/images/wcl-generator-classes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CXuesong/WikiClientLibrary/6bb0095713c41cab297dfc58cf35aee9445117c9/DocumentationProject/images/wcl-generator-classes.png -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | WikiClientLibrary 2 | --------------- 3 | 4 | Copyright 2017 CXuesong. 5 | -------------------------------------------------------------------------------- /Playground/GeoSearch.workbook: -------------------------------------------------------------------------------- 1 | --- 2 | packages: 3 | - id: CXuesong.MW.WikiClientLibrary 4 | version: 0.6.0-intX2a 5 | uti: com.xamarin.workbook 6 | id: 950c7695-3ac7-4e56-81cf-1863a2a1cdf8 7 | title: GeoSearch + PageImages 8 | platforms: 9 | - DotNetCore 10 | --- 11 | 12 | ## GeoSearch + PageImages 13 | 14 | This page demonstarates how to perform geological search on Wikipedia, and return the top 10 results with text abstracts and thumbnail images. 15 | 16 | ```csharp 17 | #r "WikiClientLibrary" 18 | using WikiClientLibrary; 19 | using WikiClientLibrary.Client; 20 | using WikiClientLibrary.Sites; 21 | using WikiClientLibrary.Pages; 22 | using WikiClientLibrary.Pages.Queries; 23 | using WikiClientLibrary.Pages.Queries.Properties; 24 | using WikiClientLibrary.Generators; 25 | ``` 26 | 27 | ```csharp 28 | var client = new WikiClient(); 29 | ``` 30 | 31 | ```csharp 32 | var site = new WikiSite(client, await WikiSite.SearchApiEndpointAsync(client, "en.wikipedia.org")); 33 | await site.Initialization; 34 | site 35 | ``` 36 | 37 | ```csharp 38 | var generator = new GeoSearchGenerator(site) { TargetTitle = "London", Radius = 10000 }; 39 | var items = await generator.EnumItemsAsync().Take(10).ToList(); 40 | ``` 41 | 42 | ```csharp 43 | var pages = await generator.EnumPagesAsync(new WikiPageQueryProvider { 44 | Properties = { 45 | new ExtractsPropertyProvider { AsPlainText = true, IntroductionOnly = true, MaxSentences = 1 }, 46 | new PageImagesPropertyProvider { QueryOriginalImage = false, ThumbnailSize = 100 } 47 | } 48 | }).Take(10).ToList(); 49 | ``` 50 | 51 | ```csharp 52 | from p in pages select new { p.GetPropertyGroup().Extract, p.GetPropertyGroup().ThumbnailImage } 53 | ``` -------------------------------------------------------------------------------- /Playground/README.md: -------------------------------------------------------------------------------- 1 | # WCL Playground 2 | 3 | This folder contains sample code snippets that can be opened with [Xamarin Workbooks](https://github.com/Microsoft/workbooks). -------------------------------------------------------------------------------- /Playground/Rolling in the files.workbook: -------------------------------------------------------------------------------- 1 | --- 2 | uti: com.xamarin.workbook 3 | id: 736b6ffa-5c46-4e95-b554-10584c77d162 4 | title: Untitled 5 | platforms: 6 | - DotNetCore 7 | packages: 8 | - id: CXuesong.MW.WikiClientLibrary 9 | version: 0.6.2 10 | --- 11 | 12 | ## Rolling in the files (simple version) 13 | 14 | This workbook demonstrates how to enumerate through all of the files on a Wiki, and to retrieve the URLs to the original files, in less than 20 LOC. 15 | 16 | N.B. the approach of using `Take` and `ToList` to take the results out of the generator does not scale. If you are working with sites with a huge amount of files, consider using the [expanded for-each pattern](https://github.com/CXuesong/WikiClientLibrary/wiki/%5BMediaWiki%5D-Generators#how-to-work-with-iasyncenumerablet "How to work with IAsyncEnumerable"). 17 | 18 | ```csharp 19 | #r "WikiClientLibrary" 20 | ``` 21 | 22 | ```csharp 23 | using WikiClientLibrary.Client; 24 | using WikiClientLibrary.Sites; 25 | using WikiClientLibrary.Pages; 26 | using WikiClientLibrary.Generators; 27 | using WikiClientLibrary; 28 | 29 | // Prepare WikiSite instance 30 | var client = new WikiClient { ClientUserAgent = "Rolling-in-the-files"}; 31 | var site = new WikiSite(client, "https://warriors.wikia.com/api.php"); 32 | await site.Initialization; 33 | ``` 34 | 35 | ```csharp 36 | // Take first 100 files, in alphabetical order, on Warriors Wiki. 37 | var gen = new AllPagesGenerator(site) { 38 | NamespaceId = BuiltInNamespaces.File, 39 | PaginationSize = 50 40 | }; 41 | var files = await gen.EnumPagesAsync().Take(100).Select(page => (page.Title, page.LastFileRevision?.Url)).ToList(); 42 | ``` 43 | 44 | ```csharp 45 | // Clean up 46 | client.Dispose(); 47 | ``` -------------------------------------------------------------------------------- /Samples/ConsoleTestApplication1/ConsoleTestApplication1.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | WikiClientLibrary.Samples.ConsoleTestApplication1 7 | Enable 8 | Enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Samples/LinqToCargo/LinqToCargo.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | WikiClientLibrary.Samples.LinqToCargo 7 | Enable 8 | Enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Samples/LinqToCargo/Program.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | using Microsoft.Extensions.Logging; 3 | using Microsoft.Extensions.Logging.Console; 4 | using WikiClientLibrary.Cargo.Linq; 5 | using WikiClientLibrary.Cargo.Schema; 6 | using WikiClientLibrary.Client; 7 | using WikiClientLibrary.Sites; 8 | 9 | var loggerFactory = LoggerFactory.Create(c => c 10 | // Use LogLevel.Debug to see the exact query parameters. 11 | .SetMinimumLevel(LogLevel.Debug) 12 | .AddSimpleConsole(c1 => 13 | { 14 | c1.ColorBehavior = LoggerColorBehavior.Enabled; 15 | c1.IncludeScopes = true; 16 | }) 17 | ); 18 | using var client = new WikiClient 19 | { 20 | ClientUserAgent = "WikiClientLibrary.Samples.CargoLinq/1.0", 21 | Logger = loggerFactory.CreateLogger(), 22 | }; 23 | var site = new WikiSite(client, "https://lol.fandom.com/api.php"); 24 | site.Logger = loggerFactory.CreateLogger(); 25 | await site.Initialization; 26 | 27 | var context = new LolCargoQueryContext(site); 28 | // Query starts here. 29 | var query = context.RosterChanges 30 | .Where(x => x.DateSort > new DateTime(2020, 12, 11)) 31 | .OrderBy(x => x.DateSort) 32 | .Take(100) 33 | // Cargo query expression ends here. 34 | .AsAsyncEnumerable() 35 | // Do some local conversion for better display on console. 36 | .Select(x => new 37 | { 38 | x.Page, 39 | x.DateSort, 40 | x.Player, 41 | x.Direction, 42 | Roles = string.Join(';', x.Roles), 43 | Tags = string.Join(';', x.Tags), 44 | Tournaments = string.Join(';', x.Tournaments), 45 | }); 46 | var counter = 0; 47 | await foreach (var item in query) 48 | { 49 | counter++; 50 | Console.WriteLine("{0,4}: {1}", counter, item); 51 | } 52 | 53 | class LolCargoQueryContext : CargoQueryContext 54 | { 55 | 56 | /// 57 | public LolCargoQueryContext(WikiSite wikiSite) : base(wikiSite) 58 | { 59 | PaginationSize = 30; 60 | } 61 | 62 | public ICargoRecordSet RosterChanges => Table(); 63 | 64 | } 65 | 66 | /// https://lol.fandom.com/Special:CargoTables/RosterChanges 67 | class RosterChanges 68 | { 69 | 70 | // `default!` is how we are bypassing NRT warning in EF for now. 71 | // Alternatively, you can initialize with ctor but you'll need a lot of params in ctor for sure. 72 | [Column(CargoSpecialColumnNames.PageName)] 73 | public string Page { get; set; } = default!; 74 | 75 | [Column("Date_Sort")] 76 | public DateTime DateSort { get; set; } 77 | 78 | public string Player { get; set; } = default!; 79 | 80 | public string Direction { get; set; } = default!; 81 | 82 | public ICollection Roles { get; set; } = default!; 83 | 84 | public ICollection Tags { get; set; } = default!; 85 | 86 | public ICollection Tournaments { get; set; } = default!; 87 | 88 | } 89 | -------------------------------------------------------------------------------- /Samples/ScribuntoInteractive/ScribuntoInteractive.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | WikiClientLibrary.Samples.ScribuntoInteractive 7 | Enable 8 | Enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Samples/WpfTestApplication1/App.xaml: -------------------------------------------------------------------------------- 1 |  6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Samples/WpfTestApplication1/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace WikiClientLibrary.Samples.WpfTestApplication1; 4 | 5 | /// 6 | /// App.xaml 的交互逻辑 7 | /// 8 | public partial class App : Application 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /Samples/WpfTestApplication1/MainWindow.xaml: -------------------------------------------------------------------------------- 1 |  11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 45 | 46 | 47 | 48 | 49 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /Samples/WpfTestApplication1/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Resources; 3 | using System.Runtime.CompilerServices; 4 | using System.Runtime.InteropServices; 5 | using System.Windows; 6 | 7 | //将 ComVisible 设置为 false 将使此程序集中的类型 8 | //对 COM 组件不可见。 如果需要从 COM 访问此程序集中的类型, 9 | //请将此类型的 ComVisible 特性设置为 true。 10 | [assembly: ComVisible(false)] 11 | 12 | //若要开始生成可本地化的应用程序,请 13 | // 中的 .csproj 文件中 14 | //例如,如果您在源文件中使用的是美国英语, 15 | //使用的是美国英语,请将 设置为 en-US。 然后取消 16 | //对以下 NeutralResourceLanguage 特性的注释。 更新 17 | //以下行中的“en-US”以匹配项目文件中的 UICulture 设置。 18 | 19 | //[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] 20 | 21 | 22 | [assembly: ThemeInfo( 23 | ResourceDictionaryLocation.None, //主题特定资源词典所处位置 24 | //(当资源未在页面 25 | //或应用程序资源字典中找到时使用) 26 | ResourceDictionaryLocation.SourceAssembly //常规资源词典所处位置 27 | //(当资源未在页面 28 | //、应用程序或任何主题专用资源字典中找到时使用) 29 | )] 30 | -------------------------------------------------------------------------------- /Samples/WpfTestApplication1/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // 此代码由工具生成。 4 | // 运行时版本:4.0.30319.42000 5 | // 6 | // 对此文件的更改可能会导致不正确的行为,并且如果 7 | // 重新生成代码,这些更改将会丢失。 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace WpfTestApplication1.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// 一个强类型的资源类,用于查找本地化的字符串等。 17 | /// 18 | // 此类是由 StronglyTypedResourceBuilder 19 | // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。 20 | // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen 21 | // (以 /str 作为命令选项),或重新生成 VS 项目。 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// 返回此类使用的缓存的 ResourceManager 实例。 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("WpfTestApplication1.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// 使用此强类型资源类,为所有资源查找 51 | /// 重写当前线程的 CurrentUICulture 属性。 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Samples/WpfTestApplication1/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // 此代码由工具生成。 4 | // 运行时版本:4.0.30319.42000 5 | // 6 | // 对此文件的更改可能会导致不正确的行为,并且如果 7 | // 重新生成代码,这些更改将会丢失。 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace WpfTestApplication1.Properties { 12 | 13 | 14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.0.0")] 16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { 17 | 18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 19 | 20 | public static Settings Default { 21 | get { 22 | return defaultInstance; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Samples/WpfTestApplication1/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Samples/WpfTestApplication1/WikiPageTemplate.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |

11 |
12 |
13 |
14 | From 15 |
16 |
Client powered by WikiClientLibrary!
17 |
18 |
19 |
20 | 21 | -------------------------------------------------------------------------------- /Samples/WpfTestApplication1/WpfTestApplication1.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | WinExe 5 | net8.0-windows 6 | WikiClientLibrary.Samples.WpfTestApplication1 7 | true 8 | Enable 9 | Enable 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /UnitTestProject1/Assembly.cs: -------------------------------------------------------------------------------- 1 | // This is a work-around for #11. 2 | // https://github.com/CXuesong/WikiClientLibrary/issues/11 3 | // We are using Bot Password on CI, which may naturally evade the issue. 4 | 5 | // [assembly: CollectionBehavior(DisableTestParallelization = true)] 6 | 7 | 8 | -------------------------------------------------------------------------------- /UnitTestProject1/Attributes.cs: -------------------------------------------------------------------------------- 1 | using Xunit.Abstractions; 2 | using Xunit.Sdk; 3 | 4 | namespace WikiClientLibrary.Tests.UnitTestProject1; 5 | 6 | public enum CISkippedReason 7 | { 8 | 9 | Unknown = 0, 10 | Unstable, 11 | Deprecated, 12 | AgentBlocked, 13 | 14 | } 15 | 16 | /// 17 | /// Mark the unit test with CI=Skipped trait. 18 | /// This will cause the test not being executed in CI environment. 19 | /// 20 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] 21 | [TraitDiscoverer("WikiClientLibrary.Tests.UnitTestProject1." + nameof(CISkippedTraitDiscoverer), "UnitTestProject1")] 22 | public class CISkippedAttribute : Attribute, ITraitAttribute 23 | { 24 | 25 | public CISkippedReason Reason { get; set; } 26 | 27 | } 28 | 29 | public class CISkippedTraitDiscoverer : ITraitDiscoverer 30 | { 31 | 32 | private static readonly KeyValuePair[] traits = new[] { new KeyValuePair("CI", "Skipped") }; 33 | 34 | /// 35 | public IEnumerable> GetTraits(IAttributeInfo traitAttribute) 36 | { 37 | return traits; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /UnitTestProject1/DemoImages/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CXuesong/WikiClientLibrary/6bb0095713c41cab297dfc58cf35aee9445117c9/UnitTestProject1/DemoImages/1.jpg -------------------------------------------------------------------------------- /UnitTestProject1/DemoImages/1.txt: -------------------------------------------------------------------------------- 1 | ---------------------------------------- 2 | DemoImage/1.jpg 3 | 4 | Size: 26176 Bytes 5 | 6 | SHA1: 81ED69FA2C2BDEEBBA277C326D1AAC9E0E57B346 7 | ---------------------------------------- 8 | 9 | This is a resized version of image 10 | 11 | https://commons.wikimedia.org/wiki/File:Japanese_Squirrel_edited_version.jpg 12 | 13 | This work has been released into the public domain by its author, Ma2bara. This applies worldwide. 14 | 15 | In some countries this may not be legally possible; if so: 16 | 17 | Ma2bara grants anyone the right to use this work for any purpose, without any conditions, unless such conditions are required by law. -------------------------------------------------------------------------------- /UnitTestProject1/DemoImages/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CXuesong/WikiClientLibrary/6bb0095713c41cab297dfc58cf35aee9445117c9/UnitTestProject1/DemoImages/2.jpg -------------------------------------------------------------------------------- /UnitTestProject1/DemoImages/2.txt: -------------------------------------------------------------------------------- 1 | ---------------------------------------- 2 | DemoImages/2.jpg 3 | 4 | Size: 12056 Bytes 5 | 6 | SHA1: 6D86095C7EE7714AD90E32FE5D251C6F1B14EF40 7 | ---------------------------------------- 8 | 9 | This is a resized version of image 10 | 11 | https://commons.wikimedia.org/wiki/File:Campo_flicker_(Colaptes_campestris)_female.JPG 12 | 13 | Author is Charlesjsharp. 14 | 15 | This file is licensed under the Creative Commons Attribution-Share Alike 4.0 International license. -------------------------------------------------------------------------------- /UnitTestProject1/Endpoints.cs: -------------------------------------------------------------------------------- 1 | namespace WikiClientLibrary.Tests.UnitTestProject1; 2 | 3 | public static class Endpoints 4 | { 5 | 6 | public const string WikipediaTest2 = "https://test2.wikipedia.org/w/api.php"; 7 | 8 | /// 9 | /// WMF beta test site. We only apply the tests that cannot be performed in test2.wikipedia.org (e.g. Flow boards). 10 | /// 11 | public const string WikipediaBetaEn = "https://en.wikipedia.beta.wmflabs.org/w/api.php"; 12 | 13 | /// 14 | /// This is NOT a test site so do not make modifications to the site. 15 | /// 16 | public const string WikipediaEn = "https://en.wikipedia.org/w/api.php"; 17 | 18 | /// 19 | /// This is NOT a test site so do not make modifications to the site. 20 | /// 21 | public const string WikipediaZh = "https://zh.wikipedia.org/w/api.php"; 22 | 23 | /// 24 | /// This is NOT a test site so do not make modifications to the site. 25 | /// 26 | public const string WikipediaLzh = "https://zh-classical.wikipedia.org/w/api.php"; 27 | 28 | public const string WikimediaCommons = "https://commons.wikimedia.org/w/api.php"; 29 | 30 | public const string WikimediaCommonsBeta = "https://commons.wikimedia.beta.wmflabs.org/w/api.php"; 31 | 32 | public const string Wikidata = "https://www.wikidata.org/w/api.php"; 33 | 34 | public const string WikidataTest = "https://test.wikidata.org/w/api.php"; 35 | 36 | public const string WikidataBeta = "https://wikidata.beta.wmflabs.org/w/api.php"; 37 | 38 | // It's on MW 1.43 now. Well. 39 | public const string WikiaTest = "https://sandbox.fandom.com/api.php"; 40 | 41 | // It's on MW 1.19 as of now. 42 | public const string TFWiki = "https://tfwiki.net/api.php"; 43 | 44 | public const string RuWarriorsWiki = "https://warriors-cats.fandom.com/ru/api.php"; 45 | 46 | public const string LolEsportsWiki = "https://lol.fandom.com/api.php"; 47 | 48 | } 49 | -------------------------------------------------------------------------------- /UnitTestProject1/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | // https://docs.microsoft.com/en-us/visualstudio/code-quality/in-source-suppression-overview?view=vs-2019 7 | 8 | using System.Diagnostics.CodeAnalysis; 9 | 10 | [assembly: SuppressMessage("Style", "VSTHRD003:Avoid awaiting or returning a Task representing work that was not started within your context as that can lead to deadlocks.", 11 | Justification = "There is no STA thread so what are you talking about? ", 12 | Scope = "namespaceanddescendants", Target = "~N:WikiClientLibrary.Tests.UnitTestProject1.Tests")] 13 | [assembly: SuppressMessage("Style", "VSTHRD200:Use Async suffix for async methods", 14 | Justification = "test methods", 15 | Scope = "namespaceanddescendants", Target = "~N:WikiClientLibrary.Tests.UnitTestProject1.Tests")] 16 | -------------------------------------------------------------------------------- /UnitTestProject1/Tests/AbuseFilterTests.cs: -------------------------------------------------------------------------------- 1 | using WikiClientLibrary.AbuseFilters; 2 | using WikiClientLibrary.Tests.UnitTestProject1.Fixtures; 3 | using Xunit; 4 | using Xunit.Abstractions; 5 | 6 | namespace WikiClientLibrary.Tests.UnitTestProject1.Tests; 7 | 8 | public class AbuseFilterTests : WikiSiteTestsBase, IClassFixture 9 | { 10 | 11 | /// 12 | public AbuseFilterTests(ITestOutputHelper output, WikiSiteProvider wikiSiteProvider) : base(output, wikiSiteProvider) 13 | { 14 | } 15 | 16 | [Fact] 17 | public async Task AbuseFilterListTest() 18 | { 19 | var site = await WpTest2SiteAsync; 20 | var aflist = new AbuseFilterList(site) { PaginationSize = 30 }; 21 | var items = await aflist.EnumItemsAsync().ToListAsync(); 22 | ShallowTrace(items); 23 | Assert.Contains(items, f => f.LastEditor == "Luke081515"); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /UnitTestProject1/Tests/PageHelperTests.cs: -------------------------------------------------------------------------------- 1 | using WikiClientLibrary.Pages; 2 | using Xunit; 3 | using Xunit.Abstractions; 4 | 5 | namespace WikiClientLibrary.Tests.UnitTestProject1.Tests; 6 | 7 | public class PageHelperTests : UnitTestsBase 8 | { 9 | 10 | /// 11 | public PageHelperTests(ITestOutputHelper output) : base(output) 12 | { 13 | } 14 | 15 | [Theory] 16 | [InlineData("Test (disambiguation)", "Test")] 17 | [InlineData("Test(assessment)", "Test")] 18 | [InlineData("Test (assessment", "Test")] 19 | [InlineData("Test \t (disambiguation)", "Test")] 20 | [InlineData("政治学 (亚里士多德)", "政治学")] 21 | [InlineData("政治学 (アリストテレス)", "政治学")] 22 | public void StripTitleDisambiguationTest(string originalTitle, string strippedTitle) 23 | { 24 | Assert.Equal(strippedTitle, PageHelper.StripTitleDisambiguation(originalTitle)); 25 | } 26 | 27 | [Theory] 28 | [InlineData("\n\n\r\ntest\n \r \n", "\n\n\ntest")] 29 | [InlineData("{{User sandbox}}\r\n\r\n\r\ntest123123\n\n\n\n", 30 | "{{User sandbox}}\n\n\ntest123123")] 31 | public void SanitizePageContentTest(string content, string sanitizedContent) 32 | { 33 | Assert.Equal(sanitizedContent, PageHelper.SanitizePageContent(content)); 34 | } 35 | 36 | [Theory] 37 | [InlineData("test123", "7288edd0fc3ffcbe93a0cf06e3568e28521687bc")] 38 | [InlineData("test\n123", "5fd30ebeba53fc4614bcf3b00c4c55b9cd70b266")] 39 | [InlineData("{{User sandbox}}\r\n\r\n\r\ntest123123\n\n\n\n", "46f9909a6eb8dac9a24e3e4b85bf2a89e6983c1e")] 40 | public void EvaluateSanitizedSha1Test(string content, string sha1) 41 | { 42 | Assert.Equal(sha1, PageHelper.EvaluateSanitizedSha1(content)); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /UnitTestProject1/Tests/RenderingTests.cs: -------------------------------------------------------------------------------- 1 | using WikiClientLibrary.Pages.Parsing; 2 | using WikiClientLibrary.Tests.UnitTestProject1.Fixtures; 3 | using Xunit; 4 | using Xunit.Abstractions; 5 | 6 | namespace WikiClientLibrary.Tests.UnitTestProject1.Tests; 7 | 8 | public class RenderingTests : WikiSiteTestsBase, IClassFixture 9 | { 10 | 11 | /// 12 | public RenderingTests(ITestOutputHelper output, WikiSiteProvider wikiSiteProvider) : base(output, wikiSiteProvider) 13 | { 14 | } 15 | 16 | [Fact] 17 | public async Task WpLzhPageParsingTest1() 18 | { 19 | var site = await WpLzhSiteAsync; 20 | // 一九五二年 21 | var result = await site.ParseRevisionAsync(240575, ParsingOptions.EffectiveLanguageLinks); 22 | 23 | WriteOutput("Parsed revision"); 24 | ShallowTrace(result); 25 | 26 | Assert.Equal("一九五二年", result.Title); 27 | Assert.Matches(@"一九五二年", result.DisplayTitle); 28 | Assert.Equal("1952", result.LanguageLinks.First(l => l.Language == "en").Title); 29 | Assert.Equal("1952年", result.LanguageLinks.First(l => l.Language == "zh").Title); 30 | Assert.Contains(">公元一九五二年於諸曆TITLE", result.DisplayTitle); 62 | Assert.Contains("

Text Text\n

TITLE\n

", result.Content); 63 | ///////////////////// 64 | result = await site.ParseContentAsync("{{ambox}}", "Summary.", "TITLE", 65 | ParsingOptions.LimitReport | ParsingOptions.TranscludedPages); 66 | ShallowTrace(result, 4); 67 | Assert.Contains(result.TranscludedPages, p => p.Title == "Template:Ambox"); 68 | Assert.True(result.ParserLimitReports.First(r => r.Name == "limitreport-expansiondepth").Value > 1); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /UnitTestProject1/Tests/WebClientTests.cs: -------------------------------------------------------------------------------- 1 | using WikiClientLibrary.Client; 2 | using WikiClientLibrary.Tests.UnitTestProject1.Fixtures; 3 | using Xunit; 4 | using Xunit.Abstractions; 5 | 6 | namespace WikiClientLibrary.Tests.UnitTestProject1.Tests; 7 | 8 | public class WebClientTests : WikiSiteTestsBase, IClassFixture 9 | { 10 | 11 | /// 12 | public WebClientTests(ITestOutputHelper output, WikiSiteProvider wikiSiteProvider) : base(output, wikiSiteProvider) 13 | { 14 | } 15 | 16 | [Fact] 17 | public async Task TestMethod1() 18 | { 19 | var client = CreateWikiClient(); 20 | var query = new { action = "query", meta = "siteinfo", format = "json" }; 21 | var json1 = await client.InvokeAsync(Endpoints.WikipediaTest2, 22 | new MediaWikiFormRequestMessage(query), 23 | MediaWikiJsonResponseParser.Default, 24 | CancellationToken.None); 25 | var json2 = await client.InvokeAsync(Endpoints.WikipediaTest2, 26 | new MediaWikiFormRequestMessage(query, true), 27 | MediaWikiJsonResponseParser.Default, 28 | CancellationToken.None); 29 | WriteOutput(json1); 30 | } 31 | 32 | [Fact] 33 | public async Task TestMethod2() 34 | { 35 | var client = CreateWikiClient(); 36 | await Assert.ThrowsAsync(() => 37 | client.InvokeAsync(Endpoints.WikipediaTest2, 38 | new MediaWikiFormRequestMessage(new 39 | { 40 | action = "invalid_action_test", description = "This is a test case for invalid action parameter.", format = "json", 41 | }), 42 | MediaWikiJsonResponseParser.Default, 43 | CancellationToken.None)); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /UnitTestProject1/Tests/WikiClientHelperTests.cs: -------------------------------------------------------------------------------- 1 | using WikiClientLibrary.Client; 2 | using Xunit; 3 | using Xunit.Abstractions; 4 | 5 | namespace WikiClientLibrary.Tests.UnitTestProject1.Tests; 6 | 7 | public class WikiClientHelperTests : UnitTestsBase 8 | { 9 | 10 | /// 11 | public WikiClientHelperTests(ITestOutputHelper output) : base(output) 12 | { 13 | } 14 | 15 | [Fact] 16 | public void BuildUserAgentTest() 17 | { 18 | Assert.Equal("MyProduct", WikiClientHelper.BuildUserAgent("MyProduct")); 19 | Assert.Equal("MyProduct/1.1", WikiClientHelper.BuildUserAgent("MyProduct", "1.1")); 20 | Assert.Equal("MyProduct/1.2 (https://example.org)", 21 | WikiClientHelper.BuildUserAgent("MyProduct", "1.2", "https://example.org")); 22 | Assert.Equal("MyProduct/1.3 (https://example.org)", 23 | WikiClientHelper.BuildUserAgent("MyProduct", "1.3", "(https://example.org)")); 24 | Assert.Equal("UnitTestProject1/1.0", WikiClientHelper.BuildUserAgent(typeof(WikiClientHelperTests).Assembly)); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /UnitTestProject1/Tests/WikiFamilyTests.cs: -------------------------------------------------------------------------------- 1 | using WikiClientLibrary.Sites; 2 | using WikiClientLibrary.Tests.UnitTestProject1.Fixtures; 3 | using Xunit; 4 | using Xunit.Abstractions; 5 | 6 | namespace WikiClientLibrary.Tests.UnitTestProject1.Tests; 7 | 8 | public class WikiFamilyTests : WikiSiteTestsBase, IClassFixture 9 | { 10 | 11 | private readonly Lazy _Family; 12 | 13 | private WikiFamily Family => _Family.Value; 14 | 15 | /// 16 | public WikiFamilyTests(ITestOutputHelper output, WikiSiteProvider wikiSiteProvider) : base(output, wikiSiteProvider) 17 | { 18 | _Family = new Lazy(() => 19 | { 20 | var f = new WikiFamily(CreateWikiClient(), "Wikipedia"); 21 | f.Register("en", GetWPEntrypoint("en")); 22 | f.Register("fr", GetWPEntrypoint("fr")); 23 | f.Register("test2", GetWPEntrypoint("test2")); 24 | f.Register("lzh", GetWPEntrypoint("zh-classical")); 25 | return f; 26 | }); 27 | } 28 | 29 | private string GetWPEntrypoint(string prefix) 30 | { 31 | return "https://" + prefix + ".wikipedia.org/w/api.php"; 32 | } 33 | 34 | private void AssertWikiLink(WikiLink link, string? interwiki, string? ns, string localTitle) 35 | { 36 | Assert.Equal(interwiki, link.InterwikiPrefix); 37 | Assert.Equal(ns, link.NamespaceName); 38 | Assert.Equal(localTitle, link.Title); 39 | } 40 | 41 | [Fact] 42 | public async Task InterwikiLinkTests() 43 | { 44 | // We will not login onto any site… 45 | var originSite = await Family.GetSiteAsync("test2"); 46 | Utility.AssertNotNull(originSite); 47 | // With originating WikiSite 48 | var link = await WikiLink.ParseAsync(originSite, Family, "WikiPedia:SANDBOX"); 49 | AssertWikiLink(link, null, "Wikipedia", "SANDBOX"); 50 | link = await WikiLink.ParseAsync(originSite, Family, "FR___:_ __Wp__ _: SANDBOX"); 51 | AssertWikiLink(link, "fr", "Wikipédia", "SANDBOX"); 52 | link = await WikiLink.ParseAsync(originSite, Family, "EN:fr: LZH:Project:SANDBOX"); 53 | AssertWikiLink(link, "lzh", "維基大典", "SANDBOX"); 54 | // We don't have de in WikiFamily, but WP has de in its inter-wiki table. 55 | // Should works as if we haven't specified Family. 56 | link = await WikiLink.ParseAsync(originSite, Family, "de:Project:SANDBOX"); 57 | AssertWikiLink(link, "de", null, "Project:SANDBOX"); 58 | // Without originating WikiSite 59 | await Assert.ThrowsAsync(() => WikiLink.ParseAsync(Family, "WikiPedia:SANDBOX")); 60 | link = await WikiLink.ParseAsync(Family, "FR___:_ __Wp__ _: SANDBOX"); 61 | AssertWikiLink(link, "fr", "Wikipédia", "SANDBOX"); 62 | link = await WikiLink.ParseAsync(Family, "EN:fr: LZH:Project:SANDBOX"); 63 | AssertWikiLink(link, "lzh", "維基大典", "SANDBOX"); 64 | await Assert.ThrowsAsync(() => WikiLink.ParseAsync(Family, "unk:WikiPedia:SANDBOX")); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /UnitTestProject1/UnitTestProject1.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | net6.0;net8.0 6 | {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 7 | WikiClientLibrary.Tests.UnitTestProject1 8 | Enable 9 | Enable 10 | 11 | 12 | 13 | 14 | $(DefineConstants);DRY_RUN 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | all 28 | runtime; build; native; contentfiles; analyzers 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | True 45 | True 46 | Resources.resx 47 | 48 | 49 | 50 | 51 | 52 | ResXFileCodeGenerator 53 | Resources.Designer.cs 54 | 55 | 56 | -------------------------------------------------------------------------------- /WikiClientLibrary.Cargo/CargoQueryParameters.cs: -------------------------------------------------------------------------------- 1 | namespace WikiClientLibrary.Cargo; 2 | 3 | public class CargoQueryParameters 4 | { 5 | 6 | /// The query offset. 7 | public int Offset { get; set; } 8 | 9 | /// A limit on the number of results returned. 10 | public int Limit { get; set; } = 50; 11 | 12 | /// The Cargo database table or tables on which to search. 13 | public IEnumerable Tables { get; set; } = Array.Empty(); 14 | 15 | /// The table field(s) to retrieve. 16 | public IEnumerable Fields { get; set; } = Array.Empty(); 17 | 18 | /// The conditions for the query, corresponding to an SQL WHERE clause. 19 | public string? Where { get; set; } 20 | 21 | /// Conditions for joining multiple tables (LEFT OUTER JOIN), corresponding to an SQL JOIN ON clause. 22 | /// a sequence containing search conditions (ON table1a.field1a = table1b.field1b, ... ) for the JOIN clause. 23 | /// Conditions will be concatenated with comma (,). 24 | public IEnumerable? JoinOn { get; set; } 25 | 26 | /// Field(s) on which to group results, corresponding to an SQL GROUP BY clause. 27 | public string? GroupBy { get; set; } 28 | 29 | /// Conditions for grouped values, corresponding to an SQL HAVING clause. 30 | public string? Having { get; set; } 31 | 32 | /// The order of results, corresponding to an SQL ORDER BY clause. 33 | public IEnumerable? OrderBy { get; set; } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /WikiClientLibrary.Cargo/Linq/CargoFunctions.cs: -------------------------------------------------------------------------------- 1 | namespace WikiClientLibrary.Cargo.Linq; 2 | 3 | /// 4 | /// Contains stub methods used inside expressions in extension methods. 5 | /// These methods are used for building LINQ query expressions and are not intended to be invoked directly. 6 | /// 7 | public static class CargoFunctions 8 | { 9 | 10 | private static Exception GetClientInvocationException(string name) 11 | { 12 | return new InvalidOperationException($"Cargo LINQ function {name} should be used inside Cargo LINQ query expression."); 13 | } 14 | 15 | public static bool Like(string value, string pattern) 16 | => throw GetClientInvocationException(nameof(Like)); 17 | 18 | /// 19 | /// Determines whether the list-typed field contains a certain value. 20 | /// (See mw:Extension:Cargo/Querying data#The "HOLDS" command.) 21 | /// 22 | /// element name. 23 | /// Cargo table field expression of type list. 24 | /// the matching element. 25 | /// whether the list contains the matching element. 26 | public static bool Holds(IEnumerable cargoList, T element) 27 | => throw GetClientInvocationException(nameof(Holds)); 28 | 29 | /// 30 | /// Determines whether the list-typed field contains any value matching the specific LIKE wildcard expression. 31 | /// (See mw:Extension:Cargo/Querying data#HOLDS LIKE.) 32 | /// 33 | /// element name. 34 | /// Cargo table field expression of type list. 35 | /// the matching pattern with % or _ wildcard. 36 | /// whether the list contains the matching element. 37 | public static bool HoldsLike(IEnumerable cargoList, string pattern) 38 | => throw GetClientInvocationException(nameof(HoldsLike)); 39 | 40 | /// 41 | /// Returns the difference ( - ) between 2 dates expressed as a value in days from one date to the other. 42 | /// 43 | /// date 1. 44 | /// date 2. 45 | public static int DateDiff(DateTime? dt1, DateTime? dt2) 46 | => throw GetClientInvocationException(nameof(DateDiff)); 47 | 48 | /// 49 | /// Returns the difference ( - ) between 2 dates expressed as a value in days from one date to the other. 50 | /// 51 | /// date 1. 52 | /// date 2. 53 | public static int DateDiff(DateTimeOffset? dt1, DateTimeOffset? dt2) 54 | => throw GetClientInvocationException(nameof(DateDiff)); 55 | 56 | } 57 | -------------------------------------------------------------------------------- /WikiClientLibrary.Cargo/Linq/CargoModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | using System.Reflection; 4 | using WikiClientLibrary.Cargo.Schema; 5 | 6 | namespace WikiClientLibrary.Cargo.Linq; 7 | 8 | /// 9 | /// Represents a mapping between Cargo table and CLR model (DTO) type. 10 | /// 11 | public class CargoModel 12 | { 13 | 14 | public static CargoModel FromClrType(Type clrType, string? nameOverride = null) 15 | { 16 | var tableAttr = clrType.GetCustomAttribute(); 17 | var fields = clrType.GetProperties() 18 | .Where(p => p.CanRead && p.CanWrite && p.GetMethod!.IsPublic && p.SetMethod!.IsPublic) 19 | .Select(p => new CargoModelProperty(p)) 20 | .ToImmutableList(); 21 | var tableDefinition = 22 | new CargoTableDefinition(nameOverride ?? tableAttr?.Name ?? clrType.Name, fields.Select(f => f.FieldDefinition)); 23 | return new CargoModel(clrType, tableDefinition, fields); 24 | } 25 | 26 | private CargoModel(Type clrType, CargoTableDefinition tableDefinition, IReadOnlyList properties) 27 | { 28 | ClrType = clrType ?? throw new ArgumentNullException(nameof(clrType)); 29 | TableDefinition = tableDefinition ?? throw new ArgumentNullException(nameof(tableDefinition)); 30 | Properties = properties; 31 | } 32 | 33 | public string Name => TableDefinition.Name; 34 | 35 | public Type ClrType { get; } 36 | 37 | public CargoTableDefinition TableDefinition { get; } 38 | 39 | public IReadOnlyList Properties { get; } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /WikiClientLibrary.Cargo/Linq/CargoModelProperty.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Reflection; 3 | using WikiClientLibrary.Cargo.Schema; 4 | 5 | namespace WikiClientLibrary.Cargo.Linq; 6 | 7 | /// 8 | /// Represents a property (or field) in . 9 | /// 10 | [DebuggerDisplay("{Name} ({ClrProperty})")] 11 | public class CargoModelProperty 12 | { 13 | 14 | internal CargoModelProperty(PropertyInfo clrProperty) 15 | { 16 | ClrProperty = clrProperty; 17 | ClrType = clrProperty.PropertyType; 18 | FieldDefinition = CargoTableFieldDefinition.FromProperty(clrProperty); 19 | } 20 | 21 | /// Cargo field name in the Cargo table. 22 | public string Name => FieldDefinition.Name; 23 | 24 | public PropertyInfo ClrProperty { get; } 25 | 26 | public Type ClrType { get; } 27 | 28 | public CargoTableFieldDefinition FieldDefinition { get; } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /WikiClientLibrary.Cargo/Linq/CargoModelUtility.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | using System.Reflection; 3 | 4 | namespace WikiClientLibrary.Cargo.Linq; 5 | 6 | internal static class CargoModelUtility 7 | { 8 | 9 | public static string ColumnNameFromProperty(MemberInfo member) 10 | { 11 | var columnAttr = member.GetCustomAttribute(); 12 | return columnAttr?.Name ?? member.Name; 13 | } 14 | 15 | public static Type? GetCollectionElementType(Type collectionType) 16 | { 17 | if (!collectionType.IsConstructedGenericType) 18 | return null; 19 | var genDef = collectionType.GetGenericTypeDefinition(); 20 | if (genDef == typeof(ICollection<>) || genDef == typeof(IList<>)) 21 | { 22 | return collectionType.GenericTypeArguments[0]; 23 | } 24 | return null; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /WikiClientLibrary.Cargo/Linq/CargoQueryContext.cs: -------------------------------------------------------------------------------- 1 | using WikiClientLibrary.Sites; 2 | 3 | namespace WikiClientLibrary.Cargo.Linq; 4 | 5 | /// 6 | /// Provides LINQ to Cargo query ability. 7 | /// 8 | public interface ICargoQueryContext 9 | { 10 | 11 | /// 12 | /// Starts a Linq query expression on the specified Cargo model and Cargo table. 13 | /// 14 | /// type of the model. 15 | /// name of the Cargo table. Specify null to use default table name corresponding to the model. 16 | /// LINQ root. 17 | ICargoRecordSet Table(string? name); 18 | 19 | /// 20 | /// Starts a Linq query expression on the specified table. 21 | /// 22 | ICargoRecordSet Table(); 23 | 24 | } 25 | 26 | public class CargoQueryContext : ICargoQueryContext 27 | { 28 | 29 | private int _PaginationSize = 10; 30 | 31 | public CargoQueryContext(WikiSite wikiSite) 32 | { 33 | WikiSite = wikiSite ?? throw new ArgumentNullException(nameof(wikiSite)); 34 | } 35 | 36 | public WikiSite WikiSite { get; } 37 | 38 | /// 39 | /// Gets/sets the default pagination size used when requesting 40 | /// for the records from MediaWiki server. (Default value: 10.) 41 | /// 42 | public int PaginationSize 43 | { 44 | get => _PaginationSize; 45 | set 46 | { 47 | if (value <= 0) 48 | throw new ArgumentOutOfRangeException(nameof(value)); 49 | _PaginationSize = value; 50 | } 51 | } 52 | 53 | /// 54 | public ICargoRecordSet Table(string? name) 55 | { 56 | return new CargoRecordSet(CargoModel.FromClrType(typeof(T), name), 57 | new CargoQueryProvider(WikiSite) { PaginationSize = _PaginationSize }); 58 | } 59 | 60 | /// 61 | public ICargoRecordSet Table() => Table(null); 62 | 63 | } 64 | -------------------------------------------------------------------------------- /WikiClientLibrary.Cargo/Linq/CargoQueryProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using WikiClientLibrary.Sites; 3 | 4 | namespace WikiClientLibrary.Cargo.Linq; 5 | 6 | internal class CargoQueryProvider : IQueryProvider 7 | { 8 | 9 | private int _PaginationSize = 10; 10 | private ICargoRecordConverter _RecordConverter = new CargoRecordConverter(); 11 | 12 | public CargoQueryProvider(WikiSite wikiSite) 13 | { 14 | WikiSite = wikiSite ?? throw new ArgumentNullException(nameof(wikiSite)); 15 | } 16 | 17 | public WikiSite WikiSite { get; } 18 | 19 | public int PaginationSize 20 | { 21 | get => _PaginationSize; 22 | set 23 | { 24 | if (value <= 0) 25 | throw new ArgumentOutOfRangeException(nameof(value)); 26 | _PaginationSize = value; 27 | } 28 | } 29 | 30 | public ICargoRecordConverter RecordConverter 31 | { 32 | get => _RecordConverter; 33 | set => _RecordConverter = value ?? throw new ArgumentNullException(nameof(value)); 34 | } 35 | 36 | /// 37 | public IQueryable CreateQuery(Expression expression) 38 | { 39 | var queryableType = expression.Type.GetInterfaces() 40 | .First(i => i.IsConstructedGenericType && i.GetGenericTypeDefinition() == typeof(IQueryable<>)); 41 | var elementType = queryableType.GenericTypeArguments[0]; 42 | // Note: CreateInstance returns null for Nullable, e.g. CreateInstance(typeof(int?)) returns null. 43 | // c.f. https://github.com/dotnet/runtime/blob/master/src/libraries/System.Private.CoreLib/src/System/Activator.RuntimeType.cs#L19 44 | return (IQueryable)Activator.CreateInstance(typeof(CargoRecordQueryable<>).MakeGenericType(elementType), this, expression)!; 45 | } 46 | 47 | /// 48 | public IQueryable CreateQuery(Expression expression) 49 | { 50 | return new CargoRecordQueryable(this, expression); 51 | } 52 | 53 | /// 54 | public object Execute(Expression expression) 55 | { 56 | throw new NotSupportedException(); 57 | } 58 | 59 | /// 60 | public TResult Execute(Expression expression) 61 | { 62 | throw new NotSupportedException(); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /WikiClientLibrary.Cargo/Linq/CargoRecordQueryableExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace WikiClientLibrary.Cargo.Linq; 2 | 3 | public static class CargoRecordQueryableExtensions 4 | { 5 | 6 | /// 7 | /// Casts the input instance into instance. 8 | /// 9 | /// type of the item. 10 | /// the input queryable that should have implemented . 11 | /// input does not implement . 12 | /// the same object reference as . 13 | /// This method is expected to have exactly the same behavior as . 14 | public static IAsyncEnumerable AsAsyncEnumerable(this IQueryable queryable) 15 | { 16 | return queryable is IAsyncEnumerable asyncEnumerable 17 | ? asyncEnumerable 18 | : throw new InvalidOperationException("Input IQueryable does not implement IAsyncEnumerable."); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /WikiClientLibrary.Cargo/Linq/CargoRecordSet.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Diagnostics; 3 | using System.Linq.Expressions; 4 | using WikiClientLibrary.Cargo.Linq.IntermediateExpressions; 5 | 6 | namespace WikiClientLibrary.Cargo.Linq; 7 | 8 | /// 9 | /// A queryable Cargo table instance. 10 | /// 11 | /// type of the model. 12 | public interface ICargoRecordSet : IQueryable 13 | { 14 | 15 | string Name { get; } 16 | 17 | } 18 | 19 | internal class CargoRecordSet : ICargoRecordSet 20 | { 21 | 22 | private readonly CargoRecordQueryable _rootQueryable; 23 | 24 | public CargoRecordSet(CargoModel model, CargoQueryProvider provider) 25 | { 26 | Debug.Assert(model != null); 27 | Debug.Assert(provider != null); 28 | Debug.Assert(model.ClrType == typeof(T)); 29 | _rootQueryable = new CargoRecordQueryable(provider, new CargoQueryExpression(model)); 30 | Model = model; 31 | Provider = provider; 32 | } 33 | 34 | public CargoModel Model { get; } 35 | 36 | /// 37 | Type IQueryable.ElementType => _rootQueryable.ElementType; 38 | 39 | public CargoQueryProvider Provider { get; } 40 | 41 | /// 42 | public string Name => Model.Name; 43 | 44 | /// 45 | Expression IQueryable.Expression => _rootQueryable.Expression; 46 | 47 | /// 48 | IQueryProvider IQueryable.Provider => _rootQueryable.Provider; 49 | 50 | /// 51 | IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_rootQueryable).GetEnumerator(); 52 | 53 | /// 54 | IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_rootQueryable).GetEnumerator(); 55 | 56 | } 57 | -------------------------------------------------------------------------------- /WikiClientLibrary.Cargo/Linq/ExpressionVisitors/CargoPreEvaluationTranslator.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Linq.Expressions; 3 | using WikiClientLibrary.Cargo.Linq.IntermediateExpressions; 4 | 5 | namespace WikiClientLibrary.Cargo.Linq.ExpressionVisitors; 6 | 7 | public class CargoPreEvaluationTranslator : ExpressionVisitor 8 | { 9 | 10 | /// 11 | protected override Expression VisitMethodCall(MethodCallExpression node) 12 | { 13 | if (node.Object == null && node.Method.DeclaringType == typeof(CargoFunctions)) 14 | { 15 | return node.Method.Name switch 16 | { 17 | nameof(CargoFunctions.Like) => new CargoBinaryOperationExpression(" LIKE ", Visit(node.Arguments[0]), 18 | Visit(node.Arguments[1]), node.Type), 19 | nameof(CargoFunctions.Holds) => 20 | new CargoBinaryOperationExpression(" HOLDS ", Visit(node.Arguments[0]), Visit(node.Arguments[1]), node.Type), 21 | nameof(CargoFunctions.HoldsLike) => new CargoBinaryOperationExpression(" HOLDS LIKE ", Visit(node.Arguments[0]), 22 | Visit(node.Arguments[1]), 23 | node.Type), 24 | nameof(CargoFunctions.DateDiff) => new CargoFunctionExpression("DATEDIFF", typeof(int), Visit(node.Arguments[0]), 25 | Visit(node.Arguments[1])), 26 | _ => throw new NotImplementedException($"CargoFunction call is not implemented: {node.Method}."), 27 | }; 28 | } 29 | return base.VisitMethodCall(node); 30 | } 31 | 32 | /// 33 | protected override Expression VisitMember(MemberExpression node) 34 | { 35 | var declaringType = node.Member.DeclaringType; 36 | if (declaringType == null) return base.VisitMember(node); 37 | if (node.Member.Name == nameof(Nullable.Value)) 38 | { 39 | var nullableUnderlyingType = Nullable.GetUnderlyingType(declaringType); 40 | if (nullableUnderlyingType != null) 41 | { 42 | Debug.Assert(node.Expression != null); 43 | // Normalize `field.Value` into `(T)field`. 44 | return Expression.Convert(node.Expression, nullableUnderlyingType); 45 | } 46 | } 47 | else if (declaringType == typeof(DateTime) || declaringType == typeof(DateTimeOffset)) 48 | { 49 | switch (node.Member.Name) 50 | { 51 | case nameof(DateTimeOffset.Now): 52 | // Converts "NOW" as NOW() server-side function call. 53 | return new CargoFunctionExpression("NOW", node.Member.DeclaringType!); 54 | } 55 | } 56 | return base.VisitMember(node); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /WikiClientLibrary.Cargo/Linq/ExpressionVisitors/ExpressionEqualityComparer.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | 3 | namespace WikiClientLibrary.Cargo.Linq.ExpressionVisitors; 4 | 5 | internal class ExpressionEqualityComparer : EqualityComparer 6 | { 7 | 8 | public static new ExpressionEqualityComparer Default { get; } = new ExpressionEqualityComparer(); 9 | 10 | /// 11 | public override bool Equals(Expression? x, Expression? y) 12 | { 13 | if (ReferenceEquals(x, y)) return true; 14 | if (x == null || y == null) return false; 15 | switch (x) 16 | { 17 | case ConstantExpression cex: 18 | if (y is ConstantExpression cey) 19 | return cex.NodeType == cey.NodeType && cex.Type == cey.Type && Equals(cex.Value, cey.Value); 20 | return false; 21 | case BinaryExpression bex: 22 | if (y is BinaryExpression bey) 23 | return bex.NodeType == bey.NodeType && Equals(bex.Method, bey.Method) && Equals(bex.Left, bey.Left) && 24 | Equals(bex.Right, bey.Right); 25 | return false; 26 | case MemberExpression mex: 27 | if (y is MemberExpression mey) 28 | return mex.NodeType == mey.NodeType && Equals(mex.Member, mey.Member) && mex.Type == mey.Type && 29 | Equals(mex.Expression, mey.Expression); 30 | return false; 31 | } 32 | return x.Equals(y); 33 | } 34 | 35 | /// 36 | public override int GetHashCode(Expression? obj) 37 | { 38 | return obj switch 39 | { 40 | ConstantExpression ce => HashCode.Combine(ce.NodeType, ce.Type, ce.Value), 41 | BinaryExpression be => HashCode.Combine(be.NodeType, GetHashCode(be.Left), GetHashCode(be.Right)), 42 | MemberExpression me => HashCode.Combine(me.NodeType, GetHashCode(me.Expression), me.Member, me.Type), 43 | null => 0, 44 | _ => obj.GetHashCode(), 45 | }; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /WikiClientLibrary.Cargo/Linq/ExpressionVisitors/ExpressionVisitorUtility.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | 3 | namespace WikiClientLibrary.Cargo.Linq.ExpressionVisitors; 4 | 5 | internal static class ExpressionVisitorUtility 6 | { 7 | 8 | public static LambdaExpression UnwindLambdaExpression(Expression lambdaOrQuote) 9 | { 10 | return lambdaOrQuote switch 11 | { 12 | null => throw new ArgumentNullException(nameof(lambdaOrQuote)), 13 | LambdaExpression lambda => lambda, 14 | UnaryExpression unary when unary.Operand is LambdaExpression lambda1 && unary.NodeType == ExpressionType.Quote => lambda1, 15 | _ => throw new ArgumentException($"Provided expression cannot be unwound to LambdaExpression: {lambdaOrQuote}.", 16 | nameof(lambdaOrQuote)), 17 | }; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /WikiClientLibrary.Cargo/Linq/ExpressionVisitors/MemberAccessExpressionReplacer.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Linq.Expressions; 3 | using System.Reflection; 4 | 5 | namespace WikiClientLibrary.Cargo.Linq.ExpressionVisitors; 6 | 7 | /// 8 | /// Replaces the member access (read) expression to a specified into the specified expression. 9 | /// 10 | public class MemberAccessExpressionReplacer : ExpressionVisitor 11 | { 12 | 13 | public MemberAccessExpressionReplacer(ParameterExpression target, IReadOnlyDictionary memberReplacements) 14 | { 15 | Debug.Assert(target != null); 16 | Target = target; 17 | MemberReplacements = memberReplacements; 18 | } 19 | 20 | public ParameterExpression Target { get; } 21 | 22 | public IReadOnlyDictionary MemberReplacements { get; } 23 | 24 | /// 25 | protected override Expression VisitMember(MemberExpression node) 26 | { 27 | if (node.Expression == Target) 28 | { 29 | return MemberReplacements[node.Member]; 30 | } 31 | return base.VisitMember(node); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /WikiClientLibrary.Cargo/Linq/IntermediateExpressions/CargoBinaryOperationExpression.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Linq.Expressions; 3 | 4 | namespace WikiClientLibrary.Cargo.Linq.IntermediateExpressions; 5 | 6 | internal sealed class CargoBinaryOperationExpression : CargoSqlExpression 7 | { 8 | 9 | public CargoBinaryOperationExpression(string @operator, Expression left, Expression right, Type type) 10 | { 11 | Debug.Assert(@operator != null); 12 | Debug.Assert(left != null); 13 | Debug.Assert(right != null); 14 | Debug.Assert(type != null); 15 | Operator = @operator; 16 | Left = left; 17 | Right = right; 18 | Type = type; 19 | } 20 | 21 | public string Operator { get; } 22 | 23 | public Expression Left { get; } 24 | 25 | public Expression Right { get; } 26 | 27 | /// 28 | public override Type Type { get; } 29 | 30 | /// 31 | protected override Expression VisitChildren(ExpressionVisitor visitor) 32 | { 33 | var left = visitor.Visit(Left); 34 | var right = visitor.Visit(Right); 35 | return Update(left, right); 36 | } 37 | 38 | public CargoBinaryOperationExpression Update(Expression left, Expression right) 39 | { 40 | if (Left == left && Right == right) 41 | return this; 42 | return new CargoBinaryOperationExpression(Operator, left, right, Type); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /WikiClientLibrary.Cargo/Linq/IntermediateExpressions/CargoFunctionExpression.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Linq.Expressions; 3 | 4 | namespace WikiClientLibrary.Cargo.Linq.IntermediateExpressions; 5 | 6 | internal sealed class CargoFunctionExpression : CargoSqlExpression 7 | { 8 | 9 | public CargoFunctionExpression(string name, Type type, params Expression[] arguments) 10 | : this(name, type, (IEnumerable)arguments) 11 | { 12 | } 13 | 14 | public CargoFunctionExpression(string name, Type type, IEnumerable arguments) 15 | { 16 | Name = name; 17 | Type = type; 18 | Arguments = arguments.ToImmutableList(); 19 | } 20 | 21 | public string Name { get; } 22 | 23 | /// 24 | public override Type Type { get; } 25 | 26 | public IImmutableList Arguments { get; } 27 | 28 | /// 29 | protected override Expression VisitChildren(ExpressionVisitor visitor) 30 | { 31 | var argBuilder = Arguments.ToImmutableList().ToBuilder(); 32 | for (int i = 0; i < Arguments.Count; i++) 33 | { 34 | var a = visitor.Visit(Arguments[i]); 35 | if (a != Arguments[i]) argBuilder[i] = a; 36 | } 37 | return Update(argBuilder.ToImmutable()); 38 | } 39 | 40 | public CargoFunctionExpression Update(IEnumerable arguments) 41 | { 42 | if (ReferenceEquals(Arguments, arguments)) 43 | return this; 44 | return new CargoFunctionExpression(Name, Type, arguments); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /WikiClientLibrary.Cargo/Linq/IntermediateExpressions/CargoSqlExpression.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | 3 | namespace WikiClientLibrary.Cargo.Linq.IntermediateExpressions; 4 | 5 | /// 6 | /// Base expression type for all the SQL expression segment that can be directly translated into Cargo query parameter. 7 | /// 8 | internal abstract class CargoSqlExpression : Expression 9 | { 10 | 11 | /// 12 | public override bool CanReduce => false; 13 | 14 | /// 15 | public override ExpressionType NodeType => ExpressionType.Extension; 16 | 17 | /// 18 | protected override Expression VisitChildren(ExpressionVisitor visitor) => this; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /WikiClientLibrary.Cargo/Linq/IntermediateExpressions/FieldRefExpression.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace WikiClientLibrary.Cargo.Linq.IntermediateExpressions; 4 | 5 | /// 6 | /// Reference to a field (i.e. model property) in a Cargo table. 7 | /// 8 | internal class FieldRefExpression : CargoSqlExpression 9 | { 10 | 11 | public FieldRefExpression(string tableAlias, CargoModelProperty property) 12 | { 13 | Debug.Assert(!string.IsNullOrEmpty(tableAlias)); 14 | Debug.Assert(property != null); 15 | TableAlias = tableAlias; 16 | Property = property; 17 | } 18 | 19 | /// 20 | public override Type Type => Property.ClrType; 21 | 22 | /// Table name or its alias. 23 | public string TableAlias { get; } 24 | 25 | /// Model metadata. 26 | public new CargoModelProperty Property { get; } 27 | 28 | public string FieldName => Property.Name; 29 | 30 | /// 31 | public override string ToString() => $"{TableAlias}.{FieldName}"; 32 | 33 | } 34 | -------------------------------------------------------------------------------- /WikiClientLibrary.Cargo/Linq/IntermediateExpressions/OrderByExpression.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | 3 | namespace WikiClientLibrary.Cargo.Linq.IntermediateExpressions; 4 | 5 | internal class OrderByExpression : CargoSqlExpression 6 | { 7 | 8 | public OrderByExpression(Expression expression) 9 | : this(expression, false) 10 | { 11 | } 12 | 13 | public OrderByExpression(Expression expression, bool descending) 14 | { 15 | Expression = expression; 16 | Descending = @descending; 17 | } 18 | 19 | /// 20 | public override Type Type => typeof(void); 21 | 22 | /// Sort by this expression. 23 | public Expression Expression { get; } 24 | 25 | public bool Descending { get; } 26 | 27 | /// 28 | public override string ToString() => Expression + (Descending ? " DESC" : " ASC"); 29 | 30 | } 31 | -------------------------------------------------------------------------------- /WikiClientLibrary.Cargo/Linq/IntermediateExpressions/ProjectionExpression.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using System.Reflection; 3 | 4 | namespace WikiClientLibrary.Cargo.Linq.IntermediateExpressions; 5 | 6 | /// 7 | /// Projection of a SQL expression into a specific alias ( AS ). 8 | /// 9 | internal class ProjectionExpression : CargoSqlExpression 10 | { 11 | 12 | public ProjectionExpression(Expression expression, string alias, MemberInfo targetMember) 13 | { 14 | if (expression == null) throw new ArgumentNullException(nameof(expression)); 15 | if (alias == null) throw new ArgumentNullException(nameof(alias)); 16 | if (alias.Length == 0) throw new ArgumentException("Parameter should not be empty.", nameof(alias)); 17 | if (targetMember == null) throw new ArgumentNullException(nameof(targetMember)); 18 | Expression = expression; 19 | Alias = alias; 20 | TargetMember = targetMember; 21 | } 22 | 23 | /// 24 | public override Type Type => typeof(void); 25 | 26 | /// The field access or other complex evaluation expression to be aliased. 27 | public Expression Expression { get; } 28 | 29 | /// The (mangled) alias of the SQL expression. 30 | public string Alias { get; } 31 | 32 | /// When populating the model object, this is the property / field to be populated. 33 | public MemberInfo TargetMember { get; } 34 | 35 | /// 36 | public override string ToString() => $"{Expression} AS {Alias}"; 37 | 38 | public static string MangleAlias(string alias) 39 | { 40 | if (alias == null) 41 | throw new ArgumentNullException(nameof(alias)); 42 | if (alias.Length == 0) return alias; 43 | // Aliases starting with underscore causes error with Cargo API. 44 | if (alias[0] == '_' || alias.StartsWith("wcl_", StringComparison.OrdinalIgnoreCase)) 45 | return "wcl_p" + alias; 46 | return alias; 47 | } 48 | 49 | public static string UnmangleAlias(string alias) 50 | { 51 | if (alias == null) 52 | throw new ArgumentNullException(nameof(alias)); 53 | if (alias.StartsWith("wcl_p")) 54 | return alias[5..]; 55 | return alias; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /WikiClientLibrary.Cargo/Linq/IntermediateExpressions/TableProjectionExpression.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace WikiClientLibrary.Cargo.Linq.IntermediateExpressions; 4 | 5 | /// 6 | /// Represents a table name and its alias as specified in SQL WHERE clause. 7 | /// 8 | internal class TableProjectionExpression : CargoSqlExpression 9 | { 10 | 11 | public TableProjectionExpression(CargoModel model, string tableAlias) 12 | { 13 | Debug.Assert(model != null); 14 | Debug.Assert(!string.IsNullOrEmpty(tableAlias)); 15 | Model = model; 16 | TableAlias = tableAlias; 17 | } 18 | 19 | public CargoModel Model { get; } 20 | 21 | public string TableName => Model.Name; 22 | 23 | public string TableAlias { get; } 24 | 25 | /// 26 | public override string ToString() => $"{TableName} AS {TableAlias}"; 27 | 28 | } 29 | -------------------------------------------------------------------------------- /WikiClientLibrary.Cargo/Schema/CargoSpecialColumnNames.cs: -------------------------------------------------------------------------------- 1 | namespace WikiClientLibrary.Cargo.Schema; 2 | 3 | /// 4 | /// Special column names as listed on mw:Extension:Cargo/Storing data#Database storage details 5 | /// 6 | public static class CargoSpecialColumnNames 7 | { 8 | 9 | /// Holds the name of the page from which this row of values was stored. 10 | public const string PageName = "_pageName"; 11 | 12 | /// Similar to , but leaves out the namespace, if there is one. 13 | public const string PageTitle = "_pageTitle"; 14 | 15 | /// Holds the numerical ID of the namespace of the page from which this row of values was stored. 16 | public const string PageNamespace = "_pageNamespace"; 17 | 18 | /// Holds the internal MediaWiki ID for that page. 19 | public const string PageId = "_pageID"; 20 | 21 | /// Holds a unique ID for this row. 22 | public const string Id = "_ID"; 23 | 24 | /// (__[ListColumnName] table) Holds the ID of the row (i.e., _ID) in the main table that this value corresponds to. 25 | public const string RowId = "_rowID"; 26 | 27 | /// 28 | /// (__[ListColumnName] table) Holds the actual, individual value. 29 | /// (__[ColumnName]_hierarchy table) The allowed value. 30 | /// 31 | public const string Value = "_value"; 32 | 33 | /// (__[ListColumnName] table) Holds the position of this value in the list (can be 1, 2, etc.) 34 | public const string Position = "_position"; 35 | 36 | /// (__[ColumnName]_hierarchy table) The number of the leftmost node represented by this value. 37 | public const string Left = "_left"; 38 | 39 | /// (__[ColumnName]_hierarchy table) The number of the rightmost node represented by this value. 40 | public const string Right = "_right"; 41 | 42 | /// (__files table) The name of the relevant field of type "File". 43 | public const string FieldName = "_fieldName"; 44 | 45 | /// (__files table) The value of the field, i.e. the name of an uploaded file. 46 | public const string FileName = "_fileName"; 47 | 48 | } 49 | -------------------------------------------------------------------------------- /WikiClientLibrary.Cargo/Schema/CargoTableDefinition.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using System.Text; 3 | 4 | namespace WikiClientLibrary.Cargo.Schema; 5 | 6 | /// 7 | /// Represents the schema of a Cargo table. 8 | /// 9 | public class CargoTableDefinition 10 | { 11 | 12 | public CargoTableDefinition(string name, IEnumerable fields) 13 | { 14 | if (name == null) 15 | throw new ArgumentNullException(nameof(name)); 16 | if (name == "") 17 | throw new ArgumentException("Value cannot be null or empty.", nameof(name)); 18 | Name = name; 19 | Fields = new ReadOnlyCollection(fields.ToList()); 20 | } 21 | 22 | public string Name { get; } 23 | 24 | public IReadOnlyList Fields { get; } 25 | 26 | /// 27 | public override string ToString() 28 | { 29 | var sb = new StringBuilder(Name.Length + Fields.Count * 10); 30 | sb.Append(Name); 31 | foreach (var f in Fields) 32 | { 33 | sb.Append(" |"); 34 | sb.Append(f); 35 | } 36 | return sb.ToString(); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /WikiClientLibrary.Cargo/Schema/DataAnnotations/CargoListAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace WikiClientLibrary.Cargo.Schema.DataAnnotations; 2 | 3 | /// 4 | /// Annotates the property as a cargo list, i.e. List () of [type]. 5 | /// 6 | /// 7 | /// All the collection properties without 8 | /// will use default settings, i.e. using comma (,) as list separator. 9 | /// 10 | [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] 11 | public class CargoListAttribute : Attribute 12 | { 13 | 14 | public const string DefaultDelimiter = ","; 15 | 16 | /// Initialize the attribute with comma (,) as list separator. 17 | public CargoListAttribute() 18 | : this(DefaultDelimiter) 19 | { 20 | } 21 | 22 | public CargoListAttribute(string delimiter) 23 | { 24 | Delimiter = delimiter ?? throw new ArgumentNullException(nameof(delimiter)); 25 | } 26 | 27 | public string Delimiter { get; } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /WikiClientLibrary.Cargo/WikiClientLibrary.Cargo.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | WikiClientLibrary.Cargo 6 | CXuesong.MW.WikiClientLibrary.Cargo 7 | 8 | WikiClientLibrary.Cargo is a .NET Standard & asynchronous client library for MediaWiki sites with Cargo support. 9 | 10 | $(PackageTags) Cargo 11 | Enable 12 | Enable 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /WikiClientLibrary.Commons/CI/Build.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [string] 3 | $Configuration = "CIRelease" 4 | ) 5 | trap { 6 | Write-Error $_ 7 | Write-Host $_.ScriptStackTrace 8 | Exit 1 9 | } 10 | # Assumes $PWD is the repo root 11 | if ($env:BUILD_SECRET_KEY) { 12 | &"$PSScriptRoot/BuildSecret.ps1" -Restore -SourceRootPath . -SecretPath $PSScriptRoot/Secret.bin -Key $env:BUILD_SECRET_KEY 13 | $env:BUILD_SECRET_KEY = "_DUMMY_" 14 | } 15 | else { 16 | Write-Warning "BUILD_SECRET_KEY is not available. Will build without secret." 17 | } 18 | 19 | dotnet build WikiClientLibrary.sln -c $Configuration 20 | 21 | $BuildResult = $LASTEXITCODE 22 | if ($env:BUILD_SECRET_KEY) { 23 | &"$PSScriptRoot/BuildSecret.ps1" -Clear -SourceRootPath . 24 | } 25 | 26 | Exit $BuildResult 27 | -------------------------------------------------------------------------------- /WikiClientLibrary.Commons/CI/CollectArtifacts.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Collects built packages as artifacts. 4 | Assumes $PWD is the repo root. 5 | #> 6 | param ( 7 | [string] 8 | $Configuration = "Release", 9 | 10 | [string] 11 | $ArtifactPath = "./CollectedArtifacts" 12 | ) 13 | 14 | trap { 15 | Write-Error $_ 16 | Write-Host $_.ScriptStackTrace 17 | Exit 1 18 | } 19 | $ErrorActionPreference = "Stop" 20 | $LASTEXITCODE = 0 21 | $PackageProjects = @( 22 | "WikiClientLibrary", 23 | "WikiClientLibrary.Cargo", 24 | "WikiClientLibrary.Flow", 25 | "WikiClientLibrary.Wikia", 26 | "WikiClientLibrary.Wikibase" 27 | ) 28 | 29 | $ArtifactRoot = New-Item $ArtifactPath -ItemType Directory 30 | 31 | Write-Host "Copy packages." 32 | foreach ($proj in $PackageProjects) { 33 | $OutDir = Resolve-Path "./$proj/bin/$Configuration/" 34 | $PackageFile = Get-ChildItem $OutDir -Filter "*.$proj.*.nupkg" 35 | Copy-Item $PackageFile $ArtifactRoot -Force 36 | } 37 | 38 | Write-Host "Collected artifacts:" 39 | Get-ChildItem $ArtifactRoot 40 | -------------------------------------------------------------------------------- /WikiClientLibrary.Commons/CI/PrepEnv.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [switch] 3 | $SHFB = $false 4 | ) 5 | 6 | $ErrorActionPreference = "Stop" 7 | 8 | trap { 9 | Write-Error $_ 10 | Write-Host $_.ScriptStackTrace 11 | Exit 1 12 | } 13 | 14 | function CheckLastExitCode($ExitCode = $LASTEXITCODE) { 15 | if ($ExitCode) { 16 | Write-Host (Get-PSCallStack) 17 | Exit $ExitCode 18 | } 19 | } 20 | 21 | # Assumes $PWD is the repo root 22 | if ($IsLinux) { 23 | if ($SHFB) { 24 | Write-Error "SHFB is not supported on Linux." 25 | } 26 | } 27 | elseif ($IsWindows) { 28 | # SHFB 29 | if ($SHFB) { 30 | Write-Host "Downloading SHFB." 31 | Invoke-WebRequest "https://github.com/EWSoftware/SHFB/releases/download/2024.2.18.0/SHFBInstaller_2024.2.18.0.zip" -OutFile SHFBInstaller.zip 32 | New-Item -ItemType Directory SHFBInstaller | Out-Null 33 | Expand-Archive SHFBInstaller.zip SHFBInstaller 34 | Write-Host "Installing SHFB." 35 | $proc = Start-Process -PassThru -Wait -FilePath ./SHFBInstaller/InstallResources/SandcastleHelpFileBuilder.msi -ArgumentList /quiet, /lwe, SHFBInstall.log 36 | Get-Content SHFBInstall.log 37 | CheckLastExitCode ($proc.ExitCode) 38 | } 39 | } 40 | else { 41 | Write-Error "Invalid Environment." 42 | } 43 | -------------------------------------------------------------------------------- /WikiClientLibrary.Commons/CI/Secret.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CXuesong/WikiClientLibrary/6bb0095713c41cab297dfc58cf35aee9445117c9/WikiClientLibrary.Commons/CI/Secret.bin -------------------------------------------------------------------------------- /WikiClientLibrary.Commons/CI/Test.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [string] 3 | $Configuration = "Release" 4 | ) 5 | trap { 6 | Write-Error $_ 7 | Exit 1 8 | } 9 | # Assumes $PWD is the repo root 10 | if ($env:BUILD_SECRET_KEY) { 11 | dotnet test ./UnitTestProject1/UnitTestProject1.csproj ` 12 | --no-build --filter "CI!=Skipped" -c $Configuration ` 13 | --logger "console;verbosity=normal" ` 14 | -- RunConfiguration.TestSessionTimeout=1800000 15 | Exit $LASTEXITCODE 16 | } 17 | else { 18 | Write-Warning "BUILD_SECRET_KEY is not available. Will not execute tests." 19 | return 0 20 | } 21 | -------------------------------------------------------------------------------- /WikiClientLibrary.Commons/WikiClientLibrary.Commons.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | latest 4 | 5 | 6 | 7 | $(DefineConstants);ENV_CI_BUILD 8 | 9 | True 10 | 11 | 12 | 13 | $(DefineConstants);BCL_FEATURE_REQUIRED_MEMBER;BCL_FEATURE_KVP_TO_DICTIONARY 14 | 15 | -------------------------------------------------------------------------------- /WikiClientLibrary.Commons/WikiClientLibrary.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | net6.0;net8.0 7 | 0.9.0-int.4 8 | 0.9.0.4 9 | 0.9.0.4 10 | Copyright (C) CXuesong 2025 11 | See https://github.com/CXuesong/WikiClientLibrary/releases . 12 | MediaWiki API Client 13 | en-us 14 | Apache-2.0 15 | https://github.com/CXuesong/WikiClientLibrary 16 | https://github.com/CXuesong/WikiClientLibrary 17 | git 18 | True 19 | True 20 | CXuesong 21 | 22 | $(NoWarn);1701;1702;1705;1573;1591;NU5105 23 | true 24 | $(MSBuildThisFileDirectory)\WikiClientLibrary.snk 25 | True 26 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /WikiClientLibrary.Commons/WikiClientLibrary.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CXuesong/WikiClientLibrary/6bb0095713c41cab297dfc58cf35aee9445117c9/WikiClientLibrary.Commons/WikiClientLibrary.snk -------------------------------------------------------------------------------- /WikiClientLibrary.Flow/Enums.cs: -------------------------------------------------------------------------------- 1 | namespace WikiClientLibrary.Flow; 2 | 3 | /// 4 | /// Actions used for moderating a Flow topic or post. 5 | /// 6 | public enum ModerationAction 7 | { 8 | 9 | Delete, 10 | Hide, 11 | Suppress, 12 | Restore, 13 | Unhide, 14 | Undelete, 15 | Unsuppress, 16 | 17 | } 18 | 19 | /// 20 | /// Moderation state of a Flow topic or post. 21 | /// 22 | public enum ModerationState 23 | { 24 | 25 | /// Not moderated. 26 | None = 0, 27 | 28 | /// An unknown moderation state. 29 | Unknown, 30 | 31 | /// Deleted. 32 | Deleted, 33 | 34 | /// Hidden. 35 | Hidden, 36 | 37 | /// Suppressed. 38 | Suppressed, 39 | 40 | } 41 | 42 | /// 43 | /// The locking operations to perform on a Flow topic. 44 | /// 45 | public enum LockAction 46 | { 47 | 48 | Lock = 0, 49 | Unlock = 1, 50 | 51 | } 52 | 53 | internal static partial class EnumParser 54 | { 55 | 56 | public static string ToString(ModerationAction value) 57 | { 58 | return value switch 59 | { 60 | ModerationAction.Delete => "delete", 61 | ModerationAction.Hide => "hide", 62 | ModerationAction.Suppress => "suppress", 63 | ModerationAction.Restore => "restore", 64 | ModerationAction.Unhide => "unhide", 65 | ModerationAction.Undelete => "undelete", 66 | ModerationAction.Unsuppress => "unsuppress", 67 | _ => throw new ArgumentOutOfRangeException(nameof(value), value, null), 68 | }; 69 | } 70 | 71 | public static string ToString(LockAction value) 72 | { 73 | return value switch 74 | { 75 | LockAction.Lock => "lock", 76 | LockAction.Unlock => "unlock", 77 | _ => throw new ArgumentOutOfRangeException(nameof(value), value, null), 78 | }; 79 | } 80 | 81 | public static ModerationState ParseModerationState(string value) 82 | { 83 | return value switch 84 | { 85 | "delete" => ModerationState.Deleted, 86 | "hide" => ModerationState.Hidden, 87 | "suppress" => ModerationState.Suppressed, 88 | _ => ModerationState.Unknown, 89 | }; 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /WikiClientLibrary.Flow/FlowNamespaces.cs: -------------------------------------------------------------------------------- 1 | namespace WikiClientLibrary.Flow; 2 | 3 | public static class FlowNamespaces 4 | { 5 | 6 | public const int Topic = 2600; 7 | 8 | } 9 | -------------------------------------------------------------------------------- /WikiClientLibrary.Flow/FlowRequestHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Text.Json.Nodes; 3 | using WikiClientLibrary.Client; 4 | using WikiClientLibrary.Sites; 5 | 6 | namespace WikiClientLibrary.Flow; 7 | 8 | internal static class FlowRequestHelper 9 | { 10 | 11 | public static async Task ReplyAsync(WikiSite site, string pageTitle, string workflowId, 12 | string content, CancellationToken cancellationToken) 13 | { 14 | Debug.Assert(site != null); 15 | Debug.Assert(pageTitle != null); 16 | Debug.Assert(workflowId != null); 17 | Debug.Assert(content != null); 18 | JsonNode jresult; 19 | using (await site.ModificationThrottler.QueueWorkAsync("Reply: " + workflowId, cancellationToken)) 20 | { 21 | jresult = await site.InvokeMediaWikiApiAsync( 22 | new MediaWikiFormRequestMessage(new 23 | { 24 | action = "flow", 25 | submodule = "reply", 26 | page = pageTitle, 27 | token = WikiSiteToken.Edit, 28 | repreplyTo = workflowId, 29 | repformat = "wikitext", 30 | repcontent = content, 31 | }), cancellationToken); 32 | } 33 | var jtopic = jresult["flow"]["reply"]["committed"]?["topic"]; 34 | if (jtopic == null) 35 | throw new UnexpectedDataException("Missing flow.reply.committed.topic JSON node in MW API response."); 36 | var rep = new Post(site, pageTitle, (string)jtopic["post-id"]); 37 | return rep; 38 | } 39 | 40 | public static async Task NewTopicAsync(WikiSite site, string pageTitle, 41 | string topicTitle, string topicContent, CancellationToken cancellationToken) 42 | { 43 | Debug.Assert(site != null); 44 | Debug.Assert(pageTitle != null); 45 | Debug.Assert(topicTitle != null); 46 | Debug.Assert(topicContent != null); 47 | JsonNode jresult; 48 | using (await site.ModificationThrottler.QueueWorkAsync("New topic", cancellationToken)) 49 | { 50 | jresult = await site.InvokeMediaWikiApiAsync( 51 | new MediaWikiFormRequestMessage(new 52 | { 53 | action = "flow", 54 | submodule = "new-topic", 55 | page = pageTitle, 56 | token = WikiSiteToken.Edit, 57 | nttopic = topicTitle, 58 | ntformat = "wikitext", 59 | ntcontent = topicContent, 60 | }), cancellationToken); 61 | } 62 | var jtopiclist = jresult["flow"]["new-topic"]?["committed"]?["topiclist"]; 63 | if (jtopiclist == null) 64 | throw new UnexpectedDataException("Missing flow.new-topic.committed.topiclist JSON node in MW API response."); 65 | var rep = new Topic(site, (string)jtopiclist["topic-page"]); 66 | return rep; 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /WikiClientLibrary.Flow/NamespaceDocs.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace WikiClientLibrary.Flow; 4 | 5 | /// 6 | /// The root namespace for 7 | /// StructuredDiscussions 8 | /// (previously known as "Flow") extension support. 9 | /// 10 | /// 11 | /// Flow is currently deployed on several talk pages on Mediawiki.org and on various language Wikipedias. 12 | /// Flow was uninstalled from English Wikipedia in November 2016, by request from the community. 13 | /// You might encounter it on other Wikimedia wikis however. 14 | /// There are currently no plans for it to return to English Wikipedia. 15 | /// For more information, see m:Wikipedia:Flow. 16 | /// 17 | [CompilerGenerated] 18 | internal class NamespaceDoc 19 | { 20 | 21 | } 22 | -------------------------------------------------------------------------------- /WikiClientLibrary.Flow/WikiClientLibrary.Flow.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | WikiClientLibrary.Flow 6 | CXuesong.MW.WikiClientLibrary.Flow 7 | 8 | WikiClientLibrary.Flow is a .NET Standard & asynchronous client library for MediaWiki sites with Structured Discussions (aka. Flow) support. 9 | 10 | $(PackageTags) Flow StructuredDiscussions 11 | Enable 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /WikiClientLibrary.Wikia/Discussions/DiscussionsExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace WikiClientLibrary.Wikia.Discussions; 2 | 3 | public static class DiscussionsExtensions 4 | { 5 | 6 | /// 7 | /// 8 | public static Task RefreshAsync(this IEnumerable posts) 9 | { 10 | return RefreshAsync(posts, PostQueryOptions.None, new CancellationToken()); 11 | } 12 | 13 | /// 14 | /// 15 | public static Task RefreshAsync(this IEnumerable posts, PostQueryOptions options) 16 | { 17 | return RefreshAsync(posts, options, new CancellationToken()); 18 | } 19 | 20 | /// 21 | /// Refreshes the post content from the server. 22 | /// 23 | /// The posts to be refreshed. 24 | /// The options used to fetch the post. 25 | /// The token used to cancel the operation. 26 | /// 27 | /// This method will not fetch replies for the . will remain unchanged after the invocation. 28 | /// 29 | /// 30 | public static Task RefreshAsync(this IEnumerable posts, PostQueryOptions options, CancellationToken cancellationToken) 31 | { 32 | if (posts == null) throw new ArgumentNullException(nameof(posts)); 33 | return RequestHelper.RefreshPostsAsync(posts, options, cancellationToken); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /WikiClientLibrary.Wikia/NamespaceDocs.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace WikiClientLibrary.Wikia 4 | { 5 | /// 6 | /// The root namespace for 7 | /// FANDOM 8 | /// and 9 | /// Wikia.org 10 | /// site-specific support. 11 | /// 12 | /// 13 | /// Wikia uses a modified MediaWiki fork based on MediaWiki v1.19. 14 | /// Since then, various public or non-publicized Wikia-specific web APIs were developed, 15 | /// including user-management, chatting, commenting and discussions. 16 | /// For now, the available API endpoints includes 17 | /// 18 | /// 19 | /// Wikia API v1 20 | /// This is the only publicized API. See its documentation at https://dev.fandom.com/api/v1. 21 | /// 22 | /// 23 | /// Nirvana API 24 | /// This API uses http://{prefix}.wikia.com/wikia.php as endpoint URL. 25 | /// 26 | /// 27 | /// AJAX API 28 | /// This API uses http://{prefix}.wikia.com/index.php?action=ajax as endpoint URL. 29 | /// 30 | /// 31 | /// For more information on these Wikia-specific API endpoints, 32 | /// see wikia:dev:Nirvana. 33 | /// 34 | /// 35 | [CompilerGenerated] 36 | internal class NamespaceDoc 37 | { 38 | 39 | } 40 | } 41 | 42 | namespace WikiClientLibrary.Wikia.Discussions 43 | { 44 | /// 45 | /// Contains classes for retrieving and creating comments on Wikia Message Wall and forums. 46 | /// 47 | [CompilerGenerated] 48 | internal class NamespaceDoc 49 | { 50 | 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /WikiClientLibrary.Wikia/Utility.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using WikiClientLibrary.Infrastructures; 3 | 4 | namespace WikiClientLibrary.Wikia; 5 | 6 | internal static class Utility 7 | { 8 | 9 | public static readonly JsonSerializerOptions WikiaApiJsonSerializerOptions = new() 10 | { 11 | PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, Converters = { new WikiReadOnlyDictionaryConverterFactory() }, 12 | }; 13 | 14 | /// 15 | /// Partitions into a sequence of , 16 | /// each child having the same length, except the last one. 17 | /// 18 | public static IEnumerable> Partition(this IEnumerable source, int partitionSize) 19 | { 20 | if (partitionSize <= 0) throw new ArgumentOutOfRangeException(nameof(partitionSize)); 21 | var list = new List(partitionSize); 22 | foreach (var item in source) 23 | { 24 | list.Add(item); 25 | if (list.Count == partitionSize) 26 | { 27 | yield return list; 28 | list.Clear(); 29 | } 30 | } 31 | if (list.Count > 0) yield return list; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /WikiClientLibrary.Wikia/WikiClientLibrary.Wikia.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | WikiClientLibrary.Wikia 6 | CXuesong.MW.WikiClientLibrary.Wikia 7 | 8 | WikiClientLibrary.Wikia is a .NET Standard & asynchronous client library for FANDOM and Wikia.org, 9 | which are using a customized MediaWiki branch based on MediaWiki 1.19. 10 | 11 | $(PackageTags) Wikia FANDOM 12 | Enable 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /WikiClientLibrary.Wikia/WikiaApi/LocalWikiSearchResultItem.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using WikiClientLibrary.Infrastructures; 3 | 4 | namespace WikiClientLibrary.Wikia.WikiaApi; 5 | 6 | /// 7 | /// Represents an item in the Wikia local wiki site search result. 8 | /// 9 | [JsonContract] 10 | public sealed class LocalWikiSearchResultItem 11 | { 12 | 13 | /// 14 | /// Id of the page. 15 | /// 16 | [JsonPropertyName("id")] 17 | public int Id { get; init; } 18 | 19 | /// 20 | /// Gets the full title of the page. 21 | /// 22 | [JsonPropertyName("title")] 23 | public string Title { get; init; } 24 | 25 | /// 26 | /// Absolute URL of the page. 27 | /// 28 | public string Url { get; init; } 29 | 30 | /// 31 | /// Namespace id of the page. 32 | /// 33 | [JsonPropertyName("ns")] 34 | public int NamespaceId { get; init; } 35 | 36 | /// 37 | /// Quality of matching. 38 | /// 39 | public int Quality { get; init; } 40 | 41 | /// 42 | /// Gets the parsed HTML snippet of the page. 43 | /// 44 | public string Snippet { get; init; } 45 | 46 | /// 47 | public override string ToString() 48 | { 49 | return $"[{Id}]{Title}"; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /WikiClientLibrary.Wikia/WikiaApi/RelatedPageItem.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using WikiClientLibrary.Infrastructures; 3 | 4 | namespace WikiClientLibrary.Wikia.WikiaApi; 5 | 6 | [JsonContract] 7 | public sealed class RelatedPageItem 8 | { 9 | 10 | /// Absolute URL of the page. 11 | [JsonInclude] 12 | [JsonPropertyName("url")] 13 | public string Url { get; private set; } 14 | 15 | /// Full title of the page. 16 | [JsonPropertyName("title")] 17 | public string Title { get; init; } 18 | 19 | /// ID of the page. 20 | [JsonPropertyName("id")] 21 | public int Id { get; init; } 22 | 23 | [JsonInclude] 24 | [JsonPropertyName("imgUrl")] 25 | public string ImageUrl { get; private set; } 26 | 27 | [JsonPropertyName("imgOriginalDimensions")] 28 | public string ImageOriginalDimensions { get; init; } 29 | 30 | /// Excerpt of the page. 31 | [JsonPropertyName("text")] 32 | public string Text { get; init; } 33 | 34 | internal void ApplyBasePath(string basePath) 35 | { 36 | if (Url != null) Url = MediaWikiHelper.MakeAbsoluteUrl(basePath, Url); 37 | if (ImageUrl != null) ImageUrl = MediaWikiHelper.MakeAbsoluteUrl(basePath, ImageUrl); 38 | } 39 | 40 | /// 41 | public override string ToString() => $"[{Id}]{Title}"; 42 | 43 | } 44 | -------------------------------------------------------------------------------- /WikiClientLibrary.Wikia/WikiaApi/UserInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using WikiClientLibrary.Infrastructures; 3 | 4 | namespace WikiClientLibrary.Wikia.WikiaApi; 5 | 6 | /// 7 | /// See https://github.com/Wikia/app/blob/dev/includes/wikia/api/UserApiController.class.php#L31-L87 . 8 | /// 9 | [JsonContract] 10 | public sealed class UserInfo 11 | { 12 | 13 | /// User ID. 14 | [JsonPropertyName("user_id")] 15 | public int Id { get; init; } 16 | 17 | /// User title. (Often the same as .) 18 | [JsonPropertyName("title")] 19 | public string Title { get; init; } 20 | 21 | /// User name. 22 | [JsonPropertyName("name")] 23 | public string Name { get; init; } 24 | 25 | /// The full URL of user's page. 26 | [JsonPropertyName("url")] 27 | [JsonInclude] 28 | public string UserPageUrl { get; private set; } 29 | 30 | [Obsolete("The field has been removed from Wikia v1 API response.")] 31 | [JsonIgnore] 32 | public ICollection PowerUserTypes { get; init; } = Array.Empty(); 33 | 34 | [JsonPropertyName("is_subject_to_ccpa")] 35 | public bool? IsSubjectToCcpa { get; init; } 36 | 37 | /// The full URL of user's avatar. 38 | [JsonPropertyName("avatar")] 39 | [JsonInclude] 40 | public string AvatarUrl { get; private set; } 41 | 42 | /// User's number of edits. 43 | [JsonPropertyName("numberofedits")] 44 | public int EditsCount { get; init; } 45 | 46 | internal void ApplyBasePath(string basePath) 47 | { 48 | if (UserPageUrl != null) UserPageUrl = MediaWikiHelper.MakeAbsoluteUrl(basePath, UserPageUrl); 49 | if (AvatarUrl != null) AvatarUrl = MediaWikiHelper.MakeAbsoluteUrl(basePath, AvatarUrl); 50 | } 51 | 52 | /// Creates a from the current user information. 53 | public UserStub ToUserStub() 54 | { 55 | return new UserStub(Name, Id); 56 | } 57 | 58 | /// 59 | public override string ToString() 60 | { 61 | return Name; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /WikiClientLibrary.Wikia/WikiaApiException.cs: -------------------------------------------------------------------------------- 1 | namespace WikiClientLibrary.Wikia; 2 | 3 | /// 4 | /// An exception that raises when received exception node 5 | /// in JSON response from Wikia API requests. 6 | /// 7 | public class WikiaApiException : WikiClientException 8 | { 9 | 10 | /// Wikia Exception type. 11 | public string ErrorType { get; } 12 | 13 | /// Wikia Exception message. 14 | public string ErrorMessage { get; } 15 | 16 | /// Wikia Exception code. 17 | public int ErrorCode { get; } 18 | 19 | /// Wikia Exception details. 20 | public string ErrorDetails { get; } 21 | 22 | /// Wikia error trace ID. 23 | public string TraceId { get; } 24 | 25 | public WikiaApiException() 26 | : this("The Wikia API invocation has failed.") 27 | { 28 | } 29 | 30 | public WikiaApiException(string errorType, string errorMessage, int errorCode, string errorDetails, string traceId) 31 | : this(null, errorType, errorMessage, errorCode, errorDetails, traceId) 32 | { 33 | } 34 | 35 | public WikiaApiException(string message, string errorType, string errorMessage, int errorCode, 36 | string errorDetails, string traceId) 37 | { 38 | if (message != null) 39 | Message = message; 40 | else if (errorDetails != null) 41 | Message = $"{errorType}:{errorMessage} ({errorCode}); {errorDetails}"; 42 | else 43 | Message = $"{errorType}:{errorMessage} ({errorCode})"; 44 | ErrorType = errorType; 45 | ErrorMessage = errorMessage; 46 | ErrorCode = errorCode; 47 | ErrorDetails = errorDetails; 48 | TraceId = traceId; 49 | } 50 | 51 | public WikiaApiException(string message) : this(message, null, null, 0, null, null) 52 | { 53 | } 54 | 55 | /// 56 | public override string Message { get; } 57 | 58 | } 59 | 60 | /// 61 | /// The CLR counterpart for Wikia NotFoundException (previously, NotFoundApiException). 62 | /// 63 | public class NotFoundApiException : WikiaApiException 64 | { 65 | 66 | public NotFoundApiException() : base() 67 | { 68 | } 69 | 70 | public NotFoundApiException(string errorType, string errorMessage, int errorCode, 71 | string errorDetails, string traceId) 72 | : base(errorType, errorMessage, errorCode, errorDetails, traceId) 73 | { 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /WikiClientLibrary.Wikia/WikiaHtmlResponseParser.cs: -------------------------------------------------------------------------------- 1 | using HtmlAgilityPack; 2 | using WikiClientLibrary.Client; 3 | 4 | namespace WikiClientLibrary.Wikia; 5 | 6 | public class WikiaHtmlResponseParser : WikiResponseMessageParser 7 | { 8 | 9 | internal static readonly WikiaHtmlResponseParser Default = new WikiaHtmlResponseParser(); 10 | 11 | /// 12 | public override async Task ParseResponseAsync(HttpResponseMessage response, WikiResponseParsingContext context) 13 | { 14 | if (!response.IsSuccessStatusCode) 15 | { 16 | context.NeedRetry = true; 17 | response.EnsureSuccessStatusCode(); 18 | } 19 | var doc = new HtmlDocument(); 20 | // TODO buffer stream, instead of reading all 21 | var content = await response.Content.ReadAsStringAsync(); 22 | doc.LoadHtml(content); 23 | return doc; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /WikiClientLibrary.Wikia/WikiaNamespaces.cs: -------------------------------------------------------------------------------- 1 | namespace WikiClientLibrary.Wikia; 2 | 3 | /// 4 | /// Contains Wikia-specific namespace IDs. 5 | /// 6 | public static class WikiaNamespaces 7 | { 8 | 9 | public const int MessageWall = 1200; 10 | public const int Thread = 1201; 11 | public const int Board = 2000; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /WikiClientLibrary.Wikia/WikiaSiteOptions.cs: -------------------------------------------------------------------------------- 1 | using WikiClientLibrary.Infrastructures; 2 | using WikiClientLibrary.Sites; 3 | using WikiClientLibrary.Wikia.Sites; 4 | 5 | namespace WikiClientLibrary.Wikia; 6 | 7 | /// 8 | /// Contains configuration for . 9 | /// 10 | public sealed class WikiaSiteOptions : SiteOptions 11 | { 12 | 13 | /// Initializes an empty instance. 14 | /// You may use overload, which might be handy. 15 | public WikiaSiteOptions() 16 | { 17 | } 18 | 19 | /// Initializes a new instance from the information in . 20 | /// The existing wiki site instance. 21 | /// is null. 22 | public WikiaSiteOptions(WikiSite site) 23 | { 24 | if (site == null) throw new ArgumentNullException(nameof(site)); 25 | ApiEndpoint = site.ApiEndpoint; 26 | var siteInfo = site.SiteInfo; 27 | ScriptUrl = MediaWikiHelper.MakeAbsoluteUrl(siteInfo.ServerUrl, siteInfo.ScriptFilePath); 28 | NirvanaEndPointUrl = MediaWikiHelper.MakeAbsoluteUrl(siteInfo.ServerUrl, "wikia.php"); 29 | WikiaApiRootUrl = MediaWikiHelper.MakeAbsoluteUrl(siteInfo.ServerUrl, "api/v1"); 30 | } 31 | 32 | /// Initializes a new instance from the root URL of a Wikia site. 33 | /// Wikia site root URL, with the ending slash. e.g. http://community.wikia.com/. 34 | /// is null. 35 | public WikiaSiteOptions(string siteRootUrl) 36 | { 37 | if (siteRootUrl == null) throw new ArgumentNullException(nameof(siteRootUrl)); 38 | ApiEndpoint = MediaWikiHelper.MakeAbsoluteUrl(siteRootUrl, "api.php"); 39 | ScriptUrl = MediaWikiHelper.MakeAbsoluteUrl(siteRootUrl, "index.php"); 40 | NirvanaEndPointUrl = MediaWikiHelper.MakeAbsoluteUrl(siteRootUrl, "wikia.php"); 41 | WikiaApiRootUrl = MediaWikiHelper.MakeAbsoluteUrl(siteRootUrl, "api/v1"); 42 | } 43 | 44 | /// 45 | /// MediaWiki script URL, as in . 46 | /// 47 | /// Typically, the value is (Server URL)/index.php. 48 | public string ScriptUrl { get; set; } 49 | 50 | /// 51 | /// Wikia Nirvana endpoint URL. 52 | /// 53 | /// Typically, the value is (Server URL)/wikia.php. 54 | public string NirvanaEndPointUrl { get; set; } 55 | 56 | /// 57 | /// Root URL of Wikia public REST-ful API v1, without the suffixing slash. 58 | /// 59 | /// Typically, the value is (Server URL)/api/v1. 60 | public string WikiaApiRootUrl { get; set; } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /WikiClientLibrary.Wikibase/Contracts/Entity.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Nodes; 3 | using System.Text.Json.Serialization; 4 | using WikiClientLibrary.Infrastructures; 5 | 6 | namespace WikiClientLibrary.Wikibase.Contracts; 7 | 8 | [JsonContract] 9 | internal sealed class Entity 10 | { 11 | 12 | [JsonExtensionData] 13 | public IDictionary? ExtensionData { get; set; } 14 | 15 | public string? Type { get; set; } 16 | 17 | public string? DataType { get; set; } 18 | 19 | public string? Id { get; set; } 20 | 21 | public IDictionary? Labels { get; set; } 22 | 23 | public IDictionary? Descriptions { get; set; } 24 | 25 | public IDictionary>? Aliases { get; set; } 26 | 27 | public IDictionary>? Claims { get; set; } 28 | 29 | public IDictionary? Sitelinks { get; set; } 30 | 31 | } 32 | 33 | [JsonContract] 34 | internal sealed class MonolingualText 35 | { 36 | 37 | public required string Language { get; set; } 38 | 39 | // This field must exist, even if Remove == true. 40 | // Otherwise, there will be internal_api_error_TypeError 41 | public string Value { get; set; } = ""; 42 | 43 | public bool Remove { get; set; } 44 | 45 | } 46 | 47 | [JsonContract] 48 | internal sealed class Claim 49 | { 50 | 51 | public Snak? MainSnak { get; set; } 52 | 53 | public string? Type { get; set; } 54 | 55 | public IDictionary>? Qualifiers { get; set; } 56 | 57 | [JsonPropertyName("qualifiers-order")] 58 | public IList? QualifiersOrder { get; set; } 59 | 60 | public string? Id { get; set; } 61 | 62 | public string? Rank { get; set; } 63 | 64 | public IList? References { get; set; } 65 | 66 | public bool Remove { get; set; } 67 | 68 | } 69 | 70 | [JsonContract] 71 | internal sealed class Snak 72 | { 73 | 74 | public string? SnakType { get; set; } 75 | 76 | public required string Property { get; set; } 77 | 78 | public string? Hash { get; set; } 79 | 80 | public JsonObject? DataValue { get; set; } 81 | 82 | public string? DataType { get; set; } 83 | 84 | } 85 | 86 | [JsonContract] 87 | internal sealed class Reference 88 | { 89 | 90 | public string? Hash { get; set; } 91 | 92 | public IDictionary>? Snaks { get; set; } 93 | 94 | [JsonPropertyName("snaks-order")] 95 | public IList? SnaksOrder { get; set; } 96 | 97 | } 98 | 99 | [JsonContract] 100 | internal sealed class SiteLink 101 | { 102 | 103 | public required string Site { get; set; } 104 | 105 | public string? Title { get; set; } 106 | 107 | public IList? Badges { get; set; } 108 | 109 | public bool Remove { get; set; } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /WikiClientLibrary.Wikibase/DataTypes/WbGlobeCoordinate.cs: -------------------------------------------------------------------------------- 1 | namespace WikiClientLibrary.Wikibase.DataTypes; 2 | 3 | /// In Wikibase, represents a point on the globe. 4 | /// 5 | public struct WbGlobeCoordinate : IEquatable 6 | { 7 | 8 | /// 9 | /// Initialize a new instance with coordinate, precision, and globe entity URI. 10 | /// 11 | /// Latitude, in degrees. 12 | /// Longitude, in degrees. 13 | /// Precision, in degrees. 14 | /// Entity URI of the globe. 15 | public WbGlobeCoordinate(double latitude, double longitude, double precision, Uri globe) 16 | { 17 | Latitude = latitude; 18 | Longitude = longitude; 19 | Precision = precision; 20 | Globe = globe ?? throw new ArgumentNullException(nameof(globe)); 21 | } 22 | 23 | /// Latitude, in degrees. 24 | public double Latitude { get; } 25 | 26 | /// Longitude, in degrees. 27 | public double Longitude { get; } 28 | 29 | /// Precision, in degrees. 30 | public double Precision { get; } 31 | 32 | /// Entity URI of the globe. 33 | public Uri Globe { get; } 34 | 35 | /// 36 | public override string ToString() 37 | { 38 | var s = Latitude.ToString(); 39 | if (Latitude >= 0) s += "°N "; 40 | else s += "°S "; 41 | s += Longitude; 42 | if (Longitude >= 0) s += "°E"; 43 | else s += "°W"; 44 | return s; 45 | } 46 | 47 | /// 48 | public bool Equals(WbGlobeCoordinate other) 49 | { 50 | return Latitude.Equals(other.Latitude) && Longitude.Equals(other.Longitude) 51 | && Precision.Equals(other.Precision) && Globe == other.Globe; 52 | } 53 | 54 | /// 55 | public override bool Equals(object? obj) 56 | { 57 | return obj is WbGlobeCoordinate coordinate && Equals(coordinate); 58 | } 59 | 60 | /// 61 | public override int GetHashCode() 62 | { 63 | return HashCode.Combine(Latitude, Longitude, Precision, Globe); 64 | } 65 | 66 | public static bool operator ==(WbGlobeCoordinate left, WbGlobeCoordinate right) 67 | { 68 | return left.Equals(right); 69 | } 70 | 71 | public static bool operator !=(WbGlobeCoordinate left, WbGlobeCoordinate right) 72 | { 73 | return !left.Equals(right); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /WikiClientLibrary.Wikibase/DataTypes/WbMonolingualText.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace WikiClientLibrary.Wikibase.DataTypes; 4 | 5 | /// 6 | /// Represents a text in certain language. 7 | /// 8 | /// The language code is normalized into lower-case in this structure. 9 | public struct WbMonolingualText : IEquatable 10 | { 11 | 12 | public static readonly WbMonolingualText Null = new WbMonolingualText(); 13 | 14 | /// 15 | /// Initializes a new from language code and text. 16 | /// 17 | /// The language code. It will be converted to lower-case. 18 | /// The text. 19 | /// Either or is null. 20 | public WbMonolingualText(string language, string text) 21 | { 22 | if (language == null) throw new ArgumentNullException(nameof(language)); 23 | if (text == null) throw new ArgumentNullException(nameof(text)); 24 | // Simple normalization. 25 | Language = language.Trim().ToLowerInvariant(); 26 | Text = text; 27 | } 28 | 29 | internal WbMonolingualText(string language, string text, bool bypassPreprocess) 30 | { 31 | Debug.Assert(bypassPreprocess); 32 | Debug.Assert(language != null); 33 | Debug.Assert(text != null); 34 | Language = language; 35 | Text = text; 36 | } 37 | 38 | /// The language code. 39 | public string Language { get; } 40 | 41 | /// The text. 42 | public string Text { get; } 43 | 44 | /// 45 | public override string ToString() 46 | { 47 | return "[" + Language + "]" + Text; 48 | } 49 | 50 | /// 51 | public bool Equals(WbMonolingualText other) 52 | { 53 | return string.Equals(Text, other.Text) && string.Equals(Language, other.Language); 54 | } 55 | 56 | /// 57 | public override bool Equals(object? obj) 58 | { 59 | return obj is WbMonolingualText text && Equals(text); 60 | } 61 | 62 | /// 63 | public override int GetHashCode() 64 | { 65 | unchecked 66 | { 67 | return ((Text != null ? Text.GetHashCode() : 0) * 397) ^ (Language != null ? Language.GetHashCode() : 0); 68 | } 69 | } 70 | 71 | public static bool operator ==(WbMonolingualText left, WbMonolingualText right) 72 | { 73 | return left.Equals(right); 74 | } 75 | 76 | public static bool operator !=(WbMonolingualText left, WbMonolingualText right) 77 | { 78 | return !left.Equals(right); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /WikiClientLibrary.Wikibase/EntityEditEntry.cs: -------------------------------------------------------------------------------- 1 | namespace WikiClientLibrary.Wikibase; 2 | 3 | /// 4 | /// Represents an item of coarse-grained modification information on . 5 | /// 6 | public sealed class EntityEditEntry 7 | { 8 | 9 | private EntityEditEntryState _State; 10 | 11 | public EntityEditEntry(string propertyName) : this(propertyName, null, EntityEditEntryState.Updated) 12 | { 13 | } 14 | 15 | public EntityEditEntry(string propertyName, object? value) : this(propertyName, value, EntityEditEntryState.Updated) 16 | { 17 | } 18 | 19 | public EntityEditEntry(string propertyName, object? value, EntityEditEntryState state) 20 | { 21 | PropertyName = propertyName; 22 | Value = value; 23 | State = state; 24 | } 25 | 26 | /// 27 | /// The CLR property name of the changed value. 28 | /// 29 | /// 30 | /// This is usually a property name in class. 31 | /// 32 | public string PropertyName { get; set; } 33 | 34 | /// 35 | /// The new item, updated existing item, or for the deletion, 36 | /// the item that has enough information to determine the item to remove. 37 | /// 38 | public object? Value { get; set; } 39 | 40 | /// 41 | /// The operation performed on this entry. 42 | /// 43 | public EntityEditEntryState State 44 | { 45 | get { return _State; } 46 | set 47 | { 48 | if (value != EntityEditEntryState.Updated && value != EntityEditEntryState.Removed) 49 | throw new ArgumentOutOfRangeException(nameof(value)); 50 | _State = value; 51 | } 52 | } 53 | 54 | } 55 | 56 | /// 57 | /// Used to mark how an item changes in . 58 | /// 59 | public enum EntityEditEntryState 60 | { 61 | 62 | /// 63 | /// Either the entry is a new item, or the value inside the item has been changed. 64 | /// 65 | Updated = 0, 66 | 67 | /// 68 | /// The entry represents an item to be removed. 69 | /// 70 | Removed = 1, 71 | 72 | } 73 | -------------------------------------------------------------------------------- /WikiClientLibrary.Wikibase/EntityExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace WikiClientLibrary.Wikibase; 2 | 3 | public static class EntityExtensions 4 | { 5 | 6 | /// 7 | public static Task RefreshAsync(this IEnumerable entities) 8 | { 9 | return RefreshAsync(entities, EntityQueryOptions.None, null, CancellationToken.None); 10 | } 11 | 12 | /// 13 | public static Task RefreshAsync(this IEnumerable entities, EntityQueryOptions options) 14 | { 15 | return RefreshAsync(entities, options, null, CancellationToken.None); 16 | } 17 | 18 | /// 19 | public static Task RefreshAsync(this IEnumerable entities, EntityQueryOptions options, ICollection languages) 20 | { 21 | return RefreshAsync(entities, options, languages, CancellationToken.None); 22 | } 23 | 24 | /// 25 | /// Asynchronously fetch information for a sequence of entities. 26 | /// 27 | /// A sequence of entities to be refreshed. 28 | /// Provides options when performing the query. 29 | /// 30 | /// Filter down the internationalized values to the specified one or more language codes. 31 | /// Set to null to fetch for all available languages. 32 | /// 33 | /// The token used to cancel the operation. 34 | /// 35 | public static async Task RefreshAsync(this IEnumerable entities, EntityQueryOptions options, 36 | ICollection? languages, CancellationToken cancellationToken) 37 | { 38 | await WikibaseRequestHelper.RefreshEntitiesAsync(entities, options, languages, cancellationToken); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /WikiClientLibrary.Wikibase/IEntity.cs: -------------------------------------------------------------------------------- 1 | using WikiClientLibrary.Wikibase.DataTypes; 2 | 3 | namespace WikiClientLibrary.Wikibase; 4 | 5 | /// 6 | /// Provides basic access to Wikibase entities. 7 | /// 8 | /// The most notable implementations of this interface are and . 9 | public interface IEntity 10 | { 11 | 12 | /// 13 | /// Id of the entity. 14 | /// 15 | /// Item or Property ID, OR null if this is a new entity that has not made any changes. 16 | string? Id { get; } 17 | 18 | /// 19 | /// For property entity, gets the data type of the property. 20 | /// 21 | /// the data type of the value when this property is used in a , or null if not applicable. 22 | WikibaseDataType? DataType { get; } 23 | 24 | /// Gets the labels (aka. names) of the entity. 25 | WbMonolingualTextCollection Labels { get; } 26 | 27 | /// Gets the descriptions of the entity. 28 | WbMonolingualTextCollection Descriptions { get; } 29 | 30 | /// Gets the aliases of the entity. 31 | WbMonolingualTextsCollection Aliases { get; } 32 | 33 | /// Gets the sitelinks of the entity. 34 | EntitySiteLinkCollection SiteLinks { get; } 35 | 36 | /// Gets the claims of the entity. 37 | ClaimCollection Claims { get; } 38 | 39 | /// Wikibase entity type. 40 | EntityType Type { get; } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /WikiClientLibrary.Wikibase/NamespaceDocs.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace WikiClientLibrary.Wikibase; 4 | 5 | /// 6 | /// The root namespace for 7 | /// Wikibase 8 | /// extension support. 9 | /// 10 | /// 11 | /// For example, Wikidata is a Wikibase Repository as well as a Wikibase Client. 12 | /// For the concepts on the Wikibase data model, see mw:Wikibase/DataModel. 13 | /// For a preliminary Wikibase API documentation, see mw:Wikibase/API. 14 | /// 15 | [CompilerGenerated] 16 | internal class NamespaceDoc 17 | { 18 | 19 | } 20 | -------------------------------------------------------------------------------- /WikiClientLibrary.Wikibase/Utility.cs: -------------------------------------------------------------------------------- 1 | namespace WikiClientLibrary.Wikibase; 2 | 3 | internal static class Utility 4 | { 5 | 6 | /// 7 | /// Partitions into a sequence of , 8 | /// each child having the same length, except the last one. 9 | /// 10 | public static IEnumerable> Partition(this IEnumerable source, int partitionSize) 11 | { 12 | if (partitionSize <= 0) throw new ArgumentOutOfRangeException(nameof(partitionSize)); 13 | var list = new List(partitionSize); 14 | foreach (var item in source) 15 | { 16 | list.Add(item); 17 | if (list.Count == partitionSize) 18 | { 19 | yield return list; 20 | list.Clear(); 21 | } 22 | } 23 | if (list.Count > 0) yield return list; 24 | } 25 | 26 | public static string NewClaimGuid(string entityId) 27 | { 28 | return entityId + "$" + Guid.NewGuid().ToString("D"); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /WikiClientLibrary.Wikibase/WikiClientLibrary.Wikibase.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | WikiClientLibrary.Wikibase 6 | CXuesong.MW.WikiClientLibrary.Wikibase 7 | 8 | WikiClientLibrary.Wikibase is a .NET Standard & asynchronous client library for MediaWiki sites with Wikibase support, e.g. Wikidata. 9 | It also contains API for working with Wikibase offline JSON dump. 10 | 11 | $(PackageTags) Wikibase Wikidata WikibaseDump 12 | Enable 13 | Enable 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /WikiClientLibrary.Wikibase/WikibaseTabularData.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Nodes; 2 | using WikiClientLibrary.Infrastructures; 3 | 4 | namespace WikiClientLibrary.Wikibase; 5 | 6 | /// 7 | /// Tabular data allows users to create CSV-like tables of data, 8 | /// and use them from other wikis to create automatic tables, lists, and graphs. 9 | /// 10 | /// 11 | /// See "https://www.mediawiki.org/wiki/Help:Tabular_Data" for more documentation about tabular data. 12 | /// 13 | //[JsonObject(MemberSerialization.OptIn, NamingStrategyType = typeof(CamelCaseNamingStrategy))] 14 | [JsonContract] 15 | internal class WikibaseTabularData // Reserved for future use. 16 | { 17 | 18 | public string? License { get; set; } 19 | 20 | public IDictionary? Description { get; set; } 21 | 22 | public string? Sources { get; set; } 23 | 24 | public JsonObject? Schema { get; set; } 25 | 26 | public JsonArray? Data { get; set; } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /WikiClientLibrary.Wikibase/WikibaseUriFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Diagnostics; 3 | 4 | namespace WikiClientLibrary.Wikibase; 5 | 6 | // TODO: Take over the URI deserialization to leverage cache in WikibaseUriFactory. 7 | 8 | /// 9 | /// A static class provides functionality for caching instances. 10 | /// 11 | public static class WikibaseUriFactory 12 | { 13 | 14 | private static readonly ConcurrentDictionary> cacheDict = new(); 15 | 16 | private static int nextTrimTrigger = 32; 17 | 18 | /// 19 | /// Gets an instance of by absolute URI string. 20 | /// 21 | /// absolute URI of the entity. 22 | /// is null. 23 | /// The provided URI string is not a valid absolute URI. 24 | public static Uri Get(string uri) 25 | { 26 | if (uri == null) throw new ArgumentNullException(nameof(uri)); 27 | Uri? inst = null; 28 | // Fast route 29 | if (cacheDict.TryGetValue(uri, out var r) && r.TryGetTarget(out inst)) 30 | return inst; 31 | // Slow route 32 | cacheDict.AddOrUpdate(uri, 33 | u => new WeakReference(inst = new Uri(u)), 34 | (u, r0) => 35 | { 36 | if (!r0.TryGetTarget(out inst)) 37 | { 38 | inst = new Uri(u); 39 | return new WeakReference(inst); 40 | } 41 | return r0; 42 | }); 43 | var c = cacheDict.Count; 44 | if (c >= nextTrimTrigger) TrimExcess(); 45 | Debug.Assert(inst != null); 46 | return inst; 47 | } 48 | 49 | private static void TrimExcess() 50 | { 51 | foreach (var p in cacheDict) 52 | { 53 | if (!p.Value.TryGetTarget(out _)) cacheDict.TryRemove(p.Key, out _); 54 | } 55 | var c = cacheDict.Count; 56 | Volatile.Write(ref nextTrimTrigger, c > 0x3FFFFFFF ? int.MaxValue : c * 2); 57 | } 58 | 59 | /// 60 | /// Clears all the cached instances. 61 | /// 62 | public static void ClearCache() 63 | { 64 | cacheDict.Clear(); 65 | nextTrimTrigger = 32; 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /WikiClientLibrary.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | NO_INDENT 3 | True 4 | True 5 | True 6 | True 7 | True 8 | True 9 | True 10 | True 11 | True 12 | True 13 | True 14 | True 15 | True 16 | True 17 | True -------------------------------------------------------------------------------- /WikiClientLibrary/AbuseFilters/AbuseFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using System.Text.Json.Serialization; 3 | using WikiClientLibrary.Infrastructures; 4 | 5 | namespace WikiClientLibrary.AbuseFilters; 6 | 7 | [JsonContract] 8 | public sealed class AbuseFilter 9 | { 10 | 11 | public int Id { get; init; } 12 | 13 | public string Description { get; init; } = ""; 14 | 15 | public string Pattern { get; init; } = ""; 16 | 17 | [JsonIgnore] 18 | public IReadOnlyCollection Actions { get; init; } = Array.Empty(); 19 | 20 | [JsonInclude] 21 | [JsonPropertyName("actions")] 22 | private string RawActions 23 | { 24 | init 25 | { 26 | Actions = string.IsNullOrEmpty(value) 27 | ? Array.Empty() 28 | : (IReadOnlyCollection)new ReadOnlyCollection(value.Split(',')); 29 | } 30 | } 31 | 32 | public int Hits { get; init; } 33 | 34 | public string Comments { get; init; } = ""; 35 | 36 | public string? LastEditor { get; init; } 37 | 38 | public DateTime LastEditTime { get; init; } 39 | 40 | [JsonPropertyName("deleted")] 41 | public bool IsDeleted { get; init; } 42 | 43 | [JsonPropertyName("private")] 44 | public bool IsPrivate { get; init; } 45 | 46 | [JsonPropertyName("enabled")] 47 | public bool IsEnabled { get; init; } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /WikiClientLibrary/AbuseFilters/AbuseFilterList.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Nodes; 3 | using WikiClientLibrary.Generators.Primitive; 4 | using WikiClientLibrary.Infrastructures; 5 | using WikiClientLibrary.Sites; 6 | 7 | namespace WikiClientLibrary.AbuseFilters; 8 | 9 | public class AbuseFilterList : WikiList 10 | { 11 | 12 | /// 13 | public AbuseFilterList(WikiSite site) : base(site) 14 | { 15 | } 16 | 17 | /// The filter ID to start enumerating from. 18 | public int StartId { get; set; } 19 | 20 | /// The filter ID to stop enumerating at. 21 | public int EndId { get; set; } 22 | 23 | /// 24 | public override string ListName => "abusefilters"; 25 | 26 | /// 27 | public override IEnumerable> EnumListParameters() 28 | { 29 | // TODO abfshow 30 | return new Dictionary 31 | { 32 | { "abfprop", "id|pattern|description|actions|comments|lasteditor|lastedittime|private|status|hits" }, 33 | { "abflimit", PaginationSize }, 34 | }; 35 | } 36 | 37 | /// 38 | protected override AbuseFilter ItemFromJson(JsonNode json) 39 | { 40 | return json.Deserialize(MediaWikiHelper.WikiJsonSerializerOptions)!; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /WikiClientLibrary/Client/IWikiClient.cs: -------------------------------------------------------------------------------- 1 | namespace WikiClientLibrary.Client; 2 | 3 | /// 4 | /// Provides methods to carry out MediaWiki or other kinds of API invocation. 5 | /// 6 | public interface IWikiClient 7 | { 8 | 9 | /// 10 | /// Performs API invocation on the specified endpoint and gets parsed result. 11 | /// 12 | /// The API endpoint URL. 13 | /// The request message. 14 | /// The parser that checks and parses the API response into the desired CLR object. 15 | /// The cancellation token that will be checked prior to completing the returned task. 16 | /// The parsed response value. The actual object type depends on the . 17 | /// Either , , or is null. 18 | /// Other -specified exceptions. 19 | /// 20 | /// The implementation of this method involves 21 | /// 22 | /// Generating from ; 23 | /// Transmitting , and gets the ; 24 | /// Parsing the using (see ); 25 | /// Retrying if possible; 26 | /// Returning the parsed result, or throwing an exception. 27 | /// 28 | /// 29 | Task InvokeAsync(string endPointUrl, WikiRequestMessage request, 30 | IWikiResponseMessageParser responseParser, CancellationToken cancellationToken); 31 | 32 | } 33 | -------------------------------------------------------------------------------- /WikiClientLibrary/Client/WikiRequestMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace WikiClientLibrary.Client; 4 | 5 | /// 6 | /// The traceable API request message to be sent to the wiki sites. 7 | /// 8 | /// 9 | /// 10 | /// In some cases, the instance of this class can be reused to issue same requests. 11 | /// However, never issue the requests the same instance concurrently. 12 | /// 13 | /// For the role this class plays in invoking wiki API, see . 14 | /// 15 | public abstract class WikiRequestMessage 16 | { 17 | 18 | private static readonly long baseCounter = 19 | (long)(Environment.TickCount ^ RuntimeInformation.OSDescription.GetHashCode()) << 32; 20 | 21 | private static int idCounter; 22 | 23 | /// Id of the request, for tracing. If left null, an automatically-generated id will be used. 24 | protected WikiRequestMessage(string? id) 25 | { 26 | Id = id ?? NextId(); 27 | } 28 | 29 | private static string NextId() 30 | { 31 | var localCounter = Interlocked.Increment(ref idCounter); 32 | return (baseCounter | (uint)localCounter).ToString("X16"); 33 | } 34 | 35 | /// 36 | /// Id of the request, for tracing. 37 | /// 38 | public string Id { get; } 39 | 40 | /// 41 | /// Gets the HTTP method used to send the request. 42 | /// 43 | public abstract HttpMethod GetHttpMethod(); 44 | 45 | /// 46 | /// Gets the URI query part for the endpoint invocation. 47 | /// 48 | /// The URI query part is the part of the request URI on the right-hand-side of 49 | /// the first question mark(?), or null if no question mark or query is appended 50 | /// to the endpoint URL. 51 | /// Returning will cause a single question mark be appended to the 52 | /// endpoint URL when sending the request. 53 | public abstract string? GetHttpQuery(); 54 | 55 | /// 56 | /// Gets the corresponding to this message. 57 | /// 58 | public abstract HttpContent GetHttpContent(); 59 | 60 | /// 61 | public override string ToString() 62 | { 63 | return Id; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /WikiClientLibrary/Client/WikiResponseParsingContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace WikiClientLibrary.Client; 4 | 5 | /// 6 | /// Provides parsing context for . 7 | /// 8 | public class WikiResponseParsingContext 9 | { 10 | 11 | /// The logger. 12 | /// The token used to cancel the operation. 13 | /// is null. 14 | public WikiResponseParsingContext(ILogger logger, CancellationToken cancellationToken) 15 | { 16 | Logger = logger ?? throw new ArgumentNullException(nameof(logger)); 17 | CancellationToken = cancellationToken; 18 | } 19 | 20 | /// 21 | /// The logger. 22 | /// 23 | public ILogger Logger { get; } 24 | 25 | /// 26 | /// The token used to cancel the operation. 27 | /// 28 | public CancellationToken CancellationToken { get; } 29 | 30 | /// 31 | /// When set in implementation, 32 | /// requests for retrying the request. 33 | /// 34 | /// 35 | /// Normally after setting this property to true, a return or throw statement will follow. 36 | /// See for the detailed usage of this property. 37 | /// 38 | public bool NeedRetry { get; set; } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /WikiClientLibrary/Generators/AllCategoriesGenerator.cs: -------------------------------------------------------------------------------- 1 | using WikiClientLibrary.Generators.Primitive; 2 | using WikiClientLibrary.Sites; 3 | 4 | namespace WikiClientLibrary.Generators; 5 | 6 | /// 7 | /// Generates all the categories with or without description pages. 8 | /// 9 | public class AllCategoriesGenerator : WikiPageGenerator 10 | { 11 | 12 | /// 13 | public AllCategoriesGenerator(WikiSite site) : base(site) 14 | { 15 | } 16 | 17 | /// 18 | /// Start listing at this title. The title does not have to exist. 19 | /// 20 | public string StartTitle { get; set; } = "!"; 21 | 22 | /// 23 | /// The page title to stop enumerating at. 24 | /// 25 | public string? EndTitle { get; set; } = null; 26 | 27 | /// 28 | /// Search for all category titles that begin with this value. 29 | /// 30 | public string? Prefix { get; set; } 31 | 32 | /// 33 | /// Minimum number of category members. 34 | /// 35 | public int? MinChildrenCount { get; set; } 36 | 37 | /// 38 | /// Maximum number of category members. 39 | /// 40 | public int? MaxChildrenCount { get; set; } 41 | 42 | /// 43 | public override string ListName => "allcategories"; 44 | 45 | /// 46 | public override IEnumerable> EnumListParameters() 47 | { 48 | return new Dictionary 49 | { 50 | { "acfrom", StartTitle }, 51 | { "acto", EndTitle }, 52 | { "aclimit", PaginationSize }, 53 | { "acprefix", Prefix }, 54 | { "cmin", MinChildrenCount }, 55 | { "cmax", MaxChildrenCount }, 56 | }; 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /WikiClientLibrary/Generators/CategoriesGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Nodes; 2 | using WikiClientLibrary.Generators.Primitive; 3 | using WikiClientLibrary.Infrastructures; 4 | using WikiClientLibrary.Pages; 5 | using WikiClientLibrary.Pages.Queries.Properties; 6 | using WikiClientLibrary.Sites; 7 | 8 | namespace WikiClientLibrary.Generators; 9 | 10 | /// 11 | /// Gets a list of all categories used on the provided pages. 12 | /// (mw:API:Categories, MediaWiki 1.11+) 13 | /// 14 | public class CategoriesGenerator : WikiPagePropertyGenerator 15 | { 16 | 17 | /// 18 | public CategoriesGenerator(WikiSite site) : base(site) 19 | { 20 | } 21 | 22 | /// 23 | public CategoriesGenerator(WikiSite site, WikiPageStub pageStub) : base(site, pageStub) 24 | { 25 | } 26 | 27 | /// 28 | /// Whether to include hidden categories in the returned list. 29 | /// 30 | public PropertyFilterOption HiddenCategoryFilter { get; set; } 31 | 32 | /// 33 | /// Only list these categories. Useful for checking whether a certain page is in a certain category. 34 | /// 35 | public IEnumerable? CategorySelection { get; set; } 36 | 37 | /// 38 | public override string PropertyName => "categories"; 39 | 40 | /// 41 | public override IEnumerable> EnumListParameters() 42 | { 43 | var p = new OrderedKeyValuePairs 44 | { 45 | { "clprop", "sortkey|timestamp|hidden" }, 46 | { "clshow", HiddenCategoryFilter.ToString("hidden", "!hidden", null) }, 47 | { "cllimit", PaginationSize }, 48 | }; 49 | if (CategorySelection != null) p.Add("clcategories", MediaWikiHelper.JoinValues(CategorySelection)); 50 | return p; 51 | } 52 | 53 | /// 54 | protected override WikiPageCategoryInfo ItemFromJson(JsonNode json, JsonObject jpage) 55 | { 56 | return CategoriesPropertyGroup.CategoryInfoFromJson(json); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /WikiClientLibrary/Generators/FileUsageGenerator.cs: -------------------------------------------------------------------------------- 1 | using WikiClientLibrary.Generators.Primitive; 2 | using WikiClientLibrary.Infrastructures; 3 | using WikiClientLibrary.Sites; 4 | 5 | namespace WikiClientLibrary.Generators; 6 | 7 | /// 8 | /// Generates all the pages that transclude the specified file. 9 | /// 10 | /// 11 | /// 12 | /// 13 | public class FileUsageGenerator : WikiPageGenerator 14 | { 15 | 16 | /// 17 | public FileUsageGenerator(WikiSite site) : base(site) 18 | { 19 | } 20 | 21 | /// 22 | /// List pages transclude this file. The file does not need to exist. 23 | public FileUsageGenerator(WikiSite site, string targetTitle) : base(site) 24 | { 25 | TargetTitle = targetTitle; 26 | } 27 | 28 | /// 29 | /// List pages transcluding this file. The file does not need to exist. 30 | /// 31 | public string TargetTitle { get; set; } = ""; 32 | 33 | /// 34 | /// Only list pages in these namespaces. 35 | /// 36 | /// Selected ids of namespace, or null if all the namespaces are selected. 37 | public IEnumerable? NamespaceIds { get; set; } 38 | 39 | /// 40 | /// How to filter redirects in the results. 41 | /// 42 | public PropertyFilterOption RedirectsFilter { get; set; } 43 | 44 | /// 45 | public override string ListName => "imageusage"; 46 | 47 | /// 48 | public override IEnumerable> EnumListParameters() 49 | { 50 | return new Dictionary 51 | { 52 | { "iutitle", TargetTitle }, 53 | { "iunamespace", NamespaceIds == null ? null : MediaWikiHelper.JoinValues(NamespaceIds) }, 54 | { "iufilterredir", RedirectsFilter.ToString("redirects", "nonredirects") }, 55 | { "iulimit", PaginationSize }, 56 | }; 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /WikiClientLibrary/Generators/FilesGenerator.cs: -------------------------------------------------------------------------------- 1 | using WikiClientLibrary.Generators.Primitive; 2 | using WikiClientLibrary.Infrastructures; 3 | using WikiClientLibrary.Pages; 4 | using WikiClientLibrary.Sites; 5 | 6 | namespace WikiClientLibrary.Generators; 7 | 8 | /// 9 | /// Generates pages from all used files on the provided page. 10 | /// (mw:API:Links, MediaWiki 1.11+) 11 | /// 12 | /// 13 | /// 14 | /// 15 | public class FilesGenerator : WikiPagePropertyGenerator 16 | { 17 | 18 | /// 19 | public FilesGenerator(WikiSite site) : base(site) 20 | { 21 | } 22 | 23 | /// 24 | public FilesGenerator(WikiSite site, WikiPageStub pageStub) : base(site, pageStub) 25 | { 26 | } 27 | 28 | /// 29 | /// Only list these files. Useful for checking whether a certain page has a certain file. 30 | /// (MediaWiki 1.17+) 31 | /// 32 | /// a sequence of page titles, or null to list all the used files. 33 | public IEnumerable? MatchingTitles { get; set; } 34 | 35 | /// 36 | /// Gets/sets a value that indicates whether the links should be listed in 37 | /// the descending order. (MediaWiki 1.19+) 38 | /// 39 | public bool OrderDescending { get; set; } 40 | 41 | /// 42 | public override string PropertyName => "images"; 43 | 44 | /// 45 | public override IEnumerable> EnumListParameters() 46 | { 47 | return new Dictionary 48 | { 49 | { "imlimit", PaginationSize }, 50 | { "imimages", MatchingTitles == null ? null : MediaWikiHelper.JoinValues(MatchingTitles) }, 51 | { "imdir", OrderDescending ? "descending" : "ascending" }, 52 | }; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /WikiClientLibrary/Generators/GeoSearchResultItem.cs: -------------------------------------------------------------------------------- 1 | using WikiClientLibrary.Pages; 2 | 3 | namespace WikiClientLibrary.Generators; 4 | 5 | /// 6 | /// An item in the GeoSearch result. 7 | /// 8 | /// 9 | public sealed class GeoSearchResultItem 10 | { 11 | 12 | internal GeoSearchResultItem(WikiPageStub page, GeoCoordinate coordinate, bool isPrimaryCoordinate, double distance) 13 | { 14 | Page = page; 15 | Coordinate = coordinate; 16 | IsPrimaryCoordinate = isPrimaryCoordinate; 17 | Distance = distance; 18 | } 19 | 20 | /// 21 | /// Gets the object's associated page stub. 22 | /// 23 | public WikiPageStub Page { get; } 24 | 25 | /// 26 | /// Gets the coordinate of the object. 27 | /// 28 | public GeoCoordinate Coordinate { get; } 29 | 30 | /// 31 | /// Gets a value indicating whether the coordinate is the primary coordinate of the object. 32 | /// 33 | public bool IsPrimaryCoordinate { get; } 34 | 35 | /// 36 | /// Distance of the object from the search location, in meters. 37 | /// 38 | /// The value is not set if the search is performed with . 39 | public double Distance { get; } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /WikiClientLibrary/Generators/LinksGenerator.cs: -------------------------------------------------------------------------------- 1 | using WikiClientLibrary.Generators.Primitive; 2 | using WikiClientLibrary.Infrastructures; 3 | using WikiClientLibrary.Pages; 4 | using WikiClientLibrary.Sites; 5 | 6 | namespace WikiClientLibrary.Generators; 7 | 8 | /// 9 | /// Generates pages from all links on the provided page. 10 | /// (mw:API:Links, MediaWiki 1.11+) 11 | /// 12 | /// 13 | /// 14 | /// 15 | public class LinksGenerator : WikiPagePropertyGenerator 16 | { 17 | 18 | /// 19 | public LinksGenerator(WikiSite site) : base(site) 20 | { 21 | } 22 | 23 | /// 24 | public LinksGenerator(WikiSite site, WikiPageStub pageStub) : base(site, pageStub) 25 | { 26 | } 27 | 28 | /// 29 | /// Only list pages in these namespaces. 30 | /// 31 | /// selected IDs of namespace, or null to select all the namespaces. 32 | public IEnumerable? NamespaceIds { get; set; } 33 | 34 | /// 35 | /// Only list links to these titles. Useful for checking whether a certain page links to a certain title. 36 | /// (MediaWiki 1.17+) 37 | /// 38 | /// a sequence of page titles, or null to list all the linked pages. 39 | public IEnumerable? MatchingTitles { get; set; } 40 | 41 | /// 42 | /// Gets/sets a value that indicates whether the links should be listed in 43 | /// the descending order. (MediaWiki 1.19+) 44 | /// 45 | public bool OrderDescending { get; set; } 46 | 47 | /// 48 | public override string PropertyName => "links"; 49 | 50 | /// 51 | public override IEnumerable> EnumListParameters() 52 | { 53 | return new Dictionary 54 | { 55 | { "plnamespace", NamespaceIds == null ? null : MediaWikiHelper.JoinValues(NamespaceIds) }, 56 | { "pllimit", PaginationSize }, 57 | { "pltitles", MatchingTitles == null ? null : MediaWikiHelper.JoinValues(MatchingTitles) }, 58 | { "pldir", OrderDescending ? "descending" : "ascending" }, 59 | }; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /WikiClientLibrary/Generators/MyWatchlistGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Nodes; 2 | using WikiClientLibrary.Generators.Primitive; 3 | using WikiClientLibrary.Infrastructures; 4 | using WikiClientLibrary.Sites; 5 | 6 | namespace WikiClientLibrary.Generators; 7 | 8 | /// 9 | /// Get all pages on the current user's watchlist. 10 | /// 11 | public class MyWatchlistGenerator : WikiPageGenerator 12 | { 13 | 14 | public MyWatchlistGenerator(WikiSite site) : base(site) 15 | { 16 | } 17 | 18 | /// 19 | /// Only list pages in the given namespaces. 20 | /// 21 | public IEnumerable? NamespaceIds { get; set; } 22 | 23 | /// 24 | /// Only show pages that have not been changed. 25 | /// 26 | public PropertyFilterOption NotChangedPagesFilter { get; set; } 27 | 28 | /// 29 | /// Gets/sets a value that indicates whether the links should be listed in 30 | /// the descending order. (MediaWiki 1.19+) 31 | /// 32 | public bool OrderDescending { get; set; } 33 | 34 | /// 35 | /// Title (with namespace prefix) to begin enumerating from. 36 | /// 37 | public string? FromTitle { get; set; } 38 | 39 | /// 40 | /// Title (with namespace prefix) to stop enumerating at. 41 | /// 42 | public string? ToTitle { get; set; } 43 | 44 | public override IEnumerable> EnumListParameters() 45 | { 46 | return new Dictionary 47 | { 48 | { "wrnamespace", NamespaceIds == null ? null : MediaWikiHelper.JoinValues(NamespaceIds) }, 49 | { "wrlimit", PaginationSize }, 50 | { "wrprop", "changed" }, 51 | { "wrshow", NotChangedPagesFilter.ToString("!changed", "changed", null) }, 52 | { "wrdir", OrderDescending ? "descending" : "ascending" }, 53 | { "wrfromtitle", FromTitle }, 54 | { "wrtotitle", ToTitle }, 55 | }; 56 | } 57 | 58 | protected override MyWatchlistResultItem ItemFromJson(JsonNode json) 59 | { 60 | DateTime? changedTime = null; 61 | if (json["changed"] != null) 62 | { 63 | changedTime = DateTime.Parse((string)json["changed"]); 64 | } 65 | 66 | return new MyWatchlistResultItem(MediaWikiHelper.PageStubFromJson(json.AsObject()), 67 | json["changed"] != null, changedTime); 68 | } 69 | 70 | public override string ListName => "watchlistraw"; 71 | 72 | } 73 | -------------------------------------------------------------------------------- /WikiClientLibrary/Generators/MyWatchlistResultItem.cs: -------------------------------------------------------------------------------- 1 | using WikiClientLibrary.Pages; 2 | 3 | namespace WikiClientLibrary.Generators; 4 | 5 | public sealed class MyWatchlistResultItem 6 | { 7 | 8 | public WikiPageStub Page { get; } 9 | public bool IsChanged { get; } 10 | public DateTime? ChangedTime { get; } 11 | 12 | public MyWatchlistResultItem(WikiPageStub page, bool isChanged, DateTime? changedTime = null) 13 | { 14 | Page = page; 15 | IsChanged = isChanged; 16 | ChangedTime = changedTime; 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /WikiClientLibrary/Generators/PagesWithPropGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Nodes; 2 | using WikiClientLibrary.Generators.Primitive; 3 | using WikiClientLibrary.Infrastructures; 4 | using WikiClientLibrary.Sites; 5 | 6 | namespace WikiClientLibrary.Generators; 7 | 8 | /// 9 | /// List all pages using a given page property. 10 | /// 11 | /// 12 | public class PagesWithPropGenerator : WikiPageGenerator 13 | { 14 | 15 | public PagesWithPropGenerator(WikiSite site, string propertyName) : base(site) 16 | { 17 | PropertyName = propertyName; 18 | } 19 | 20 | /// 21 | /// Page property for which to enumerate pages. 22 | /// 23 | /// 24 | /// 25 | /// The list of available properties can be found at 26 | /// action=query&list=pagepropnames. 27 | /// A non-exhaustive example includes 28 | /// 29 | /// defaultsort 30 | /// disambiguation 31 | /// displaytitle 32 | /// forcetoc 33 | /// 34 | /// 35 | /// 36 | public string PropertyName { get; set; } 37 | 38 | /// 39 | /// Gets/sets a value that indicates whether the links should be listed in 40 | /// the descending order. (MediaWiki 1.19+) 41 | /// 42 | public bool OrderDescending { get; set; } 43 | 44 | public override string ListName => "pageswithprop"; 45 | 46 | public override IEnumerable> EnumListParameters() 47 | { 48 | return new Dictionary 49 | { 50 | { "pwpprop", "ids|title|value" }, 51 | { "pwppropname", PropertyName }, 52 | { "pwplimit", PaginationSize }, 53 | { "pwpdir", OrderDescending ? "descending" : "ascending" }, 54 | }; 55 | } 56 | 57 | protected override PagesWithPropResultItem ItemFromJson(JsonNode json) 58 | { 59 | return new PagesWithPropResultItem(MediaWikiHelper.PageStubFromJson(json.AsObject()), (string)json["value"]); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /WikiClientLibrary/Generators/PagesWithPropResultItem.cs: -------------------------------------------------------------------------------- 1 | using WikiClientLibrary.Pages; 2 | 3 | namespace WikiClientLibrary.Generators; 4 | 5 | public class PagesWithPropResultItem 6 | { 7 | 8 | public WikiPageStub Page { get; } 9 | public string Value { get; set; } 10 | 11 | public PagesWithPropResultItem(WikiPageStub wikiPageStub, string value) 12 | { 13 | Page = wikiPageStub; 14 | Value = value; 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /WikiClientLibrary/Generators/Primitive/WikiListCompatibilityOptions.cs: -------------------------------------------------------------------------------- 1 | namespace WikiClientLibrary.Generators.Primitive; 2 | 3 | /// 4 | /// Provides more options on detailed behavior of -derived instances. 5 | /// Most of the options are provided for compatibility purpose. 6 | /// 7 | /// 8 | public class WikiListCompatibilityOptions 9 | { 10 | 11 | /// 12 | /// Specifies the behavior when detects itself is getting into a loop 13 | /// due to the continuation parameter set provided by the server shares the exact same value. 14 | /// 15 | public WikiListContinuationLoopBehaviors ContinuationLoopBehaviors { get; set; } 16 | 17 | } 18 | 19 | /// 20 | /// Controls the behavior when detects itself is getting into a loop 21 | /// due to the continuation parameter set provided by the server has the exact same values 22 | /// as query parameters. 23 | /// 24 | /// 25 | /// On old MediaWiki builds with raw query continuation, 26 | /// if there are too many logs in the same timestamp (seconds precision), such situation can happen. If there are 100 logs sharing 27 | /// the same timestamp (truncated into seconds), while we only take first 50 of them as the first page, 28 | /// the continuation parameter set will indicate the next batch starts with the same timestamp as the first item, 29 | /// eventually causing client to fetch the next batch with the exactly same set of parameters. 30 | /// 31 | [Flags] 32 | public enum WikiListContinuationLoopBehaviors 33 | { 34 | 35 | /// 36 | /// Do nothing. This will cause an to be thrown. 37 | /// 38 | None = 0, 39 | 40 | /// 41 | /// Tries to fetch more items, so the last item might have a different timestamp, causing the continuation continues. 42 | /// will fetch for 1000 items at most, depending on whether the user has `apihighlimits` right. 43 | /// If it still cannot get out of the continuation loop, an will be thrown. 44 | /// 45 | FetchMore = 1, 46 | 47 | // TODO implement the following options. 48 | ///// 49 | ///// Tries to increment/decrement the raw query continuation parameter value, so as to get out of the continuation loop, 50 | ///// at the cost of skipping some of the items. 51 | ///// 52 | //SkipItems = 2, 53 | ///// 54 | ///// Tries first, then . 55 | ///// 56 | //FetchMoreThenSkip = FetchMore | SkipItems, 57 | 58 | } 59 | -------------------------------------------------------------------------------- /WikiClientLibrary/Generators/RandomPageGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Nodes; 2 | using WikiClientLibrary.Generators.Primitive; 3 | using WikiClientLibrary.Infrastructures; 4 | using WikiClientLibrary.Pages; 5 | using WikiClientLibrary.Sites; 6 | 7 | namespace WikiClientLibrary.Generators; 8 | 9 | /// 10 | /// Gets random pages in a specific namespace. 11 | /// (mw:API:Random, MediaWiki 1.12+) 12 | /// 13 | /// 14 | /// The max allowed is 10 for regular users, 15 | /// and 20 for users with the apihighlimits right (typically in bot or sysop group). 16 | /// 17 | /// 18 | public class RandomPageGenerator : WikiPageGenerator 19 | { 20 | 21 | /// 22 | public RandomPageGenerator(WikiSite site) : base(site) 23 | { 24 | } 25 | 26 | /// 27 | protected override WikiPageStub ItemFromJson(JsonNode json) 28 | { 29 | // Note: page ID is contained in ["id"] rather than ["pageid"]. 30 | return new WikiPageStub((long)json["id"], (string?)json["title"], (int)json["ns"]); 31 | } 32 | 33 | /// 34 | /// Only list pages in these namespaces. 35 | /// 36 | /// Selected ids of namespace, or null if all the namespaces are selected. 37 | public IEnumerable? NamespaceIds { get; set; } 38 | 39 | /// 40 | /// How to filter redirects. 41 | /// 42 | public PropertyFilterOption RedirectsFilter { get; set; } 43 | 44 | /// 45 | public override string ListName => "random"; 46 | 47 | /// 48 | public override IEnumerable> EnumListParameters() 49 | { 50 | var dict = new Dictionary 51 | { 52 | { "rnnamespace", NamespaceIds == null ? null : MediaWikiHelper.JoinValues(NamespaceIds) }, { "rnlimit", PaginationSize }, 53 | }; 54 | if (Site.SiteInfo.Version >= new MediaWikiVersion(1, 26)) 55 | { 56 | dict.Add("rnfilterredir", RedirectsFilter.ToString("redirects", "nonredirects")); 57 | } 58 | else if (RedirectsFilter == PropertyFilterOption.WithProperty) 59 | { 60 | dict.Add("rnredirect", true); 61 | // for MW 1.26-, we cannot really implement RedirectsFilter == PropertyFilterOption.WithoutProperty 62 | } 63 | return dict; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /WikiClientLibrary/Generators/SearchResultItem.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using WikiClientLibrary.Infrastructures; 3 | 4 | namespace WikiClientLibrary.Generators; 5 | 6 | /// 7 | /// Represents an item in the search result. 8 | /// 9 | [JsonContract] 10 | public sealed class SearchResultItem 11 | { 12 | 13 | /// 14 | /// ID of the page. 15 | /// 16 | [JsonPropertyName("pageid")] 17 | public long Id { get; init; } 18 | 19 | /// 20 | /// Namespace id of the page. 21 | /// 22 | [JsonPropertyName("ns")] 23 | public int NamespaceId { get; init; } 24 | 25 | /// 26 | /// Gets the full title of the page. 27 | /// 28 | public string Title { get; init; } = ""; 29 | 30 | /// 31 | /// Gets the content length, in bytes. 32 | /// 33 | [JsonPropertyName("size")] 34 | public int ContentLength { get; init; } 35 | 36 | /// 37 | /// Gets the word count. 38 | /// 39 | public int WordCount { get; init; } 40 | 41 | /// 42 | /// Gets the parsed HTML snippet of the page. 43 | /// 44 | public string Snippet { get; init; } = ""; 45 | 46 | /// 47 | /// Gets the timestamp of when the page was last edited. 48 | /// 49 | public DateTime TimeStamp { get; init; } 50 | 51 | /// 52 | public override string ToString() 53 | { 54 | return $"[{Id}]{Title}"; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /WikiClientLibrary/Generators/TranscludedInGenerator.cs: -------------------------------------------------------------------------------- 1 | using WikiClientLibrary.Generators.Primitive; 2 | using WikiClientLibrary.Infrastructures; 3 | using WikiClientLibrary.Sites; 4 | 5 | namespace WikiClientLibrary.Generators; 6 | 7 | /// 8 | /// Generates all the pages that transclude the specified title. 9 | /// 10 | /// 11 | /// 12 | /// 13 | public class TranscludedInGenerator : WikiPageGenerator 14 | { 15 | 16 | /// 17 | public TranscludedInGenerator(WikiSite site) : base(site) 18 | { 19 | } 20 | 21 | /// 22 | /// List pages transclude this title. The title does not need to exist. 23 | public TranscludedInGenerator(WikiSite site, string targetTitle) : base(site) 24 | { 25 | TargetTitle = targetTitle; 26 | } 27 | 28 | /// 29 | /// List pages transcluding this title. The title does not need to exist. 30 | /// 31 | public string TargetTitle { get; set; } = ""; 32 | 33 | /// 34 | /// Only list pages in these namespaces. 35 | /// 36 | /// Selected ids of namespace, or null if all the namespaces are selected. 37 | public IEnumerable? NamespaceIds { get; set; } 38 | 39 | /// 40 | /// How to filter redirects in the results. 41 | /// 42 | public PropertyFilterOption RedirectsFilter { get; set; } 43 | 44 | /// 45 | public override string ListName => "embeddedin"; 46 | 47 | /// 48 | public override IEnumerable> EnumListParameters() 49 | { 50 | return new Dictionary 51 | { 52 | { "eititle", TargetTitle }, 53 | { "einamespace", NamespaceIds == null ? null : MediaWikiHelper.JoinValues(NamespaceIds) }, 54 | { "eifilterredir", RedirectsFilter.ToString("redirects", "nonredirects") }, 55 | { "eilimit", PaginationSize }, 56 | }; 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /WikiClientLibrary/Generators/TransclusionsGenerator.cs: -------------------------------------------------------------------------------- 1 | using WikiClientLibrary.Generators.Primitive; 2 | using WikiClientLibrary.Infrastructures; 3 | using WikiClientLibrary.Pages; 4 | using WikiClientLibrary.Sites; 5 | 6 | namespace WikiClientLibrary.Generators; 7 | 8 | /// 9 | /// Generates pages from all pages (typically templates) transcluded in the provided page. 10 | /// (mw:API:Templates, MediaWiki 1.11+) 11 | /// 12 | /// 13 | /// 14 | public class TransclusionsGenerator : WikiPagePropertyGenerator 15 | { 16 | 17 | /// 18 | public TransclusionsGenerator(WikiSite site) : base(site) 19 | { 20 | } 21 | 22 | /// 23 | public TransclusionsGenerator(WikiSite site, WikiPageStub pageStub) : base(site, pageStub) 24 | { 25 | } 26 | 27 | /// 28 | /// Only list pages in these namespaces. 29 | /// 30 | /// Selected IDs of namespace, or null if all the namespaces are selected. 31 | public IEnumerable? NamespaceIds { get; set; } 32 | 33 | /// 34 | /// Only list transclusion to these titles. Useful for checking whether a certain page links to a certain title. 35 | /// (MediaWiki 1.17+) 36 | /// 37 | /// A sequence of page titles, or null to list all the linked pages. 38 | public IEnumerable? MatchingTitles { get; set; } 39 | 40 | /// 41 | /// Gets/sets a value that indicates whether the links should be listed in 42 | /// the descending order. (MediaWiki 1.19+) 43 | /// 44 | public bool OrderDescending { get; set; } 45 | 46 | /// 47 | public override string PropertyName => "templates"; 48 | 49 | /// 50 | public override IEnumerable> EnumListParameters() 51 | { 52 | return new Dictionary 53 | { 54 | { "tlnamespace", NamespaceIds == null ? null : MediaWikiHelper.JoinValues(NamespaceIds) }, 55 | { "tllimit", PaginationSize }, 56 | { "tltemplates", MatchingTitles == null ? null : MediaWikiHelper.JoinValues(MatchingTitles) }, 57 | { "tldir", OrderDescending ? "descending" : "ascending" }, 58 | }; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /WikiClientLibrary/Generators/WikiPageExtensions.cs: -------------------------------------------------------------------------------- 1 | using WikiClientLibrary.Pages; 2 | 3 | namespace WikiClientLibrary.Generators; 4 | 5 | /// 6 | /// Extension method for constructing generators from . 7 | /// 8 | public static class WikiPageExtensions 9 | { 10 | 11 | /// 12 | /// Creates a instance from the specified page, 13 | /// which generates pages from all links on the page. 14 | /// 15 | /// The page. 16 | public static LinksGenerator CreateLinksGenerator(this WikiPage page) 17 | { 18 | if (page == null) throw new ArgumentNullException(nameof(page)); 19 | return new LinksGenerator(page.Site, page.PageStub); 20 | } 21 | 22 | /// 23 | /// Creates a instance from the specified page, 24 | /// which generates files from all used files on the page. 25 | /// 26 | /// The page. 27 | public static FilesGenerator CreateFilesGenerator(this WikiPage page) 28 | { 29 | if (page == null) throw new ArgumentNullException(nameof(page)); 30 | return new FilesGenerator(page.Site, page.PageStub); 31 | } 32 | 33 | /// 34 | /// Creates a instance from the specified page, 35 | /// which generates pages from all pages (typically templates) transcluded in the page. 36 | /// 37 | /// The page. 38 | public static TransclusionsGenerator CreateTransclusionsGenerator(this WikiPage page) 39 | { 40 | if (page == null) throw new ArgumentNullException(nameof(page)); 41 | return new TransclusionsGenerator(page.Site, page.PageStub); 42 | } 43 | 44 | /// 45 | /// Creates a instance from the specified page, 46 | /// which enumerates the sequence of revisions on the page. 47 | /// 48 | /// The page. 49 | public static RevisionsGenerator CreateRevisionsGenerator(this WikiPage page) 50 | { 51 | if (page == null) throw new ArgumentNullException(nameof(page)); 52 | return new RevisionsGenerator(page.Site, page.PageStub); 53 | } 54 | 55 | /// 56 | /// Creates a instance from the specified page, 57 | /// which enumerates the categories used on the page. 58 | /// 59 | /// The page. 60 | public static CategoriesGenerator CreateCategoriesGenerator(this WikiPage page) 61 | { 62 | if (page == null) throw new ArgumentNullException(nameof(page)); 63 | return new CategoriesGenerator(page.Site, page.PageStub); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /WikiClientLibrary/IWikiClientAsyncInitialization.cs: -------------------------------------------------------------------------------- 1 | namespace WikiClientLibrary; 2 | 3 | /// 4 | /// Provides properties to expose the asynchronous initialization status of an instance. 5 | /// 6 | /// 7 | /// When instantiating types implementing this interface, you need to wait for 8 | /// to complete before you access any other members of the type. 9 | /// For more about asynchronous initialization, see the blog post 10 | /// Async OOP 2: Constructors. 11 | /// 12 | public interface IWikiClientAsyncInitialization 13 | { 14 | 15 | /// 16 | /// A task that indicates the asynchronous initialization status of this instance. 17 | /// 18 | Task Initialization { get; } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /WikiClientLibrary/IWikiClientLoggable.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Microsoft.Extensions.Logging.Abstractions; 3 | 4 | namespace WikiClientLibrary; 5 | 6 | /// 7 | /// Provides methods for setting up logger for a class. 8 | /// 9 | public interface IWikiClientLoggable 10 | { 11 | 12 | /// 13 | /// Replaces the logger factory of the specified instance. 14 | /// 15 | /// Setting this property to null is equivalent to setting it to . 16 | ILogger Logger { get; set; } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /WikiClientLibrary/Infrastructures/AsyncInitializationHelper.cs: -------------------------------------------------------------------------------- 1 | namespace WikiClientLibrary.Infrastructures; 2 | 3 | /// 4 | /// Provides helper methods for . 5 | /// 6 | public static class AsyncInitializationHelper 7 | { 8 | 9 | /// 10 | /// Ensures the instance has been initialized. 11 | /// 12 | /// The instance has not been initialized. 13 | public static void EnsureInitialized(IWikiClientAsyncInitialization obj) 14 | { 15 | if (obj == null) throw new ArgumentNullException(nameof(obj)); 16 | var task = obj.Initialization; 17 | if (task == null) return; 18 | if (task.Status == TaskStatus.RanToCompletion) return; 19 | EnsureInitialized(obj.GetType(), task); 20 | } 21 | 22 | /// 23 | /// Ensures the specified asynchronous initialization task of the instance has completed successfully. 24 | /// 25 | /// The instance has not been initialized. 26 | public static void EnsureInitialized(Type objectType, Task initializationTask) 27 | { 28 | if (objectType == null) throw new ArgumentNullException(nameof(objectType)); 29 | if (initializationTask == null) return; 30 | if (initializationTask.Status == TaskStatus.RanToCompletion) return; 31 | var name = objectType.Name; 32 | throw initializationTask.Status switch 33 | { 34 | TaskStatus.Canceled => new InvalidOperationException(string.Format(Prompts.ExceptionAsyncInitCancelled1, name)), 35 | TaskStatus.Faulted => new InvalidOperationException( 36 | string.Format(Prompts.ExceptionAsyncInitFaulted2, name, initializationTask.Exception), 37 | initializationTask.Exception), 38 | _ => new InvalidOperationException(string.Format(Prompts.ExceptionAsyncInitNotComplete1, name)), 39 | }; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /WikiClientLibrary/Infrastructures/ExecutionContextStash.cs: -------------------------------------------------------------------------------- 1 | namespace WikiClientLibrary.Infrastructures; 2 | 3 | // c.f. https://github.com/dotnet/runtime/issues/47951 4 | // c.f. https://github.com/dotnet/runtime/issues/47802 5 | /// 6 | /// This type is infrastructure of WCL and is not intended to be used directly in your own code. 7 | /// Restores the execution context by calling on this structure. 8 | /// 9 | /// 10 | /// This helper is for correctly restoring execution context (and ) after yield return. 11 | /// Microsoft.Extension.Logging depends on that for scoped logging. 12 | /// 13 | public readonly struct ExecutionContextStash : IDisposable 14 | { 15 | 16 | private readonly ExecutionContext? executionContext; 17 | 18 | public static ExecutionContextStash Capture() 19 | { 20 | return new ExecutionContextStash(ExecutionContext.Capture()); 21 | } 22 | 23 | private ExecutionContextStash(ExecutionContext? context) 24 | { 25 | this.executionContext = context; 26 | } 27 | 28 | /// 29 | /// Restores the captured execution context. 30 | /// 31 | public void RestoreExecutionContext() 32 | { 33 | if (executionContext == null) return; 34 | // Restore execution context inline. 35 | ExecutionContext.Restore(executionContext); 36 | } 37 | 38 | /// Calls . 39 | public void Dispose() => RestoreExecutionContext(); 40 | 41 | } 42 | -------------------------------------------------------------------------------- /WikiClientLibrary/Infrastructures/HttpContentEx.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | using System.Text; 3 | using System.Web; 4 | 5 | namespace WikiClientLibrary.Infrastructures; 6 | 7 | /// 8 | /// A container for name/value tuples encoded using application/x-www-form-urlencoded MIME type. 9 | /// This implementation solves issue #6, that an exception is thrown when the content is too long. 10 | /// 11 | internal class FormLongUrlEncodedContent : ByteArrayContent 12 | { 13 | 14 | // as defined in HttpRuleParser.DefaultHttpEncoding 15 | public static readonly Encoding DefaultHttpEncoding = Encoding.GetEncoding("iso-8859-1"); 16 | 17 | public FormLongUrlEncodedContent(IEnumerable> nameValueCollection) 18 | : base(GetContentByteArray(nameValueCollection)) 19 | { 20 | Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); 21 | } 22 | 23 | private static byte[] GetContentByteArray(IEnumerable> pairs) 24 | { 25 | if (pairs == null) 26 | throw new ArgumentNullException(nameof(pairs)); 27 | var sb = new StringBuilder(); 28 | foreach (var nameValue in pairs) 29 | { 30 | if (sb.Length > 0) 31 | sb.Append('&'); 32 | 33 | sb.Append(HttpUtility.UrlEncode(nameValue.Key)); 34 | sb.Append('='); 35 | sb.Append(HttpUtility.UrlEncode(nameValue.Value)); 36 | } 37 | 38 | return DefaultHttpEncoding.GetBytes(sb.ToString()); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /WikiClientLibrary/Infrastructures/JsonContractAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | 3 | namespace WikiClientLibrary.Infrastructures; 4 | 5 | /// 6 | /// Infrastructure. Not intended to be used directly in your code. 7 | /// Indicates the specified class will be used as JSON contract class with API. 8 | /// 9 | [AttributeUsage(AttributeTargets.Class)] 10 | public class JsonContractAttribute : Attribute 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /WikiClientLibrary/Infrastructures/JsonHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Nodes; 3 | 4 | namespace WikiClientLibrary.Infrastructures; 5 | 6 | public static class JsonHelper 7 | { 8 | 9 | public static void InplaceMerge(JsonNode root1, JsonNode root2) 10 | { 11 | if (root1.GetValueKind() != root2.GetValueKind()) 12 | throw new JsonException($"Attempt to merge {root2.GetValueKind()} into {root1.GetValueKind()}"); 13 | 14 | if (root1 is JsonObject obj1 && root2 is JsonObject obj2) 15 | { 16 | if (obj2.Count == 0) return; 17 | var obj2Entries = obj2.ToList(); 18 | // Detach children from obj2 19 | obj2.Clear(); 20 | foreach (var (key, value2) in obj2Entries) 21 | { 22 | if (obj1.TryGetPropertyValue(key, out var value1)) 23 | { 24 | if (value1 is JsonArray && value2 is JsonArray 25 | || value1 is JsonObject && value2 is JsonObject) 26 | { 27 | InplaceMerge(value1, value2); 28 | } 29 | else 30 | { 31 | obj1[key] = value2; 32 | } 33 | } 34 | else 35 | { 36 | obj1[key] = value2; 37 | } 38 | } 39 | return; 40 | } 41 | if (root1 is JsonArray arr1 && root2 is JsonArray arr2) 42 | { 43 | if (arr2.Count == 0) return; 44 | var arr2Items = arr2.ToList(); 45 | // Detach children from arr2 46 | arr2.Clear(); 47 | foreach (var item in arr2Items) arr1.Add(item); 48 | return; 49 | } 50 | throw new JsonException($"Merging {root1.GetValueKind()} is not supported."); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /WikiClientLibrary/Infrastructures/OrderedKeyValuePairs.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | 3 | namespace WikiClientLibrary.Infrastructures; 4 | 5 | /// 6 | /// A sequence of ordered key-value pairs. The keys can duplicate with each other. 7 | /// 8 | public class OrderedKeyValuePairs : Collection> 9 | { 10 | 11 | public OrderedKeyValuePairs() : this(null) 12 | { 13 | } 14 | 15 | public OrderedKeyValuePairs(IEqualityComparer? keyComparer) 16 | { 17 | KeyComparer = keyComparer ?? EqualityComparer.Default; 18 | } 19 | 20 | public IEqualityComparer KeyComparer { get; } 21 | 22 | public void Add(TKey key, TValue value) 23 | { 24 | Add(new KeyValuePair(key, value)); 25 | } 26 | 27 | public void AddRange(IEnumerable> items) 28 | { 29 | if (items == null) throw new ArgumentNullException(nameof(items)); 30 | foreach (var item in items) Add(item); 31 | } 32 | 33 | public TValue this[TKey key] 34 | { 35 | get 36 | { 37 | foreach (var p in Items) 38 | { 39 | if (KeyComparer.Equals(p.Key, key)) return p.Value; 40 | } 41 | throw new KeyNotFoundException(); 42 | } 43 | set 44 | { 45 | for (var i = 0; i < Items.Count; i++) 46 | { 47 | var p = Items[i]; 48 | if (KeyComparer.Equals(p.Key, key)) 49 | { 50 | Items[i] = new KeyValuePair(key, value); 51 | return; 52 | } 53 | } 54 | Items.Add(new KeyValuePair(key, value)); 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /WikiClientLibrary/Infrastructures/Polyfill.cs: -------------------------------------------------------------------------------- 1 | #if !BCL_FEATURE_REQUIRED_MEMBER 2 | 3 | namespace System.Runtime.CompilerServices 4 | { 5 | internal sealed class RequiredMemberAttribute : Attribute 6 | { 7 | 8 | } 9 | 10 | internal sealed class CompilerFeatureRequiredAttribute : Attribute 11 | { 12 | 13 | public CompilerFeatureRequiredAttribute(string name) { } 14 | 15 | } 16 | } 17 | 18 | namespace System.Diagnostics.CodeAnalysis 19 | { 20 | internal sealed class SetsRequiredMembersAttribute : Attribute 21 | { 22 | 23 | } 24 | } 25 | 26 | #endif 27 | -------------------------------------------------------------------------------- /WikiClientLibrary/Infrastructures/TaskUtility.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace WikiClientLibrary.Infrastructures; 4 | 5 | /// 6 | /// Provides support for asynchronous lazy initialization. This type is fully threadsafe. 7 | /// 8 | /// The type of object that is being asynchronously initialized. 9 | // http://blog.stephencleary.com/2012/08/asynchronous-lazy-initialization.html 10 | internal sealed class AsyncLazy 11 | { 12 | 13 | /// 14 | /// The underlying lazy task. 15 | /// 16 | private readonly Lazy> instance; 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | /// The delegate that is invoked on a background thread to produce the value when it is needed. 22 | public AsyncLazy(Func factory) 23 | { 24 | instance = new Lazy>(() => Task.Run(factory)); 25 | } 26 | 27 | /// 28 | /// Initializes a new instance of the class. 29 | /// 30 | /// The asynchronous delegate that is invoked on a background thread to produce the value when it is needed. 31 | public AsyncLazy(Func> factory) 32 | { 33 | instance = new Lazy>(() => Task.Run(factory)); 34 | } 35 | 36 | /// 37 | /// Asynchronous infrastructure support. This method permits instances of to be await'ed. 38 | /// 39 | public TaskAwaiter GetAwaiter() 40 | { 41 | return instance.Value.GetAwaiter(); 42 | } 43 | 44 | /// 45 | /// Starts the asynchronous initialization, if it has not already started. 46 | /// 47 | public void Start() 48 | { 49 | var unused = instance.Value; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /WikiClientLibrary/Pages/PageFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Nodes; 2 | using WikiClientLibrary.Infrastructures; 3 | using WikiClientLibrary.Sites; 4 | using WikiClientLibrary.Pages.Queries; 5 | 6 | namespace WikiClientLibrary.Pages; 7 | 8 | // Factory methods 9 | partial class WikiPage 10 | { 11 | 12 | /// 13 | /// Creates a list of based on JSON query result. 14 | /// 15 | /// A object. 16 | /// The [root].qurey.pages node value object of JSON result. 17 | /// 18 | /// Retrieved pages. 19 | internal static IList FromJsonQueryResult(WikiSite site, JsonObject jpages, IWikiPageQueryProvider options) 20 | { 21 | if (site == null) throw new ArgumentNullException(nameof(site)); 22 | if (jpages == null) throw new ArgumentNullException(nameof(jpages)); 23 | // If query.pages.xxx.index exists, sort the pages by the given index. 24 | // This is specifically used with SearchGenerator, to keep the search result in order. 25 | // For other generators, this property simply does not exist. OrderBy does stable sort. 26 | // See https://www.mediawiki.org/wiki/API_talk:Query#On_the_order_of_titles_taken_out_of_generator . 27 | return jpages.OrderBy(page => (int?)page.Value["index"] ?? -1) 28 | .Select(page => 29 | { 30 | var newInst = new WikiPage(site, 0); 31 | MediaWikiHelper.PopulatePageFromJson(newInst, page.Value.AsObject(), options); 32 | return newInst; 33 | }).ToList(); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /WikiClientLibrary/Pages/Parsing/ParsedContentInfo.cs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CXuesong/WikiClientLibrary/6bb0095713c41cab297dfc58cf35aee9445117c9/WikiClientLibrary/Pages/Parsing/ParsedContentInfo.cs -------------------------------------------------------------------------------- /WikiClientLibrary/Pages/Parsing/ParsingOptions.cs: -------------------------------------------------------------------------------- 1 | namespace WikiClientLibrary.Pages.Parsing; 2 | 3 | /// 4 | /// Options for page or content parsing. 5 | /// 6 | [Flags] 7 | public enum ParsingOptions 8 | { 9 | 10 | /// 11 | /// No parsing options. 12 | /// 13 | None = 0, 14 | 15 | /// 16 | /// When parsing by page title or page id, returns the target page when meeting redirects. 17 | /// 18 | ResolveRedirects = 1, 19 | 20 | /// 21 | /// Disable table of contents in output. (1.23+) 22 | /// 23 | DisableToc = 2, 24 | 25 | /// 26 | /// Disable edit section links from the parser output. (1.24+) 27 | /// 28 | DisableEditSection = 4, 29 | 30 | /// 31 | /// Do not run HTML cleanup (e.g. tidy) on the parser output. (1.26+) 32 | /// 33 | DisableTidy = 8, 34 | 35 | /// 36 | /// Parse in preview mode. (1.22+) 37 | /// 38 | Preview = 0x1000, 39 | 40 | /// 41 | /// Parse in section preview mode (enables preview mode too). (1.22+) 42 | /// 43 | SectionPreview = 0x2000, 44 | 45 | /// 46 | /// Return parse output in a format suitable for mobile devices. (?) 47 | /// 48 | MobileFormat = 0x4000, 49 | 50 | /// 51 | /// Disable images in mobile output. (?) 52 | /// 53 | NoImages = 0x8000, 54 | 55 | /// 56 | /// Gives the structured limit report. (1.23+) 57 | /// This flag fills . 58 | /// 59 | LimitReport = 0x10000, 60 | 61 | /// 62 | /// Omit the limit report ("NewPP limit report") from the parser output. (1.17~1.22, disablepp; 1.23+, disablelimitreport) 63 | /// will be empty if both this flag and is set. 64 | /// 65 | /// By default, the limit report will be included as comment in the parsed HTML content. 66 | /// This flag can suppress such output. 67 | DisableLimitReport = 0x20000, 68 | 69 | /// 70 | /// Includes language links supplied by extensions, in addition to the links specified on the page. (1.22+) 71 | /// 72 | EffectiveLanguageLinks = 0x40000, 73 | 74 | /// 75 | /// Gives the templates and other transcluded pages/modules in the parsed wikitext. 76 | /// 77 | TranscludedPages = 0x80000, 78 | 79 | } 80 | -------------------------------------------------------------------------------- /WikiClientLibrary/Pages/PurgeFailureInfo.cs: -------------------------------------------------------------------------------- 1 | namespace WikiClientLibrary.Pages; 2 | 3 | /// 4 | /// Represents the details of purge failure on a MediaWiki page. 5 | /// 6 | /// 7 | public class PurgeFailureInfo 8 | { 9 | 10 | internal PurgeFailureInfo(WikiPageStub page, string? invalidReason) 11 | { 12 | Page = page; 13 | InvalidReason = invalidReason; 14 | } 15 | 16 | public WikiPageStub Page { get; } 17 | 18 | public bool IsMissing => Page.IsMissing; 19 | 20 | public bool IsInvalid => Page.IsInvalid; 21 | 22 | public string? InvalidReason { get; } 23 | 24 | /// 25 | public override string ToString() 26 | { 27 | if (InvalidReason != null) 28 | return Page + ": " + InvalidReason; 29 | return Page.ToString(); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /WikiClientLibrary/Pages/Queries/Properties/CategoryInfoPropertyProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Nodes; 2 | 3 | namespace WikiClientLibrary.Pages.Queries.Properties; 4 | 5 | /// 6 | /// Provides information for category pages. 7 | /// (mw:API:Categoryinfo, MediaWiki 1.13+) 8 | /// 9 | public class CategoryInfoPropertyProvider : WikiPagePropertyProvider 10 | { 11 | 12 | /// 13 | public override IEnumerable> EnumParameters(MediaWikiVersion version) 14 | { 15 | return Enumerable.Empty>(); 16 | } 17 | 18 | /// 19 | public override CategoryInfoPropertyGroup? ParsePropertyGroup(JsonObject json) 20 | { 21 | return CategoryInfoPropertyGroup.Create(json); 22 | } 23 | 24 | /// 25 | public override string? PropertyName => "categoryinfo"; 26 | 27 | } 28 | 29 | /// 30 | /// Property group for category page information. 31 | /// 32 | /// 33 | /// For the categories that has sub-items but without category page, this property group is still valid. 34 | /// 35 | public class CategoryInfoPropertyGroup : WikiPagePropertyGroup 36 | { 37 | 38 | public static CategoryInfoPropertyGroup? Create(JsonObject jPage) 39 | { 40 | var cat = jPage["categoryinfo"]; 41 | // jpage["imageinfo"] == null indicates the page may not be a valid Category. 42 | if (cat == null) return null; 43 | return new CategoryInfoPropertyGroup(cat); 44 | } 45 | 46 | private CategoryInfoPropertyGroup(JsonNode jCategoryInfo) 47 | { 48 | MembersCount = (int)jCategoryInfo["size"]; 49 | PagesCount = (int)jCategoryInfo["pages"]; 50 | FilesCount = (int)jCategoryInfo["files"]; 51 | SubcategoriesCount = (int)jCategoryInfo["subcats"]; 52 | } 53 | 54 | /// Count of members in this category. 55 | public int MembersCount { get; } 56 | 57 | /// Count of pages in this category. 58 | public int PagesCount { get; } 59 | 60 | /// Count of files in this category. 61 | public int FilesCount { get; } 62 | 63 | /// Count of sub-categories in this category. 64 | public int SubcategoriesCount { get; } 65 | 66 | /// 67 | public override string ToString() 68 | { 69 | return $"M:{MembersCount}, P:{PagesCount}, S:{SubcategoriesCount}, F:{FilesCount}"; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /WikiClientLibrary/Pages/Queries/Properties/PageInfoPropertyProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using System.Text.Json; 3 | using System.Text.Json.Nodes; 4 | using WikiClientLibrary.Infrastructures; 5 | 6 | namespace WikiClientLibrary.Pages.Queries.Properties; 7 | 8 | public class PageInfoPropertyProvider : WikiPagePropertyProvider 9 | { 10 | 11 | private static readonly IEnumerable> fixedProp = new ReadOnlyCollection>( 12 | new OrderedKeyValuePairs { { "inprop", "protection" } }); 13 | 14 | /// 15 | public override IEnumerable> EnumParameters(MediaWikiVersion version) 16 | { 17 | return fixedProp; 18 | } 19 | 20 | /// 21 | public override PageInfoPropertyGroup? ParsePropertyGroup(JsonObject json) 22 | { 23 | return new PageInfoPropertyGroup(json); 24 | } 25 | 26 | /// 27 | public override string? PropertyName => "info"; 28 | 29 | } 30 | 31 | public class PageInfoPropertyGroup : WikiPagePropertyGroup 32 | { 33 | 34 | protected internal PageInfoPropertyGroup(JsonObject jPage) 35 | { 36 | ContentModel = (string)jPage["contentmodel"]; 37 | PageLanguage = (string)jPage["pagelanguage"]; 38 | PageLanguageDirection = (string)jPage["pagelanguagedir"]; 39 | IsRedirect = jPage["redirect"] != null; 40 | Protections = Array.Empty(); 41 | LastTouched = DateTime.MinValue; 42 | RestrictionTypes = Array.Empty(); 43 | if (jPage["missing"] != null || jPage["invalid"] != null || jPage["special"] != null) 44 | { 45 | ContentLength = 0; 46 | LastRevisionId = 0; 47 | } 48 | else 49 | { 50 | ContentLength = (int)jPage["length"]; 51 | LastRevisionId = (long)jPage["lastrevid"]; 52 | LastTouched = (DateTime)jPage["touched"]; 53 | if (jPage["protection"]?.AsArray().Count > 0) 54 | Protections = jPage["protection"] 55 | .Deserialize>(MediaWikiHelper.WikiJsonSerializerOptions)!; 56 | if (jPage["restrictiontypes"]?.AsArray().Count > 0) 57 | RestrictionTypes = jPage["restrictiontypes"] 58 | .Deserialize>(MediaWikiHelper.WikiJsonSerializerOptions)!; 59 | } 60 | } 61 | 62 | public string ContentModel { get; } 63 | 64 | public string PageLanguage { get; } 65 | 66 | public string PageLanguageDirection { get; } 67 | 68 | public DateTime LastTouched { get; } 69 | 70 | public long LastRevisionId { get; } 71 | 72 | public int ContentLength { get; } 73 | 74 | public bool IsRedirect { get; } 75 | 76 | public IReadOnlyCollection Protections { get; } 77 | 78 | /// 79 | /// Applicable protection types. (MediaWiki 1.25) 80 | /// 81 | public IReadOnlyCollection RestrictionTypes { get; } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /WikiClientLibrary/Pages/Queries/Properties/PagePropertiesPropertyProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Nodes; 3 | using WikiClientLibrary.Infrastructures; 4 | 5 | namespace WikiClientLibrary.Pages.Queries.Properties; 6 | 7 | public class PagePropertiesPropertyProvider : WikiPagePropertyProvider 8 | { 9 | 10 | /// 11 | public override IEnumerable> EnumParameters(MediaWikiVersion version) 12 | { 13 | return new OrderedKeyValuePairs 14 | { 15 | { "ppprop", SelectedProperties == null ? null : MediaWikiHelper.JoinValues(SelectedProperties) }, 16 | }; 17 | } 18 | 19 | /// 20 | public override PagePropertiesPropertyGroup? ParsePropertyGroup(JsonObject json) 21 | { 22 | return PagePropertiesPropertyGroup.Create(json); 23 | } 24 | 25 | /// 26 | /// Only list these page properties (action=query&list=pagepropnames returns page property names in use). 27 | /// Useful for checking whether pages use a certain page property. 28 | /// 29 | /// A sequence of selected property names, or null to select all of the properties. 30 | public IEnumerable? SelectedProperties { get; set; } 31 | 32 | /// 33 | public override string? PropertyName => "pageprops"; 34 | 35 | } 36 | 37 | public class PagePropertiesPropertyGroup : WikiPagePropertyGroup 38 | { 39 | 40 | private static readonly PagePropertiesPropertyGroup Empty = new PagePropertiesPropertyGroup(); 41 | 42 | internal static PagePropertiesPropertyGroup? Create(JsonObject jpage) 43 | { 44 | var props = jpage["pageprops"]?.AsObject(); 45 | // jpage["pageprops"] == null for pages with no pageprop item, 46 | // even if client specified prop=pageprops 47 | // if (props == null) return null; 48 | if (props == null || props.Count == 0) return Empty; 49 | return new PagePropertiesPropertyGroup(props); 50 | } 51 | 52 | private PagePropertiesPropertyGroup() 53 | { 54 | PageProperties = PagePropertyCollection.Empty; 55 | } 56 | 57 | private PagePropertiesPropertyGroup(JsonObject jpageprops) 58 | { 59 | PageProperties = jpageprops.Deserialize(MediaWikiHelper.WikiJsonSerializerOptions) ?? PagePropertyCollection.Empty; 60 | } 61 | 62 | /// 63 | /// Gets the properties of the page. 64 | /// 65 | public PagePropertyCollection PageProperties { get; } 66 | 67 | /// 68 | public override string? ToString() 69 | { 70 | return PageProperties.ToString(); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /WikiClientLibrary/Pages/Queries/Properties/WikiPagePropertyGroup.cs: -------------------------------------------------------------------------------- 1 | namespace WikiClientLibrary.Pages.Queries.Properties; 2 | 3 | /// 4 | /// A marker interface which indicates the implementation type is an immutable group 5 | /// of property values associated with that can be fetched from 6 | /// MediaWiki server. 7 | /// 8 | /// 9 | /// It's recommended that you derive your custom property groups from 10 | /// instead of directly implementing interface. 11 | /// 12 | public interface IWikiPagePropertyGroup 13 | { 14 | 15 | } 16 | 17 | /// 18 | /// 19 | /// A base class for an immutable group of extendable properties. 20 | /// The default implementation for . 21 | /// 22 | public class WikiPagePropertyGroup : IWikiPagePropertyGroup 23 | { 24 | 25 | } 26 | -------------------------------------------------------------------------------- /WikiClientLibrary/Sites/WikiSiteToken.cs: -------------------------------------------------------------------------------- 1 | using WikiClientLibrary.Client; 2 | 3 | namespace WikiClientLibrary.Sites; 4 | 5 | /// 6 | /// Represents a token placeholder in the . 7 | /// This enables to detect bad tokens. 8 | /// 9 | /// 10 | /// For backwards-compatibility, please use the most specific token type where possible 11 | /// (e.g., or instead of ). 12 | /// 13 | public sealed class WikiSiteToken 14 | { 15 | 16 | public static WikiSiteToken Edit = new WikiSiteToken("edit"); 17 | 18 | public static WikiSiteToken Move = new WikiSiteToken("move"); 19 | 20 | public static WikiSiteToken Delete = new WikiSiteToken("delete"); 21 | 22 | public static WikiSiteToken Patrol = new WikiSiteToken("patrol"); 23 | 24 | /// General CSRF token. This token type is not supported prior to MW 1.24. 25 | public static WikiSiteToken Csrf = new WikiSiteToken("csrf"); 26 | 27 | public WikiSiteToken(string type) 28 | { 29 | Type = type ?? throw new ArgumentNullException(nameof(type)); 30 | } 31 | 32 | public string Type { get; } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /WikiClientLibrary/WikiClientLibrary.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | WikiClientLibrary 6 | CXuesong.MW.WikiClientLibrary 7 | 8 | Wiki Client Library is a .NET Standard & asynchronous client library for MediaWiki sites. 9 | 10 | This portable & asynchronous MediaWiki API client provides an easy and asynchronous access to commonly-used MediaWiki API. It has the following features: 11 | 12 | * Queries and edits for pages, including standard pages, category pages, and file pages. 13 | * Queries for category statistical info and its members. 14 | * Queries for basic file info, and file uploading. 15 | * Login/logout via simple asynchronous functions. 16 | * Client code has access to CookieContainer, and have chance to persist it. 17 | * Tokens are hidden in the library functions, so that client won't bother to retrieve them over and over again. 18 | * Query continuations are hidden by IAsyncEnumerable, which will ease the pain when using page generators. 19 | * Other miscellaneous MediaWiki API, such as OpenSearch, Page parsing, and Patrol. 20 | * Scribunto Lua console and server-side module execution support 21 | 22 | Enable 23 | Enable 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | True 37 | True 38 | Prompts.resx 39 | 40 | 41 | 42 | 43 | 44 | ResXFileCodeGenerator 45 | Prompts.Designer.cs 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | --------------------------------------------------------------------------------